python-aidot-cameras 0.5.1__tar.gz → 0.5.4__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.1/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.4}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/pyproject.toml +1 -1
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/device_client.py +57 -7
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/LICENSE +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/README.md +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/setup.cfg +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4}/tests/test_token_refresh.py +0 -0
{python_aidot_cameras-0.5.1/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.4}/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.4
|
|
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.4"
|
|
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"
|
|
@@ -2110,7 +2110,32 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
2110
2110
|
while True:
|
|
2111
2111
|
fr = fifo.read(1024) # AAC wants 1024-sample frames
|
|
2112
2112
|
if fr is None:
|
|
2113
|
-
|
|
2113
|
+
# 160 PCMA samples → 960 resampled @ 48 kHz, but AAC needs 1024.
|
|
2114
|
+
# Every ~17 packets (~340 ms) the FIFO falls 64 samples short
|
|
2115
|
+
# and would skip an entire 21 ms frame — audible choppiness.
|
|
2116
|
+
# Pad the leftover with silence so the frame is complete.
|
|
2117
|
+
s = getattr(fifo, "samples", 0)
|
|
2118
|
+
if s == 0:
|
|
2119
|
+
break
|
|
2120
|
+
fr = fifo.read(s)
|
|
2121
|
+
if fr is None or fr.samples == 0:
|
|
2122
|
+
break
|
|
2123
|
+
if _np is not None:
|
|
2124
|
+
try:
|
|
2125
|
+
arr = fr.to_ndarray()
|
|
2126
|
+
pad_n = 1024 - arr.shape[1]
|
|
2127
|
+
if pad_n > 0:
|
|
2128
|
+
padded = _np.concatenate(
|
|
2129
|
+
[arr, _np.zeros((arr.shape[0], pad_n), dtype=arr.dtype)],
|
|
2130
|
+
axis=1,
|
|
2131
|
+
)
|
|
2132
|
+
pfr = av.AudioFrame.from_ndarray(
|
|
2133
|
+
padded, format="fltp", layout="mono"
|
|
2134
|
+
)
|
|
2135
|
+
pfr.sample_rate = 48000
|
|
2136
|
+
fr = pfr
|
|
2137
|
+
except Exception:
|
|
2138
|
+
break
|
|
2114
2139
|
fr.pts = a_pts[0]
|
|
2115
2140
|
fr.time_base = Fraction(1, 48000)
|
|
2116
2141
|
a_pts[0] += fr.samples
|
|
@@ -2124,7 +2149,7 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
2124
2149
|
while not stop_flag.is_set():
|
|
2125
2150
|
_flush_video()
|
|
2126
2151
|
_flush_audio()
|
|
2127
|
-
_t.sleep(0.
|
|
2152
|
+
_t.sleep(0.005) # 5 ms — processes audio within 5 ms of arrival vs 20 ms
|
|
2128
2153
|
_flush_video()
|
|
2129
2154
|
_flush_audio()
|
|
2130
2155
|
try:
|
|
@@ -3433,7 +3458,7 @@ class DeviceClient(object):
|
|
|
3433
3458
|
"MotionDetection_Enable", "1" if enabled else "0")
|
|
3434
3459
|
|
|
3435
3460
|
async def async_set_floodlight(self, on: bool, brightness: int = 100) -> bool:
|
|
3436
|
-
# Confirmed 2026-05-05: cameras with autoLightEnable=1 (PTZ,
|
|
3461
|
+
# Confirmed 2026-05-05: cameras with autoLightEnable=1 (A001064 PTZ, A000088) ignore
|
|
3437
3462
|
# manual LightOnOff commands unless auto-light is disabled first.
|
|
3438
3463
|
# Turning on: disable auto-light, set LightOnOff=1 (and optional Dimming).
|
|
3439
3464
|
# Turning off: set LightOnOff=0 only (leave autoLightEnable as-is so the
|
|
@@ -4415,14 +4440,32 @@ class DeviceClient(object):
|
|
|
4415
4440
|
import threading as _threading
|
|
4416
4441
|
import queue as _queue
|
|
4417
4442
|
serve_url = self._keepalive_rtsp_url
|
|
4418
|
-
|
|
4443
|
+
# APK parity: the official app gates re-connects to ~15 s (f0.java I1=15000)
|
|
4444
|
+
# and never hammers. We previously floored at 5 s and a partial-success →
|
|
4445
|
+
# reset path could drop the effective spacing even lower, pounding a flaky
|
|
4446
|
+
# A000088 camera fast enough to wedge its DTLS stack (ICE completes but DTLS
|
|
4447
|
+
# never fires until a power-cycle). Floor the backoff at the 15 s gate and,
|
|
4448
|
+
# independently, enforce a hard minimum wall-clock spacing between OPEN
|
|
4449
|
+
# attempts so no code path (reset bug, fast PC-death) can pound the camera.
|
|
4450
|
+
_MIN_DELAY, _MAX_DELAY = 15.0, 300.0
|
|
4451
|
+
_open_gate = float(os.environ.get("AIDOT_DTLS_RETRY_GATE_S", "15"))
|
|
4419
4452
|
retry_delay = _MIN_DELAY
|
|
4453
|
+
_last_open_at = 0.0 # monotonic of the previous open attempt (0 = none yet)
|
|
4420
4454
|
loop = asyncio.get_running_loop()
|
|
4421
4455
|
while self._streaming_active:
|
|
4422
4456
|
# Thread-safe queues: taps run on the loop, the A/V mux in a thread.
|
|
4423
4457
|
vq: "_queue.Queue" = _queue.Queue(maxsize=600)
|
|
4424
4458
|
aq: "_queue.Queue" = _queue.Queue(maxsize=600)
|
|
4425
4459
|
self._serve_ready.clear() # fresh (cold) session: not ready until bound
|
|
4460
|
+
# Hard inter-attempt gate (APK parity): never start an open within
|
|
4461
|
+
# _open_gate seconds of the previous one, regardless of retry_delay.
|
|
4462
|
+
_since_open = loop.time() - _last_open_at
|
|
4463
|
+
if _last_open_at and _since_open < _open_gate:
|
|
4464
|
+
try:
|
|
4465
|
+
await asyncio.sleep(_open_gate - _since_open)
|
|
4466
|
+
except asyncio.CancelledError:
|
|
4467
|
+
return
|
|
4468
|
+
_last_open_at = loop.time()
|
|
4426
4469
|
try:
|
|
4427
4470
|
session = await self.async_open_webrtc_stream(on_frame=lambda _f: None)
|
|
4428
4471
|
except asyncio.CancelledError:
|
|
@@ -4600,6 +4643,13 @@ class DeviceClient(object):
|
|
|
4600
4643
|
return None
|
|
4601
4644
|
cmd = [
|
|
4602
4645
|
"ffmpeg", "-y", "-loglevel", "warning",
|
|
4646
|
+
# Suppress input-side buffering: the PyAV mux already writes
|
|
4647
|
+
# correctly-interleaved, timestamped MPEG-TS to the pipe every
|
|
4648
|
+
# ~20ms. Without +nobuffer ffmpeg's mpegts demuxer accumulates a
|
|
4649
|
+
# read-ahead window (and the output mpegts muxer defaults to 700ms
|
|
4650
|
+
# of A/V interleave delay) before flushing to go2rtc - exactly the
|
|
4651
|
+
# bursty/choppy audio pattern. Matches the SDES serve's approach.
|
|
4652
|
+
"-fflags", "+nobuffer",
|
|
4603
4653
|
"-i", "pipe:0",
|
|
4604
4654
|
"-c", "copy", "-f", "mpegts", "-listen", "1", serve_url,
|
|
4605
4655
|
]
|
|
@@ -5363,7 +5413,7 @@ class DeviceClient(object):
|
|
|
5363
5413
|
liveplay_resp_fut: asyncio.Future = loop.create_future() # set on livePlayResp
|
|
5364
5414
|
camera_reconnect_ev: asyncio.Event = asyncio.Event() # set when camera sends device/connect
|
|
5365
5415
|
# Mutable flag: set True when setDevAttrNotif delivers sptPreconn:1.
|
|
5366
|
-
# Confirmed 2026-05-02: both A000088
|
|
5416
|
+
# Confirmed 2026-05-02: both A000088 and A001064 PTZ report
|
|
5367
5417
|
# sptPreconn:1. AVIO LIVING (SESSION_MODE_REQ=5376) must be sent
|
|
5368
5418
|
# via the data channel to trigger streaming in PreCon cameras.
|
|
5369
5419
|
_spt_preconn: list = [False]
|
|
@@ -5563,7 +5613,7 @@ class DeviceClient(object):
|
|
|
5563
5613
|
# and won't send its own binding requests.
|
|
5564
5614
|
_extract_cam_ip("setDevAttrNotif", inner, msg)
|
|
5565
5615
|
# Also capture sptPreconn (PreCon / PreConnect support flag).
|
|
5566
|
-
# Confirmed 2026-05-02: both A001064 PTZ and A000088
|
|
5616
|
+
# Confirmed 2026-05-02: both A001064 PTZ and A000088 have
|
|
5567
5617
|
# sptPreconn:1. Per BaseKVSCameraView.k():805-815, the official
|
|
5568
5618
|
# client sends AVIO LIVING (SESSION_MODE_REQ=5376) via the data
|
|
5569
5619
|
# channel only when isSupportPreCon() is true. Set the flag
|
|
@@ -6312,7 +6362,7 @@ class DeviceClient(object):
|
|
|
6312
6362
|
|
|
6313
6363
|
# AVIO LIVING (E_CMD_AVIO_CTRL_SESSION_MODE_REQ=5376) is sent
|
|
6314
6364
|
# unconditionally when DC opens. 2026-05-03 testing confirmed:
|
|
6315
|
-
# A000088 cameras
|
|
6365
|
+
# A000088 cameras connect via DTLS,
|
|
6316
6366
|
# SCTP comes up, DC opens at t+1-2s - but camera tears down at
|
|
6317
6367
|
# t+22s if LIVING is not sent. This is the PreCon watchdog:
|
|
6318
6368
|
# without LIVING the camera gives up waiting for a viewer.
|
{python_aidot_cameras-0.5.1 → python_aidot_cameras-0.5.4/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.4
|
|
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
|