python-aidot-cameras 0.9.0__tar.gz → 0.9.2__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.9.0/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.9.2}/PKG-INFO +5 -5
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/pyproject.toml +10 -5
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/client.py +19 -19
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/models.py +3 -3
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/playback.py +2 -2
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/protocol.py +47 -16
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/sdes.py +7 -7
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/sdes_open.py +43 -43
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/tutk.py +2 -2
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/webrtc.py +4 -4
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/webrtc_open.py +9 -9
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2/src/python_aidot_cameras.egg-info}/PKG-INFO +5 -5
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/requires.txt +4 -4
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/LICENSE +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/README.md +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/py.typed +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_token_refresh.py +0 -0
{python_aidot_cameras-0.9.0/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.9.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
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
|
|
@@ -11,11 +11,11 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: aiohttp
|
|
14
|
+
Requires-Dist: aiohttp>=3.9
|
|
15
15
|
Requires-Dist: paho-mqtt>=2.0
|
|
16
|
-
Requires-Dist: cryptography
|
|
17
|
-
Requires-Dist: pycryptodome
|
|
18
|
-
Requires-Dist: dacite
|
|
16
|
+
Requires-Dist: cryptography>=42.0
|
|
17
|
+
Requires-Dist: pycryptodome>=3.20
|
|
18
|
+
Requires-Dist: dacite>=1.8
|
|
19
19
|
Provides-Extra: webrtc
|
|
20
20
|
Requires-Dist: aiortc>=1.9.0; extra == "webrtc"
|
|
21
21
|
Requires-Dist: aioice<0.12,>=0.9.0; extra == "webrtc"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
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"
|
|
@@ -15,12 +15,17 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
17
17
|
]
|
|
18
|
+
# Conservative lower bounds only - no upper caps. When installed under Home
|
|
19
|
+
# Assistant, HA core pins aiohttp/cryptography itself; an upper cap here would
|
|
20
|
+
# fight HA's resolver on the box. Floors just exclude ancient releases while
|
|
21
|
+
# leaving the consumer (or HA) free to use a newer version. Bump a floor only
|
|
22
|
+
# after re-validating against it.
|
|
18
23
|
dependencies = [
|
|
19
|
-
"aiohttp",
|
|
24
|
+
"aiohttp>=3.9",
|
|
20
25
|
"paho-mqtt>=2.0",
|
|
21
|
-
"cryptography",
|
|
22
|
-
"pycryptodome",
|
|
23
|
-
"dacite",
|
|
26
|
+
"cryptography>=42.0",
|
|
27
|
+
"pycryptodome>=3.20",
|
|
28
|
+
"dacite>=1.8",
|
|
24
29
|
]
|
|
25
30
|
|
|
26
31
|
[project.optional-dependencies]
|
|
@@ -1467,7 +1467,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1467
1467
|
try:
|
|
1468
1468
|
self.status.update_from_camera_attributes({attr: value})
|
|
1469
1469
|
except Exception:
|
|
1470
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_set_device_attribute', exc_info=True)
|
|
1470
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_set_device_attribute', exc_info=True)
|
|
1471
1471
|
return ok
|
|
1472
1472
|
|
|
1473
1473
|
async def async_trigger_device_action(
|
|
@@ -1542,7 +1542,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1542
1542
|
try:
|
|
1543
1543
|
await self.async_wake_camera()
|
|
1544
1544
|
except Exception:
|
|
1545
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_get_camera_attributes', exc_info=True)
|
|
1545
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_get_camera_attributes', exc_info=True)
|
|
1546
1546
|
|
|
1547
1547
|
smarthome_auth = await self._async_get_smarthome_auth()
|
|
1548
1548
|
mqtt_user = (smarthome_auth or {}).get("mqttUser") or str(self.user_id)
|
|
@@ -1929,7 +1929,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1929
1929
|
try:
|
|
1930
1930
|
_os.unlink(_tmp_ts)
|
|
1931
1931
|
except Exception:
|
|
1932
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_snapshot', exc_info=True)
|
|
1932
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_snapshot', exc_info=True)
|
|
1933
1933
|
|
|
1934
1934
|
# ── DTLS path: on_frame callback delivers frames from aiortc ─────── #
|
|
1935
1935
|
frame_event = _asyncio.Event()
|
|
@@ -2002,21 +2002,21 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2002
2002
|
try:
|
|
2003
2003
|
await g_task
|
|
2004
2004
|
except (asyncio.CancelledError, Exception):
|
|
2005
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_stop_streaming', exc_info=True)
|
|
2005
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2006
2006
|
await self._deregister_go2rtc()
|
|
2007
2007
|
session, self._stream_session = self._stream_session, None
|
|
2008
2008
|
if session is not None:
|
|
2009
2009
|
try:
|
|
2010
2010
|
await session.stop()
|
|
2011
2011
|
except Exception:
|
|
2012
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_stop_streaming', exc_info=True)
|
|
2012
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2013
2013
|
task, self._stream_task = self._stream_task, None
|
|
2014
2014
|
if task is not None and not task.done():
|
|
2015
2015
|
task.cancel()
|
|
2016
2016
|
try:
|
|
2017
2017
|
await task
|
|
2018
2018
|
except (asyncio.CancelledError, Exception):
|
|
2019
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_stop_streaming', exc_info=True)
|
|
2019
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2020
2020
|
# Reap a persistent-MQTT stream drain that no session stopped (e.g. an
|
|
2021
2021
|
# open cancelled mid-handshake) so its handler is removed from the shared
|
|
2022
2022
|
# connection and its blocked executor thread is released.
|
|
@@ -2041,13 +2041,13 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2041
2041
|
try:
|
|
2042
2042
|
outq.put_nowait(None) # release the executor thread in outgoing_q.get
|
|
2043
2043
|
except Exception:
|
|
2044
|
-
_LOGGER.debug("camera %s: swallowed exception", '_reap_stream_drain', exc_info=True)
|
|
2044
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_reap_stream_drain', exc_info=True)
|
|
2045
2045
|
if not drain.done():
|
|
2046
2046
|
drain.cancel()
|
|
2047
2047
|
try:
|
|
2048
2048
|
await drain
|
|
2049
2049
|
except (asyncio.CancelledError, Exception):
|
|
2050
|
-
_LOGGER.debug("camera %s: swallowed exception", '_reap_stream_drain', exc_info=True)
|
|
2050
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_reap_stream_drain', exc_info=True)
|
|
2051
2051
|
|
|
2052
2052
|
async def async_start_motion_polling(
|
|
2053
2053
|
self, callback: Callable, interval: float = 30.0, lookback_s: int = 600,
|
|
@@ -2087,7 +2087,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2087
2087
|
try:
|
|
2088
2088
|
await task
|
|
2089
2089
|
except (asyncio.CancelledError, Exception):
|
|
2090
|
-
_LOGGER.debug("camera %s: swallowed exception", 'async_stop_motion_polling', exc_info=True)
|
|
2090
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_motion_polling', exc_info=True)
|
|
2091
2091
|
|
|
2092
2092
|
async def _motion_poll_loop(self, lookback_s: int) -> None:
|
|
2093
2093
|
"""Background: poll the cloud event list; fire callback on newly-recorded events."""
|
|
@@ -2241,7 +2241,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2241
2241
|
try:
|
|
2242
2242
|
await self.async_wait_serve_ready(timeout=40.0)
|
|
2243
2243
|
except Exception:
|
|
2244
|
-
_LOGGER.debug("camera %s: swallowed exception", '_register_with_go2rtc', exc_info=True)
|
|
2244
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_register_with_go2rtc', exc_info=True)
|
|
2245
2245
|
if not (self._streaming_active and self._go2rtc_url and self._keepalive_rtsp_url):
|
|
2246
2246
|
return
|
|
2247
2247
|
name = f"aidot_{self.device_id[:12]}"
|
|
@@ -2254,7 +2254,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2254
2254
|
_LOGGER.info(
|
|
2255
2255
|
"camera %s: preferring go2rtc stream -> %s", self.device_id, url)
|
|
2256
2256
|
except Exception:
|
|
2257
|
-
_LOGGER.debug("camera %s: swallowed exception", '_register_with_go2rtc', exc_info=True)
|
|
2257
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_register_with_go2rtc', exc_info=True)
|
|
2258
2258
|
|
|
2259
2259
|
async def _deregister_go2rtc(self) -> None:
|
|
2260
2260
|
"""Remove this camera's stream from go2rtc (best-effort)."""
|
|
@@ -2269,7 +2269,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2269
2269
|
async with aiohttp.ClientSession() as _s2:
|
|
2270
2270
|
await Go2rtcClient(_s2, base).remove_stream(name)
|
|
2271
2271
|
except Exception:
|
|
2272
|
-
_LOGGER.debug("camera %s: swallowed exception", '_deregister_go2rtc', exc_info=True)
|
|
2272
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_deregister_go2rtc', exc_info=True)
|
|
2273
2273
|
|
|
2274
2274
|
async def async_wait_serve_ready(self, timeout: float = 20.0) -> bool:
|
|
2275
2275
|
"""Wait until the DTLS serve is bound + serving (or ``timeout``).
|
|
@@ -2520,7 +2520,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2520
2520
|
try:
|
|
2521
2521
|
await session.stop()
|
|
2522
2522
|
except Exception:
|
|
2523
|
-
_LOGGER.debug("camera %s: swallowed exception", '_sdes_keepalive_loop', exc_info=True)
|
|
2523
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_sdes_keepalive_loop', exc_info=True)
|
|
2524
2524
|
self._streaming_active = False
|
|
2525
2525
|
self._keepalive_rtsp_url = None
|
|
2526
2526
|
self._serve_ready.clear()
|
|
@@ -2540,7 +2540,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2540
2540
|
try:
|
|
2541
2541
|
await session.stop()
|
|
2542
2542
|
except Exception:
|
|
2543
|
-
_LOGGER.debug("camera %s: swallowed exception", '_sdes_keepalive_loop', exc_info=True)
|
|
2543
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_sdes_keepalive_loop', exc_info=True)
|
|
2544
2544
|
|
|
2545
2545
|
# Adaptive bookkeeping: a fast attempt that never delivered media
|
|
2546
2546
|
# latches the loop onto the full relay path for its remaining opens.
|
|
@@ -2651,7 +2651,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2651
2651
|
try:
|
|
2652
2652
|
await session.stop()
|
|
2653
2653
|
except Exception:
|
|
2654
|
-
_LOGGER.debug("camera %s: swallowed exception", '_on_frame', exc_info=True)
|
|
2654
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_on_frame', exc_info=True)
|
|
2655
2655
|
|
|
2656
2656
|
if self._streaming_active:
|
|
2657
2657
|
# Reset backoff if this session produced frames (a normal drop
|
|
@@ -2707,7 +2707,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2707
2707
|
except Exception:
|
|
2708
2708
|
pass # full -> drop (PLI re-arms a GOP)
|
|
2709
2709
|
except Exception:
|
|
2710
|
-
_LOGGER.debug("
|
|
2710
|
+
_LOGGER.debug("swallowed exception in %s", '_tap_put', exc_info=True)
|
|
2711
2711
|
return _orig_put(task, *a, **k)
|
|
2712
2712
|
|
|
2713
2713
|
_qd.put = _tap_put
|
|
@@ -3229,7 +3229,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3229
3229
|
try:
|
|
3230
3230
|
wfile.close()
|
|
3231
3231
|
except Exception:
|
|
3232
|
-
_LOGGER.debug("camera %s: swallowed exception", '_pc_dead', exc_info=True)
|
|
3232
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_pc_dead', exc_info=True)
|
|
3233
3233
|
wfile = None
|
|
3234
3234
|
mux_thread.join(timeout=2.0)
|
|
3235
3235
|
mux_thread = stop_flag = None
|
|
@@ -3248,7 +3248,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3248
3248
|
try:
|
|
3249
3249
|
wfile.close()
|
|
3250
3250
|
except Exception:
|
|
3251
|
-
_LOGGER.debug("camera %s: swallowed exception", '_pc_dead', exc_info=True)
|
|
3251
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_pc_dead', exc_info=True)
|
|
3252
3252
|
if mux_thread is not None:
|
|
3253
3253
|
mux_thread.join(timeout=2.0)
|
|
3254
3254
|
_terminate_proc(proc) # never orphan ffmpeg on teardown
|
|
@@ -3256,7 +3256,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3256
3256
|
try:
|
|
3257
3257
|
await session.stop()
|
|
3258
3258
|
except Exception:
|
|
3259
|
-
_LOGGER.debug("camera %s: swallowed exception", '_pc_dead', exc_info=True)
|
|
3259
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_pc_dead', exc_info=True)
|
|
3260
3260
|
|
|
3261
3261
|
if cancelled:
|
|
3262
3262
|
return
|
|
@@ -102,7 +102,7 @@ class CameraStatusData(DeviceStatusData):
|
|
|
102
102
|
try:
|
|
103
103
|
self.battery_remaining = int(v)
|
|
104
104
|
except (ValueError, TypeError):
|
|
105
|
-
_LOGGER.debug("
|
|
105
|
+
_LOGGER.debug("swallowed exception in %s", 'update', exc_info=True)
|
|
106
106
|
if (v := attr.get("Occupancy")) is not None:
|
|
107
107
|
self.occupancy = bool(int(v))
|
|
108
108
|
if (v := attr.get("SDcardStatus")) is not None:
|
|
@@ -111,7 +111,7 @@ class CameraStatusData(DeviceStatusData):
|
|
|
111
111
|
try:
|
|
112
112
|
self.wifi_rssi = int(v)
|
|
113
113
|
except (ValueError, TypeError):
|
|
114
|
-
_LOGGER.debug("
|
|
114
|
+
_LOGGER.debug("swallowed exception in %s", 'update', exc_info=True)
|
|
115
115
|
|
|
116
116
|
# Cloud "properties" keys that belong to lights, not cameras. A camera's
|
|
117
117
|
# image "Dimming" must not be read as a light brightness (and could TypeError
|
|
@@ -163,7 +163,7 @@ class CameraDeviceInformation(DeviceInformation):
|
|
|
163
163
|
if isinstance(codes, list):
|
|
164
164
|
self.ptz_directions = [int(c) for c in codes]
|
|
165
165
|
except Exception:
|
|
166
|
-
_LOGGER.debug("
|
|
166
|
+
_LOGGER.debug("swallowed exception in %s", '__init__', exc_info=True)
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
@dataclass
|
|
@@ -291,7 +291,7 @@ class CloudPlaybackSession:
|
|
|
291
291
|
self._writer.close()
|
|
292
292
|
await self._writer.wait_closed()
|
|
293
293
|
except Exception:
|
|
294
|
-
_LOGGER.debug("
|
|
294
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
295
295
|
self._writer = None
|
|
296
296
|
self._reader = None
|
|
297
297
|
|
|
@@ -480,6 +480,6 @@ class LiveStreamSession:
|
|
|
480
480
|
self._writer.close()
|
|
481
481
|
await self._writer.wait_closed()
|
|
482
482
|
except Exception:
|
|
483
|
-
_LOGGER.debug("
|
|
483
|
+
_LOGGER.debug("swallowed exception in %s", '_cleanup', exc_info=True)
|
|
484
484
|
self._writer = None
|
|
485
485
|
self._reader = None
|
|
@@ -302,13 +302,13 @@ def _compress_sdp_for_camera(sdp: str) -> str:
|
|
|
302
302
|
try:
|
|
303
303
|
seen["H264/90000_pt"] = ln.split(":")[1].split(" ")[0]
|
|
304
304
|
except Exception:
|
|
305
|
-
_LOGGER.debug("
|
|
305
|
+
_LOGGER.debug("swallowed exception in %s", 'keep', exc_info=True)
|
|
306
306
|
elif "H265/90000" in ln and seen.get("H265/90000") is None:
|
|
307
307
|
keep(ln, "H265/90000")
|
|
308
308
|
try:
|
|
309
309
|
seen["H265/90000_pt"] = ln.split(":")[1].split(" ")[0]
|
|
310
310
|
except Exception:
|
|
311
|
-
_LOGGER.debug("
|
|
311
|
+
_LOGGER.debug("swallowed exception in %s", 'keep', exc_info=True)
|
|
312
312
|
elif "apt=" in ln:
|
|
313
313
|
try:
|
|
314
314
|
apt = ln.split("apt=")[1].strip()
|
|
@@ -499,7 +499,7 @@ def _terminate_proc(proc) -> None:
|
|
|
499
499
|
if proc.returncode is None:
|
|
500
500
|
proc.terminate()
|
|
501
501
|
except Exception:
|
|
502
|
-
_LOGGER.debug("
|
|
502
|
+
_LOGGER.debug("swallowed exception in %s", '_terminate_proc', exc_info=True)
|
|
503
503
|
|
|
504
504
|
|
|
505
505
|
_XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
|
|
@@ -897,6 +897,8 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
897
897
|
have_audio = False
|
|
898
898
|
v0 = [None]
|
|
899
899
|
a_pts = [0]
|
|
900
|
+
a_rtp0 = [None] # first audio RTP timestamp (8 kHz units), for gap detection
|
|
901
|
+
a_in = [0] # 8 kHz samples emitted to the resampler so far (incl. concealed)
|
|
900
902
|
vstarted = [False]
|
|
901
903
|
|
|
902
904
|
def _flush_video():
|
|
@@ -918,7 +920,7 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
918
920
|
out.mux(pkt)
|
|
919
921
|
progress[0] = _t.monotonic()
|
|
920
922
|
except Exception:
|
|
921
|
-
_LOGGER.debug("
|
|
923
|
+
_LOGGER.debug("swallowed exception in %s", '_flush_video', exc_info=True)
|
|
922
924
|
|
|
923
925
|
def _flush_audio(drain=False):
|
|
924
926
|
if not have_audio:
|
|
@@ -934,7 +936,35 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
934
936
|
data, _ts = aq.get_nowait()
|
|
935
937
|
except _q.Empty:
|
|
936
938
|
break
|
|
939
|
+
# Lock audio to its RTP clock. The camera's PCMA RTP timestamp is
|
|
940
|
+
# an 8 kHz sample count; on packet loss it jumps ahead of the
|
|
941
|
+
# samples we've actually decoded. Without filling that gap the
|
|
942
|
+
# audio timeline silently COMPRESSES (the lost time vanishes), so
|
|
943
|
+
# audio runs ahead of the RTP-timestamped video -> growing A/V
|
|
944
|
+
# desync and player jitter-buffer resyncs, heard as choppiness.
|
|
945
|
+
# Conceal the gap with silence (PLC) through the same stateful
|
|
946
|
+
# resampler, mirroring what the video path does with its 90 kHz
|
|
947
|
+
# timestamps. Lossless streams have _gap == 0 (no-op).
|
|
948
|
+
if _np is not None:
|
|
949
|
+
if a_rtp0[0] is None:
|
|
950
|
+
a_rtp0[0] = int(_ts)
|
|
951
|
+
_expected = (int(_ts) - a_rtp0[0]) & 0xFFFFFFFF
|
|
952
|
+
_gap = _expected - a_in[0]
|
|
953
|
+
if _gap < 0 or _gap > 8000 * 5:
|
|
954
|
+
a_in[0] = _expected # wrap / stream reset -> resync, no fill
|
|
955
|
+
elif _gap >= 160: # >= 20 ms missing: conceal with silence
|
|
956
|
+
try:
|
|
957
|
+
_sil = _np.zeros((1, int(_gap)), dtype=_np.int16)
|
|
958
|
+
_sfr = av.AudioFrame.from_ndarray(
|
|
959
|
+
_sil, format="s16", layout="mono")
|
|
960
|
+
_sfr.sample_rate = 8000
|
|
961
|
+
for _rfr in resampler.resample(_sfr):
|
|
962
|
+
fifo.write(_rfr)
|
|
963
|
+
a_in[0] += int(_gap)
|
|
964
|
+
except Exception:
|
|
965
|
+
_LOGGER.debug("swallowed exception in %s", '_flush_audio', exc_info=True)
|
|
937
966
|
for fr in adec.decode(av.Packet(data)):
|
|
967
|
+
_ndec = fr.samples
|
|
938
968
|
fr.pts = None
|
|
939
969
|
if _np is not None:
|
|
940
970
|
try:
|
|
@@ -961,11 +991,12 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
961
991
|
_g.sample_rate = 8000
|
|
962
992
|
fr = _g
|
|
963
993
|
except Exception:
|
|
964
|
-
_LOGGER.debug("
|
|
994
|
+
_LOGGER.debug("swallowed exception in %s", '_flush_audio', exc_info=True)
|
|
965
995
|
for rfr in resampler.resample(fr): # 8k PCMA -> 48k fltp
|
|
966
996
|
fifo.write(rfr)
|
|
997
|
+
a_in[0] += _ndec
|
|
967
998
|
except Exception:
|
|
968
|
-
_LOGGER.debug("
|
|
999
|
+
_LOGGER.debug("swallowed exception in %s", '_flush_audio', exc_info=True)
|
|
969
1000
|
while True:
|
|
970
1001
|
fr = fifo.read(1024) # AAC wants 1024-sample frames
|
|
971
1002
|
if fr is None:
|
|
@@ -1009,7 +1040,7 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1009
1040
|
out.mux(opkt)
|
|
1010
1041
|
progress[0] = _t.monotonic()
|
|
1011
1042
|
except Exception:
|
|
1012
|
-
_LOGGER.debug("
|
|
1043
|
+
_LOGGER.debug("swallowed exception in %s", '_flush_audio', exc_info=True)
|
|
1013
1044
|
|
|
1014
1045
|
while not stop_flag.is_set():
|
|
1015
1046
|
_flush_video()
|
|
@@ -1022,11 +1053,11 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1022
1053
|
for opkt in aenc.encode(None): # flush
|
|
1023
1054
|
out.mux(opkt)
|
|
1024
1055
|
except Exception:
|
|
1025
|
-
_LOGGER.debug("
|
|
1056
|
+
_LOGGER.debug("swallowed exception in %s", '_dtls_av_mux_run', exc_info=True)
|
|
1026
1057
|
try:
|
|
1027
1058
|
out.close()
|
|
1028
1059
|
except Exception:
|
|
1029
|
-
_LOGGER.debug("
|
|
1060
|
+
_LOGGER.debug("swallowed exception in %s", '_dtls_av_mux_run', exc_info=True)
|
|
1030
1061
|
|
|
1031
1062
|
|
|
1032
1063
|
def _h264_has_keyframe(data: bytes) -> bool:
|
|
@@ -1170,7 +1201,7 @@ def _mqtt_session_sync(
|
|
|
1170
1201
|
try:
|
|
1171
1202
|
client.disconnect()
|
|
1172
1203
|
except Exception:
|
|
1173
|
-
_LOGGER.debug("
|
|
1204
|
+
_LOGGER.debug("swallowed exception in %s", '_on_log', exc_info=True)
|
|
1174
1205
|
return [], status
|
|
1175
1206
|
|
|
1176
1207
|
if not status["connected"]:
|
|
@@ -1182,7 +1213,7 @@ def _mqtt_session_sync(
|
|
|
1182
1213
|
try:
|
|
1183
1214
|
client.disconnect()
|
|
1184
1215
|
except Exception:
|
|
1185
|
-
_LOGGER.debug("
|
|
1216
|
+
_LOGGER.debug("swallowed exception in %s", '_on_log', exc_info=True)
|
|
1186
1217
|
return [], status
|
|
1187
1218
|
|
|
1188
1219
|
_LOGGER.info("_mqtt_session: connected to %s:%d clientId=%s", hostname, port, client_id)
|
|
@@ -1199,7 +1230,7 @@ def _mqtt_session_sync(
|
|
|
1199
1230
|
try:
|
|
1200
1231
|
on_ready(status)
|
|
1201
1232
|
except Exception:
|
|
1202
|
-
_LOGGER.debug("
|
|
1233
|
+
_LOGGER.debug("swallowed exception in %s", '_on_log', exc_info=True)
|
|
1203
1234
|
|
|
1204
1235
|
collected = []
|
|
1205
1236
|
deadline = _time.monotonic() + duration
|
|
@@ -1222,7 +1253,7 @@ def _mqtt_session_sync(
|
|
|
1222
1253
|
try:
|
|
1223
1254
|
client.disconnect()
|
|
1224
1255
|
except Exception:
|
|
1225
|
-
_LOGGER.debug("
|
|
1256
|
+
_LOGGER.debug("swallowed exception in %s", '_on_log', exc_info=True)
|
|
1226
1257
|
return collected, status
|
|
1227
1258
|
pub_topic, pub_payload = out
|
|
1228
1259
|
client.publish(pub_topic, pub_payload)
|
|
@@ -1235,13 +1266,13 @@ def _mqtt_session_sync(
|
|
|
1235
1266
|
try:
|
|
1236
1267
|
on_message(*item)
|
|
1237
1268
|
except Exception:
|
|
1238
|
-
_LOGGER.debug("
|
|
1269
|
+
_LOGGER.debug("swallowed exception in %s", '_on_log', exc_info=True)
|
|
1239
1270
|
|
|
1240
1271
|
client.loop_stop()
|
|
1241
1272
|
try:
|
|
1242
1273
|
client.disconnect()
|
|
1243
1274
|
except Exception:
|
|
1244
|
-
_LOGGER.debug("
|
|
1275
|
+
_LOGGER.debug("swallowed exception in %s", '_mqtt_session_sync', exc_info=True)
|
|
1245
1276
|
return collected, status
|
|
1246
1277
|
|
|
1247
1278
|
|
|
@@ -1566,7 +1597,7 @@ async def _mqtt_get_playback_server_info(
|
|
|
1566
1597
|
pl["serverIP"] = pl.get("serverIP") or pl.get("serverIp")
|
|
1567
1598
|
result_holder.append(pl)
|
|
1568
1599
|
except Exception:
|
|
1569
|
-
_LOGGER.debug("
|
|
1600
|
+
_LOGGER.debug("swallowed exception in %s", '_check', exc_info=True)
|
|
1570
1601
|
|
|
1571
1602
|
await _mqtt_session(
|
|
1572
1603
|
mqtt_url, mqtt_user, mqtt_pwd, client_id,
|
|
@@ -192,7 +192,7 @@ class SdesSession:
|
|
|
192
192
|
try:
|
|
193
193
|
await _stop_loop.run_in_executor(None, lambda: self._proc.wait(5))
|
|
194
194
|
except Exception:
|
|
195
|
-
_LOGGER.debug("
|
|
195
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
196
196
|
# Read drained stderr in the executor with a hard timeout: proc.stderr.read()
|
|
197
197
|
# blocks until EOF, which never arrives if the killed process is still a
|
|
198
198
|
# zombie / stuck in uninterruptible I/O - doing it inline would hang the
|
|
@@ -205,32 +205,32 @@ class SdesSession:
|
|
|
205
205
|
timeout=2.0,
|
|
206
206
|
)
|
|
207
207
|
except Exception: # incl. asyncio.TimeoutError - never let teardown hang here
|
|
208
|
-
_LOGGER.debug("
|
|
208
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
209
209
|
# On timeout the executor thread is still blocked in stderr.read() on
|
|
210
210
|
# a wedged ffmpeg; close the pipe so that read returns instead of
|
|
211
211
|
# pinning a default-pool thread for the life of the process.
|
|
212
212
|
try:
|
|
213
213
|
self._proc.stderr.close()
|
|
214
214
|
except Exception:
|
|
215
|
-
_LOGGER.debug("
|
|
215
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
216
216
|
if stderr_bytes:
|
|
217
217
|
_LOGGER.warning("ffmpeg SDES stderr:\n%s", stderr_bytes.decode(errors="replace"))
|
|
218
218
|
import os
|
|
219
219
|
try:
|
|
220
220
|
os.unlink(self._sdp_path)
|
|
221
221
|
except Exception:
|
|
222
|
-
_LOGGER.debug("
|
|
222
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
223
223
|
for _sock in (self._audio_sock, self._video_sock):
|
|
224
224
|
if _sock is not None:
|
|
225
225
|
try:
|
|
226
226
|
_sock.close()
|
|
227
227
|
except Exception:
|
|
228
|
-
_LOGGER.debug("
|
|
228
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
229
229
|
self._outgoing_q.put_nowait(None)
|
|
230
230
|
try:
|
|
231
231
|
await asyncio.wait_for(self._mqtt_fut, timeout=5.0)
|
|
232
232
|
except Exception:
|
|
233
|
-
_LOGGER.debug("
|
|
233
|
+
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
def _run_sdes_talk_pump(state: dict) -> None:
|
|
@@ -297,7 +297,7 @@ def _run_sdes_talk_pump(state: dict) -> None:
|
|
|
297
297
|
try:
|
|
298
298
|
_sock.sendto(_tx.protect(_hdr + _alaw), _src)
|
|
299
299
|
except Exception:
|
|
300
|
-
_LOGGER.debug("
|
|
300
|
+
_LOGGER.debug("swallowed exception in %s", '_run_sdes_talk_pump', exc_info=True)
|
|
301
301
|
_seq = (_seq + 1) & 0xFFFF
|
|
302
302
|
_ts = (_ts + len(_alaw)) & 0xFFFFFFFF
|
|
303
303
|
# Active talk: hold 20 ms pacing for the audio cadence.
|