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.
Files changed (71) hide show
  1. {python_aidot_cameras-0.9.0/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.9.2}/PKG-INFO +5 -5
  2. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/pyproject.toml +10 -5
  3. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/client.py +19 -19
  4. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/models.py +3 -3
  5. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/playback.py +2 -2
  6. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/protocol.py +47 -16
  7. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/sdes.py +7 -7
  8. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/sdes_open.py +43 -43
  9. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/tutk.py +2 -2
  10. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/webrtc.py +4 -4
  11. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/webrtc_open.py +9 -9
  12. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2/src/python_aidot_cameras.egg-info}/PKG-INFO +5 -5
  13. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/requires.txt +4 -4
  14. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/LICENSE +0 -0
  15. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/MANIFEST.in +0 -0
  16. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/README.md +0 -0
  17. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/setup.cfg +0 -0
  18. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/__init__.py +0 -0
  19. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/aes_utils.py +0 -0
  20. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/__init__.py +0 -0
  21. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/constants.py +0 -0
  22. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/controls.py +0 -0
  23. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/go2rtc.py +0 -0
  24. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/camera/lan_control.py +0 -0
  25. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/client.py +0 -0
  26. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/const.py +0 -0
  27. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/credentials.py +0 -0
  28. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/device_client.py +0 -0
  29. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/discover.py +0 -0
  30. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/exceptions.py +0 -0
  31. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/g711.py +0 -0
  32. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/login_const.py +0 -0
  33. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/__init__.py +0 -0
  34. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/device_client_model.py +0 -0
  35. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/device_model.py +0 -0
  36. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/models/discover_model.py +0 -0
  37. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/aidot/py.typed +0 -0
  38. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  39. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  40. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  41. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_aioice_compat.py +0 -0
  42. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_alarm_event.py +0 -0
  43. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_backoff.py +0 -0
  44. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_device_login_guard.py +0 -0
  45. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_go2rtc.py +0 -0
  46. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_highport_nomination.py +0 -0
  47. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_ice_config_cache.py +0 -0
  48. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_lan_control.py +0 -0
  49. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_live_stream_param.py +0 -0
  50. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_motion_poll.py +0 -0
  51. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_no_undefined_names.py +0 -0
  52. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_persistent_mqtt.py +0 -0
  53. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_persistent_mqtt_robustness.py +0 -0
  54. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_post_merge_hardening.py +0 -0
  55. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_adaptive.py +0 -0
  56. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_fast_liveplay.py +0 -0
  57. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_idle_release.py +0 -0
  58. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_serve_audio.py +0 -0
  59. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_serve_cmd.py +0 -0
  60. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_sprop.py +0 -0
  61. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_talk.py +0 -0
  62. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_sdes_watchdog.py +0 -0
  63. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_serve_relay.py +0 -0
  64. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_session_stats.py +0 -0
  65. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_speak.py +0 -0
  66. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_stream_cap.py +0 -0
  67. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_stream_idle.py +0 -0
  68. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_stream_teardown.py +0 -0
  69. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_talk.py +0 -0
  70. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/tests/test_terminal_ack.py +0 -0
  71. {python_aidot_cameras-0.9.0 → python_aidot_cameras-0.9.2}/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.9.0
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.0"
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("camera %s: swallowed exception", '_tap_put', exc_info=True)
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("camera %s: swallowed exception", 'update', exc_info=True)
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("camera %s: swallowed exception", 'update', exc_info=True)
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("camera %s: swallowed exception", '__init__', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", '_cleanup', exc_info=True)
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("camera %s: swallowed exception", 'keep', exc_info=True)
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("camera %s: swallowed exception", 'keep', exc_info=True)
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("camera %s: swallowed exception", '_terminate_proc', exc_info=True)
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("camera %s: swallowed exception", '_flush_video', exc_info=True)
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("camera %s: swallowed exception", '_flush_audio', exc_info=True)
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("camera %s: swallowed exception", '_flush_audio', exc_info=True)
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("camera %s: swallowed exception", '_flush_audio', exc_info=True)
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("camera %s: swallowed exception", '_dtls_av_mux_run', exc_info=True)
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("camera %s: swallowed exception", '_dtls_av_mux_run', exc_info=True)
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("camera %s: swallowed exception", '_on_log', exc_info=True)
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("camera %s: swallowed exception", '_on_log', exc_info=True)
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("camera %s: swallowed exception", '_on_log', exc_info=True)
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("camera %s: swallowed exception", '_on_log', exc_info=True)
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("camera %s: swallowed exception", '_on_log', exc_info=True)
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("camera %s: swallowed exception", '_mqtt_session_sync', exc_info=True)
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("camera %s: swallowed exception", '_check', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", 'stop', exc_info=True)
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("camera %s: swallowed exception", '_run_sdes_talk_pump', exc_info=True)
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.