python-aidot-cameras 0.7.28__tar.gz → 0.7.29__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 (61) hide show
  1. {python_aidot_cameras-0.7.28/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.29}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/pyproject.toml +1 -1
  3. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/client.py +34 -10
  4. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/protocol.py +45 -0
  5. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/LICENSE +0 -0
  7. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/MANIFEST.in +0 -0
  8. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/README.md +0 -0
  9. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/setup.cfg +0 -0
  10. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/__init__.py +0 -0
  11. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/aes_utils.py +0 -0
  12. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/__init__.py +0 -0
  13. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/constants.py +0 -0
  14. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/controls.py +0 -0
  15. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/go2rtc.py +0 -0
  16. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/lan_control.py +0 -0
  17. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/models.py +0 -0
  18. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/playback.py +0 -0
  19. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/sdes.py +0 -0
  20. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/tutk.py +0 -0
  21. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/camera/webrtc.py +0 -0
  22. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/client.py +0 -0
  23. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/const.py +0 -0
  24. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/credentials.py +0 -0
  25. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/device_client.py +0 -0
  26. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/discover.py +0 -0
  27. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/exceptions.py +0 -0
  28. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/g711.py +0 -0
  29. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/login_const.py +0 -0
  30. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/models/__init__.py +0 -0
  31. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/models/device_client_model.py +0 -0
  32. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/models/device_model.py +0 -0
  33. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/models/discover_model.py +0 -0
  34. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/aidot/py.typed +0 -0
  35. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  36. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  37. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
  38. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  39. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_alarm_event.py +0 -0
  40. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_backoff.py +0 -0
  41. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_go2rtc.py +0 -0
  42. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_highport_nomination.py +0 -0
  43. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_lan_control.py +0 -0
  44. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_live_stream_param.py +0 -0
  45. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_motion_poll.py +0 -0
  46. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_no_undefined_names.py +0 -0
  47. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_persistent_mqtt.py +0 -0
  48. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_post_merge_hardening.py +0 -0
  49. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_sdes_adaptive.py +0 -0
  50. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_sdes_fast_liveplay.py +0 -0
  51. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_sdes_idle_release.py +0 -0
  52. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_sdes_sprop.py +0 -0
  53. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_sdes_talk.py +0 -0
  54. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_sdes_watchdog.py +0 -0
  55. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_serve_relay.py +0 -0
  56. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_speak.py +0 -0
  57. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_stream_cap.py +0 -0
  58. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_stream_idle.py +0 -0
  59. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_talk.py +0 -0
  60. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/tests/test_terminal_ack.py +0 -0
  61. {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.29}/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.28
3
+ Version: 0.7.29
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.28"
7
+ version = "0.7.29"
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"
@@ -4093,16 +4093,40 @@ class CameraMixin(_CameraControlsMixin):
4093
4093
  )
4094
4094
  )
4095
4095
 
4096
- # Run MQTT in a thread executor (very long duration; stopped via
4097
- # outgoing_q sentinel when the caller calls WebRTCSession.stop()).
4098
- mqtt_fut = loop.run_in_executor(
4099
- None,
4100
- lambda: _mqtt_session_sync(
4101
- mqtt_url, mqtt_user, mqtt_pwd, mqtt_cid,
4102
- sub_topics, [], 3600.0, _on_mqtt_message,
4103
- "/mqtt", _on_mqtt_ready, outgoing_q,
4104
- ),
4105
- )
4096
+ # MQTT transport for the stream signaling. Default: a dedicated
4097
+ # connect-per-stream session in an executor (stopped via outgoing_q
4098
+ # sentinel on WebRTCSession.stop()). AIDOT_PERSISTENT_MQTT (Phase 2):
4099
+ # ride the SAME account-level persistent connection commands/attrs use
4100
+ # (the stream's mqtt_cid IS the authorized mqttClientId, so it's the same
4101
+ # connection) - subscribe + register a handler + drain outgoing_q through
4102
+ # it, and DON'T tear the connection down on stop (matching the app).
4103
+ _pm_stream = (await self._get_persistent_mqtt()
4104
+ if self._resolve_persistent_mqtt() else None)
4105
+ if _pm_stream is not None:
4106
+ await _pm_stream.subscribe(sub_topics)
4107
+ _pm_stream.add_handler(_on_mqtt_message)
4108
+
4109
+ async def _pm_stream_drain():
4110
+ try:
4111
+ while True:
4112
+ out = await loop.run_in_executor(None, outgoing_q.get)
4113
+ if out is None: # stop sentinel from WebRTCSession.stop()
4114
+ return
4115
+ await _pm_stream.publish(out[0], out[1])
4116
+ finally:
4117
+ _pm_stream.remove_handler(_on_mqtt_message)
4118
+
4119
+ mqtt_fut = asyncio.ensure_future(_pm_stream_drain())
4120
+ _on_mqtt_ready({"connected": True, "rc": 0, "rc_str": "persistent"})
4121
+ else:
4122
+ mqtt_fut = loop.run_in_executor(
4123
+ None,
4124
+ lambda: _mqtt_session_sync(
4125
+ mqtt_url, mqtt_user, mqtt_pwd, mqtt_cid,
4126
+ sub_topics, [], 3600.0, _on_mqtt_message,
4127
+ "/mqtt", _on_mqtt_ready, outgoing_q,
4128
+ ),
4129
+ )
4106
4130
 
