python-aidot-cameras 0.7.35__tar.gz → 0.7.36__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.7.35/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.36}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/pyproject.toml +1 -1
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/client.py +18 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.7.36/tests/test_stream_teardown.py +75 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/LICENSE +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/README.md +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/setup.cfg +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/py.typed +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/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.7.
|
|
3
|
+
Version: 0.7.36
|
|
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.7.
|
|
7
|
+
version = "0.7.36"
|
|
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"
|
|
@@ -584,6 +584,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
584
584
|
self._streaming_active: bool = False
|
|
585
585
|
self._stream_session: Optional[Any] = None
|
|
586
586
|
self._stream_task: Optional["asyncio.Task[None]"] = None
|
|
587
|
+
self._stream_mqtt_drain: Optional["asyncio.Future"] = None
|
|
587
588
|
self._last_frame_time: float = 0.0
|
|
588
589
|
self._keepalive_rtsp_url: Optional[str] = None # local serve URL (go2rtc pulls)
|
|
589
590
|
self._go2rtc_url: Optional[str] = None # go2rtc API base (prefer-go2rtc)
|
|
@@ -1981,6 +1982,16 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
1981
1982
|
await task
|
|
1982
1983
|
except (asyncio.CancelledError, Exception):
|
|
1983
1984
|
_LOGGER.debug("camera %s: swallowed exception", 'async_stop_streaming', exc_info=True)
|
|
1985
|
+
# Reap a persistent-MQTT stream drain that no session stopped (e.g. an
|
|
1986
|
+
# open cancelled mid-handshake): cancelling it runs its finally, which
|
|
1987
|
+
# removes the handler from the shared persistent connection.
|
|
1988
|
+
drain, self._stream_mqtt_drain = getattr(self, "_stream_mqtt_drain", None), None
|
|
1989
|
+
if drain is not None and not drain.done():
|
|
1990
|
+
drain.cancel()
|
|
1991
|
+
try:
|
|
1992
|
+
await drain
|
|
1993
|
+
except (asyncio.CancelledError, Exception):
|
|
1994
|
+
_LOGGER.debug("camera %s: swallowed exception", 'async_stop_streaming', exc_info=True)
|
|
1984
1995
|
|
|
1985
1996
|
async def async_start_motion_polling(
|
|
1986
1997
|
self, callback: Callable, interval: float = 30.0, lookback_s: int = 600,
|
|
@@ -4243,6 +4254,13 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
4243
4254
|
_pm_stream.remove_handler(_on_mqtt_message)
|
|
4244
4255
|
|
|
4245
4256
|
mqtt_fut = asyncio.ensure_future(_pm_stream_drain())
|
|
4257
|
+
# Track the drain so teardown can reap it even if this open is
|
|
4258
|
+
# cancelled before a WebRTCSession takes ownership (the session
|
|
4259
|
+
# normally stops it via the outgoing_q sentinel). Without this an
|
|
4260
|
+
# open cancelled mid-handshake leaves the drain blocked on
|
|
4261
|
+
# outgoing_q.get forever and its handler registered on the shared
|
|
4262
|
+
# persistent connection.
|
|
4263
|
+
self._stream_mqtt_drain = mqtt_fut
|
|
4246
4264
|
_on_mqtt_ready({"connected": True, "rc": 0, "rc_str": "persistent"})
|
|
4247
4265
|
else:
|
|
4248
4266
|
mqtt_fut = loop.run_in_executor(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.36
|
|
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,75 @@
|
|
|
1
|
+
"""Unit test: async_stop_streaming reaps an orphaned persistent-MQTT drain.
|
|
2
|
+
|
|
3
|
+
If a stream open is cancelled before a WebRTCSession takes ownership of the
|
|
4
|
+
drain task, the drain would otherwise block forever on outgoing_q.get with its
|
|
5
|
+
handler still registered. async_stop_streaming must cancel it (its finally then
|
|
6
|
+
removes the handler). No camera needed.
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
13
|
+
|
|
14
|
+
import aidot.camera.client as cc
|
|
15
|
+
|
|
16
|
+
_CAM = next(v for v in vars(cc).values()
|
|
17
|
+
if isinstance(v, type) and "async_stop_streaming" in v.__dict__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _bare_client():
|
|
21
|
+
c = _CAM.__new__(_CAM)
|
|
22
|
+
c._streaming_active = True
|
|
23
|
+
c._serve_ready = asyncio.Event()
|
|
24
|
+
c._go2rtc_task = None
|
|
25
|
+
c._stream_session = None
|
|
26
|
+
c._stream_task = None
|
|
27
|
+
c._stream_mqtt_drain = None
|
|
28
|
+
|
|
29
|
+
async def _noop_deregister():
|
|
30
|
+
return None
|
|
31
|
+
c._deregister_go2rtc = _noop_deregister
|
|
32
|
+
return c
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_orphaned_drain_is_cancelled():
|
|
36
|
+
async def _run():
|
|
37
|
+
c = _bare_client()
|
|
38
|
+
handler_removed = {"v": False}
|
|
39
|
+
|
|
40
|
+
async def _drain():
|
|
41
|
+
try:
|
|
42
|
+
await asyncio.Event().wait() # blocks forever, like outgoing_q.get
|
|
43
|
+
finally:
|
|
44
|
+
handler_removed["v"] = True # the real finally calls remove_handler
|
|
45
|
+
c._stream_mqtt_drain = asyncio.ensure_future(_drain())
|
|
46
|
+
await asyncio.sleep(0) # let the drain start blocking
|
|
47
|
+
|
|
48
|
+
await c.async_stop_streaming()
|
|
49
|
+
|
|
50
|
+
assert c._stream_mqtt_drain is None # cleared
|
|
51
|
+
assert handler_removed["v"] is True # drain's finally ran (handler removed)
|
|
52
|
+
asyncio.run(_run())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_no_drain_is_safe():
|
|
56
|
+
async def _run():
|
|
57
|
+
c = _bare_client() # _stream_mqtt_drain is None
|
|
58
|
+
await c.async_stop_streaming() # must not raise
|
|
59
|
+
assert c._streaming_active is False
|
|
60
|
+
asyncio.run(_run())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
import traceback
|
|
65
|
+
_fail = 0
|
|
66
|
+
for _k, _v in sorted(globals().items()):
|
|
67
|
+
if _k.startswith("test_"):
|
|
68
|
+
try:
|
|
69
|
+
_v()
|
|
70
|
+
print(f"PASS {_k}")
|
|
71
|
+
except Exception:
|
|
72
|
+
_fail += 1
|
|
73
|
+
print(f"FAIL {_k}")
|
|
74
|
+
traceback.print_exc()
|
|
75
|
+
raise SystemExit(1 if _fail else 0)
|
|
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
|
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/device_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/src/aidot/models/discover_model.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.7.35 → python_aidot_cameras-0.7.36}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.35 → python_aidot_cameras-0.7.36}/tests/test_sdes_fast_liveplay.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
|