python-aidot-cameras 0.7.23__tar.gz → 0.7.25__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 (59) hide show
  1. {python_aidot_cameras-0.7.23/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.25}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/pyproject.toml +1 -1
  3. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/client.py +55 -9
  4. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  5. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_sdes_fast_liveplay.py +16 -0
  6. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/LICENSE +0 -0
  7. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/MANIFEST.in +0 -0
  8. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/README.md +0 -0
  9. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/setup.cfg +0 -0
  10. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/__init__.py +0 -0
  11. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/aes_utils.py +0 -0
  12. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/__init__.py +0 -0
  13. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/constants.py +0 -0
  14. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/controls.py +0 -0
  15. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/go2rtc.py +0 -0
  16. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/lan_control.py +0 -0
  17. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/models.py +0 -0
  18. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/playback.py +0 -0
  19. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/protocol.py +0 -0
  20. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/sdes.py +0 -0
  21. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/tutk.py +0 -0
  22. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/camera/webrtc.py +0 -0
  23. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/client.py +0 -0
  24. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/const.py +0 -0
  25. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/credentials.py +0 -0
  26. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/device_client.py +0 -0
  27. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/discover.py +0 -0
  28. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/exceptions.py +0 -0
  29. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/g711.py +0 -0
  30. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/login_const.py +0 -0
  31. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/models/__init__.py +0 -0
  32. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/models/device_client_model.py +0 -0
  33. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/models/device_model.py +0 -0
  34. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/models/discover_model.py +0 -0
  35. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/aidot/py.typed +0 -0
  36. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  37. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  38. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
  39. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  40. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_alarm_event.py +0 -0
  41. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_backoff.py +0 -0
  42. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_go2rtc.py +0 -0
  43. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_highport_nomination.py +0 -0
  44. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_lan_control.py +0 -0
  45. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_live_stream_param.py +0 -0
  46. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_motion_poll.py +0 -0
  47. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_no_undefined_names.py +0 -0
  48. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_post_merge_hardening.py +0 -0
  49. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_sdes_idle_release.py +0 -0
  50. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_sdes_sprop.py +0 -0
  51. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_sdes_talk.py +0 -0
  52. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_sdes_watchdog.py +0 -0
  53. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_serve_relay.py +0 -0
  54. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_speak.py +0 -0
  55. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_stream_cap.py +0 -0
  56. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_stream_idle.py +0 -0
  57. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_talk.py +0 -0
  58. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/tests/test_terminal_ack.py +0 -0
  59. {python_aidot_cameras-0.7.23 → python_aidot_cameras-0.7.25}/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.23
3
+ Version: 0.7.25
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.23"
7
+ version = "0.7.25"
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"
@@ -543,6 +543,14 @@ class CameraMixin(_CameraControlsMixin):
543
543
  # data-channel-less; it's parked on the SDES/TUTK side anyway.
544
544
  _NO_DATACHANNEL_MODELS: frozenset = frozenset({"LK.IPC.A001064"})
545
545
 
546
+ # A001064's role-reversal handshake (the camera echoes our offer back as its
547
+ # own webrtcReq before doing ICE, and won't stream until it gets our
548
+ # webrtcResp) needs the camera armed BEFORE our webrtcReq. sdes_fast_liveplay
549
+ # sends webrtcReq ~4.5 s earlier, which degrades A001064 media reliability
550
+ # (2/2 -> 1/2 media in a live A/B; the flag was soak-validated only on the
551
+ # A001513 battery cameras). Exclude role-reversal models from the flag.
552
+ _NO_FAST_LIVEPLAY_MODELS: frozenset = frozenset({"LK.IPC.A001064"})
553
+
546
554
  @property
547
555
  def _offer_should_include_datachannel(self) -> bool:
548
556
  model = getattr(getattr(self, "info", None), "model_id", None)
@@ -2537,7 +2545,14 @@ class CameraMixin(_CameraControlsMixin):
2537
2545
 
2538
2546
  Per-camera ``sdes_fast_liveplay`` (set via start_keepalive) wins; else the
2539
2547
  ``AIDOT_SDES_FAST_LIVEPLAY`` env (truthy = 1/true/yes/on), default off.
2540
- Only consulted for SDES cameras (the caller gates on is_sdes_camera)."""
2548
+ Only consulted for SDES cameras (the caller gates on is_sdes_camera).
2549
+
2550
+ Role-reversal models (``_NO_FAST_LIVEPLAY_MODELS``, e.g. A001064) are always
2551
+ excluded - their handshake needs the camera armed before our webrtcReq, so
2552
+ the flag's early webrtcReq degrades their media reliability."""
2553
+ model = getattr(getattr(self, "info", None), "model_id", None)
2554
+ if model in self._NO_FAST_LIVEPLAY_MODELS:
2555
+ return False
2541
2556
  opt = getattr(self, "_sdes_fast_liveplay_opt", None)
2542
2557
  if opt is not None:
2543
2558
  return bool(opt)
@@ -3705,7 +3720,14 @@ class CameraMixin(_CameraControlsMixin):
3705
3720
  or msg.get("devId") == device_id):
3706
3721
  loop.call_soon_threadsafe(camera_ready_ev.set)
3707
3722
  # livePlayResp: explicit camera ack/nack for start-play command.
3708
- if method == "livePlayResp" and inner.get("devId") == device_id:
3723
+ # The camera echoes our peer_id (verified live); its payload has NO
3724
+ # devId, so the old devId-only match never fired and this future never
3725
+ # resolved (the wait always timed out). Match on the echoed peer_id
3726
+ # (per-open, precise); keep devId as a fallback for any camera that
3727
+ # does send it. dstAddr is the account-wide userId - too loose to use.
3728
+ if method == "livePlayResp" and (
3729
+ inner.get("peerid") == peer_id
3730
+ or inner.get("devId") == device_id):
3709
3731
  if not liveplay_resp_fut.done():