4107
4131
  # Wait for MQTT to be connected and subscribed before proceeding.
4108
4132
  # threading.Event.wait(timeout) returns True if set, False on timeout.
@@ -1270,6 +1270,7 @@ class _PersistentMqtt:
1270
1270
  self._lock = threading.Lock()
1271
1271
  self._subs = set() # topics to (re)subscribe on connect
1272
1272
  self._collectors = [] # transient queues, each receives every msg
1273
+ self._handlers = [] # persistent on_message callbacks (e.g. a stream)
1273
1274
  self._started = False
1274
1275
  self.connects = 0 # observability: how many times we connected
1275
1276
 
@@ -1330,8 +1331,14 @@ class _PersistentMqtt:
1330
1331
  item = (msg.topic, payload)
1331
1332
  with self._lock:
1332
1333
  cols = list(self._collectors)
1334
+ handlers = list(self._handlers)
1333
1335
  for q in cols:
1334
1336
  q.put(item)
1337
+ for h in handlers: # persistent subscribers (stream, etc.)
1338
+ try:
1339
+ h(msg.topic, payload)
1340
+ except Exception:
1341
+ _LOGGER.debug("persistent mqtt: handler raised", exc_info=True)
1335
1342
 
1336
1343
  def _ensure_started_sync(self, timeout=15.0):
1337
1344
  with self._lock:
@@ -1409,6 +1416,44 @@ class _PersistentMqtt:
1409
1416
  return await loop.run_in_executor(
1410
1417
  None, functools.partial(self._ensure_started_sync, timeout))
1411
1418
 
1419
+ # --- persistent subscriber API (for the stream signaling, Phase 2) -------- #
1420
+ def add_handler(self, callback):
1421
+ """Register a persistent on_message callback ``callback(topic, payload)``
1422
+ that receives every message for the connection's lifetime (until removed).
1423
+ Use for a long-lived consumer like an open stream's signaling handler."""
1424
+ with self._lock:
1425
+ self._handlers.append(callback)
1426
+ return callback
1427
+
1428
+ def remove_handler(self, callback):
1429
+ with self._lock:
1430
+ try:
1431
+ self._handlers.remove(callback)
1432
+ except ValueError:
1433
+ pass
1434
+
1435
+ async def subscribe(self, topics):
1436
+ """Ensure the connection is up and subscribe ``topics`` (tracked for replay)."""
1437
+ import functools
1438
+ loop = asyncio.get_running_loop()
1439
+ await loop.run_in_executor(None, self._ensure_started_sync, 15.0)
1440
+ await loop.run_in_executor(None, functools.partial(self._subscribe_sync, topics))
1441
+
1442
+ async def publish(self, topic, payload):
1443
+ """Publish on the shared connection (ensures it's up first)."""
1444
+ import functools
1445
+
1446
+ def _pub():
1447
+ if not self._ensure_started_sync():
1448
+ return False
1449
+ try:
1450
+ self._client.publish(topic, payload)
1451
+ return True
1452
+ except Exception:
1453
+ return False
1454
+ loop = asyncio.get_running_loop()
1455
+ return await loop.run_in_executor(None, functools.partial(_pub))
1456
+
1412
1457
  def close(self):
1413
1458
  c = self._client
1414
1459
  self._client = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.7.28
3
+ Version: 0.7.29
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