3710
3732
  loop.call_soon_threadsafe(liveplay_resp_fut.set_result, inner)
3711
3733
  # livePlayReq echo: broker/camera confirmed delivery of our livePlayReq.
@@ -4171,9 +4193,19 @@ class CameraMixin(_CameraControlsMixin):
4171
4193
  _lp_arrived = True
4172
4194
  _lp_code = int(_lp_resp.get("code", 200))
4173
4195
  _lp_on = int(_lp_resp.get("livePlay", 1))
4174
- if _lp_code not in (0, 200) or _lp_on == 0:
4196
+ # Fast-fail only on an unambiguous refusal (livePlay=0).
4197
+ # Other non-OK codes (incl. -50019 "not ready") are
4198
+ # transient on battery cameras and recover via ICE; abort
4199
+ # there would spuriously kill otherwise-good streams.
4200
+ if _lp_on == 0:
4175
4201
  raise RuntimeError(
4176
- f"livePlay rejected by camera (code={_lp_code}, livePlay={_lp_on})"
4202
+ f"livePlay refused by camera (livePlay=0, code={_lp_code})"
4203
+ )
4204
+ elif _lp_code not in (0, 200):
4205
+ _status(
4206
+ f"livePlayResp: non-OK code {_lp_code}"
4207
+ f"{' (not ready, transient)' if _lp_code == -50019 else ''}"
4208
+ " - proceeding"
4177
4209
  )
4178
4210
  except TimeoutError:
4179
4211
  pass
@@ -4216,9 +4248,15 @@ class CameraMixin(_CameraControlsMixin):
4216
4248
  _lp_resp2 = liveplay_resp_fut.result()
4217
4249
  _lp_code2 = int(_lp_resp2.get("code", 200))
4218
4250
  _lp_on2 = int(_lp_resp2.get("livePlay", 1))
4219
- if _lp_code2 not in (0, 200) or _lp_on2 == 0:
4251
+ if _lp_on2 == 0:
4220
4252
  raise RuntimeError(
4221
- f"livePlay rejected by camera (code={_lp_code2}, livePlay={_lp_on2})"
4253
+ f"livePlay refused by camera (livePlay=0, code={_lp_code2})"
4254
+ )
4255
+ elif _lp_code2 not in (0, 200):
4256
+ _status(
4257
+ f"livePlayResp: non-OK code {_lp_code2}"
4258
+ f"{' (not ready, transient)' if _lp_code2 == -50019 else ''}"
4259
+ " - proceeding"
4222
4260
  )
4223
4261
  except RuntimeError:
4224
4262
  raise
@@ -7085,10 +7123,18 @@ class CameraMixin(_CameraControlsMixin):
7085
7123
  _lp_arrived = True
7086
7124
  _lp_code_sdes = int(_lp_resp_sdes.get("code", 200))
7087
7125
  _lp_on_sdes = int(_lp_resp_sdes.get("livePlay", 1))
7088
- if _lp_code_sdes not in (0, 200) or _lp_on_sdes == 0:
7126
+ # Only an explicit livePlay=0 is an unambiguous refusal (fast-fail).
7127
+ # Numeric codes (e.g. -50019 "not ready" on a waking battery cam)
7128
+ # are transient - the camera recovers and streams - so log and
7129
+ # proceed rather than abort on a code we can't classify as terminal
7130
+ # (genuine terminal rejects are still caught on the webrtcResp ack).
7131
+ if _lp_on_sdes == 0:
7089
7132
  raise RuntimeError(
7090
- f"livePlay rejected by camera (code={_lp_code_sdes}, livePlay={_lp_on_sdes})"
7091
- )
7133
+ f"livePlay refused by camera (livePlay=0, code={_lp_code_sdes})")
7134
+ elif _lp_code_sdes not in (0, 200):
7135
+ _status(f"livePlayResp: non-OK code {_lp_code_sdes}"
7136
+ f"{' (not ready, transient)' if _lp_code_sdes == -50019 else ''}"
7137
+ " - proceeding")
7092
7138
  except TimeoutError:
7093
7139
  pass
7094
7140
  _LOGGER.info(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.7.23
3
+ Version: 0.7.25
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
@@ -38,6 +38,22 @@ def test_env_falsey_values(monkeypatch):
38
38
  assert _cam()._resolve_sdes_fast_liveplay() is False, val
39
39
 
40
40
 
41
+ def test_role_reversal_model_always_excluded(monkeypatch):
42
+ # A001064 (role-reversal) must NOT use the flag even when opt/env force it on -
43
+ # the early webrtcReq degrades its media reliability.
44
+ from types import SimpleNamespace
45
+ monkeypatch.setenv("AIDOT_SDES_FAST_LIVEPLAY", "1")
46
+ cam = _cam()
47
+ cam.info = SimpleNamespace(model_id="LK.IPC.A001064")
48
+ cam._sdes_fast_liveplay_opt = True
49
+ assert cam._resolve_sdes_fast_liveplay() is False
50
+ # a non-excluded SDES model still honours the flag
51
+ cam2 = _cam()
52
+ cam2.info = SimpleNamespace(model_id="LK.IPC.A001513")
53
+ cam2._sdes_fast_liveplay_opt = True
54
+ assert cam2._resolve_sdes_fast_liveplay() is True
55
+
56
+
41
57
  def test_kwarg_option_wins_over_env(monkeypatch):
42
58
  monkeypatch.setenv("AIDOT_SDES_FAST_LIVEPLAY", "1")
43
59
  cam = _cam()