python-aidot-cameras 0.7.18__tar.gz → 0.7.19__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-0.7.18/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.19}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/pyproject.toml +1 -1
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/client.py +83 -8
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.7.19/tests/test_stream_idle.py +69 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/LICENSE +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/README.md +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/setup.cfg +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/py.typed +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_token_refresh.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.19
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.19"
|
|
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"
|
|
@@ -508,6 +508,9 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
508
508
|
# Cold-start serve-port relay (holds the public port connectable through
|
|
509
509
|
# the handshake so an eager go2rtc pull waits instead of being refused).
|
|
510
510
|
self._serve_relay: "Optional[_ServeRelay]" = None
|
|
511
|
+
# Cold-start instrumentation: monotonic at the start of an open, so any
|
|
512
|
+
# thread (signaling, bridge, serve) can log elapsed-ms phase markers.
|
|
513
|
+
self._cold_open_t0: "Optional[float]" = None
|
|
511
514
|
# Motion/event polling (cloud event list). NOTE: the camera does NOT push motion
|
|
512
515
|
# to a passive MQTT subscriber - alarmType is only emitted during an active live
|
|
513
516
|
# view (decompiled NewLiveFragment) - so real-time-ish motion for HA comes from
|
|
@@ -1946,6 +1949,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
1946
1949
|
go2rtc_url: Optional[str] = None,
|
|
1947
1950
|
live_stream_param: Optional[bool] = None,
|
|
1948
1951
|
serve_relay: Optional[bool] = None,
|
|
1952
|
+
stream_idle_s: Optional[float] = None,
|
|
1949
1953
|
) -> None:
|
|
1950
1954
|
"""Start a persistent stream that keeps the camera session alive.
|
|
1951
1955
|
|
|
@@ -1974,6 +1978,14 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
1974
1978
|
``AIDOT_SERVE_RELAY`` env var (default on); set False to serve ffmpeg
|
|
1975
1979
|
directly on the public port (the pre-0.7.18 behaviour).
|
|
1976
1980
|
|
|
1981
|
+
``stream_idle_s`` sets the no-viewer idle-release window in seconds,
|
|
1982
|
+
overriding the ``AIDOT_STREAM_IDLE_S`` env default (120). ``0`` (or
|
|
1983
|
+
negative) keeps the warm WebRTC session forever so re-views are instant
|
|
1984
|
+
(app-like) - sensible for mains cameras (no battery cost), but it holds a
|
|
1985
|
+
concurrent-stream slot for the camera's lifetime, so don't pin more
|
|
1986
|
+
cameras than ``AIDOT_MAX_CONCURRENT_STREAMS`` allows. Leave None for
|
|
1987
|
+
battery cameras (motion-prewarm + idle-release preserves battery).
|
|
1988
|
+
|
|
1977
1989
|
Safe to call multiple times - does nothing if already running.
|
|
1978
1990
|
"""
|
|
1979
1991
|
if fast_connect is not None:
|
|
@@ -1984,6 +1996,8 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
1984
1996
|
self._live_stream_param_opt = live_stream_param
|
|
1985
1997
|
if serve_relay is not None:
|
|
1986
1998
|
self._serve_relay_opt = serve_relay
|
|
1999
|
+
if stream_idle_s is not None:
|
|
2000
|
+
self._stream_idle_opt = stream_idle_s
|
|
1987
2001
|
if self._stream_task is not None and not self._stream_task.done():
|
|
1988
2002
|
return
|
|
1989
2003
|
self._keepalive_rtsp_url = rtsp_push_url
|
|
@@ -2236,8 +2250,10 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
2236
2250
|
# window as DTLS; the next view re-runs camera.stream_source(). Fail
|
|
2237
2251
|
# safe: unknown (non-Linux /proc) never releases. Escape hatch:
|
|
2238
2252
|
# AIDOT_SDES_IDLE_RELEASE=0.
|
|
2239
|
-
|
|
2240
|
-
_idle_secs =
|
|
2253
|
+
# stream_idle_s / AIDOT_STREAM_IDLE_S override; <= 0 = never release.
|
|
2254
|
+
_idle_secs = self._resolve_idle_secs()
|
|
2255
|
+
_idle_on = (os.environ.get("AIDOT_SDES_IDLE_RELEASE", "1") != "0"
|
|
2256
|
+
and _idle_secs > 0)
|
|
2241
2257
|
_serve_port = _sdes_serve_port(self._keepalive_rtsp_url)
|
|
2242
2258
|
_last_consumer = _started_at # grace: count idle from session open
|
|
2243
2259
|
try:
|
|
@@ -2480,6 +2496,38 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
2480
2496
|
got_a = self._install_encoded_tap(_r, aq, False)
|
|
2481
2497
|
return got_v
|
|
2482
2498
|
|
|
2499
|
+
def _cold_phase(self, label: str) -> None:
|
|
2500
|
+
"""Log elapsed-ms since the open began (cold-start instrumentation).
|
|
2501
|
+
|
|
2502
|
+
Defensive + best-effort: never raises, no-op if no open is in flight.
|
|
2503
|
+
Emits a single greppable INFO line per phase so a cold connect's timeline
|
|
2504
|
+
(open -> webrtcReq -> first-media -> serving) can be measured without a
|
|
2505
|
+
debugger or ad-hoc log scraping. Grep ``cold-start[``."""
|
|
2506
|
+
try:
|
|
2507
|
+
t0 = self._cold_open_t0
|
|
2508
|
+
if t0 is None:
|
|
2509
|
+
return
|
|
2510
|
+
_LOGGER.info(
|
|
2511
|
+
"cold-start[%s] %s +%dms",
|
|
2512
|
+
self.device_id, label, int((time.monotonic() - t0) * 1000),
|
|
2513
|
+
)
|
|
2514
|
+
except Exception:
|
|
2515
|
+
pass
|
|
2516
|
+
|
|
2517
|
+
def _resolve_idle_secs(self) -> float:
|
|
2518
|
+
"""Serve idle-release window (seconds). A per-camera ``stream_idle_s``
|
|
2519
|
+
(set via start_keepalive) overrides the ``AIDOT_STREAM_IDLE_S`` env
|
|
2520
|
+
default of 120 s. <= 0 means *never* idle-release (keep the warm session
|
|
2521
|
+
for instant re-views — sensible for mains cameras, which have no battery
|
|
2522
|
+
cost; note it holds a concurrent-stream slot for the camera's lifetime)."""
|
|
2523
|
+
opt = getattr(self, "_stream_idle_opt", None)
|
|
2524
|
+
if opt is not None:
|
|
2525
|
+
return float(opt)
|
|
2526
|
+
try:
|
|
2527
|
+
return float(os.environ.get("AIDOT_STREAM_IDLE_S", "120"))
|
|
2528
|
+
except (TypeError, ValueError):
|
|
2529
|
+
return 120.0
|
|
2530
|
+
|
|
2483
2531
|
def _maybe_start_serve_relay(self, serve_url: Optional[str]) -> "Optional[_ServeRelay]":
|
|
2484
2532
|
"""Hold the public serve port via a _ServeRelay so an eager go2rtc pull
|
|
2485
2533
|
connects-and-waits instead of hitting ECONNREFUSED during the ~16-25s
|
|
@@ -2646,7 +2694,9 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
2646
2694
|
# Must exceed HA's stream-worker reconnect interval (~40s) or the serve
|
|
2647
2695
|
# is torn down between a viewer's retries -> "Connection refused". 120s
|
|
2648
2696
|
# survives several retries while still releasing an unviewed camera.
|
|
2649
|
-
|
|
2697
|
+
# stream_idle_s / AIDOT_STREAM_IDLE_S override; <= 0 = never release
|
|
2698
|
+
# (keep warm for instant re-views; mains cameras only - see P1).
|
|
2699
|
+
idle_secs = self._resolve_idle_secs()
|
|
2650
2700
|
proc = wfile = stop_flag = mux_thread = None
|
|
2651
2701
|
cancelled = idle_release = False
|
|
2652
2702
|
try:
|
|
@@ -2705,6 +2755,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
2705
2755
|
# client and tear down before go2rtc connects.)
|
|
2706
2756
|
await asyncio.sleep(0.3)
|
|
2707
2757
|
self._serve_ready.set()
|
|
2758
|
+
self._cold_phase("serving (dtls)")
|
|
2708
2759
|
# Periodic keyframe request shortens the camera's GOP so HA's
|
|
2709
2760
|
# HLS segmenter (which cuts on keyframes) produces short
|
|
2710
2761
|
# segments -> far less player buffering (the ~20s HLS lag is
|
|
@@ -2722,8 +2773,9 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
2722
2773
|
await self._send_video_pli(pc)
|
|
2723
2774
|
_last_gop_pli = loop.time()
|
|
2724
2775
|
# No consumer -> the pipe fills, the mux blocks, progress
|
|
2725
|
-
# goes stale. A real viewer keeps it fresh.
|
|
2726
|
-
|
|
2776
|
+
# goes stale. A real viewer keeps it fresh. idle_secs<=0
|
|
2777
|
+
# disables release entirely (keep warm for instant views).
|
|
2778
|
+
if idle_secs > 0 and loop.time() - progress[0] > idle_secs:
|
|
2727
2779
|
idle_release = True
|
|
2728
2780
|
break
|
|
2729
2781
|
# Tear down this ffmpeg+mux cycle before the next.
|
|
@@ -3236,6 +3288,10 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
3236
3288
|
"""
|
|
3237
3289
|
import queue as _q_mod
|
|
3238
3290
|
|
|
3291
|
+
# Cold-start instrumentation: stamp t0 so phase markers below (and in the
|
|
3292
|
+
# bridge/serve threads) log elapsed-ms. Grep ``cold-start[``.
|
|
3293
|
+
self._cold_open_t0 = time.monotonic()
|
|
3294
|
+
|
|
3239
3295
|
use_sdes = force_sdes if force_sdes is not None else self.is_sdes_camera
|
|
3240
3296
|
|
|
3241
3297
|
# Battery cameras must have their live-stream session provisioned by the
|
|
@@ -4912,6 +4968,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
4912
4968
|
},
|
|
4913
4969
|
})
|
|
4914
4970
|
outgoing_q.put_nowait((webrtc_req_topic, webrtc_req_payload))
|
|
4971
|
+
self._cold_phase("webrtcReq")
|
|
4915
4972
|
_status(f"webrtcReq sent peerid={peer_id}"
|
|
4916
4973
|
f" IceServerList×{len(_ice_server_list)}"
|
|
4917
4974
|
f" payload={len(webrtc_req_payload)}B")
|
|
@@ -8404,9 +8461,25 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
8404
8461
|
# on the camera's own GOP). We can't decode here to detect the
|
|
8405
8462
|
# first keyframe, so we time-box the burst and keep a slow
|
|
8406
8463
|
# safety PLI in case the camera's GOP is long.
|
|
8407
|
-
|
|
8464
|
+
# P2: front-load the burst so the first decodable IDR arrives
|
|
8465
|
+
# sooner on a cold open. Was 3 PLIs at a flat 5 s (first IDR
|
|
8466
|
+
# up to ~10 s); now a denser early ramp, then the same 30 s
|
|
8467
|
+
# safety PLI. AIDOT_SDES_PLI_GAPS overrides the early gaps
|
|
8468
|
+
# (comma-sep seconds; e.g. "5,5,5" restores the old cadence).
|
|
8469
|
+
_pli_gaps = getattr(_bridge_fn, '_pli_gaps', None)
|
|
8470
|
+
if _pli_gaps is None:
|
|
8471
|
+
try:
|
|
8472
|
+
_pli_gaps = tuple(
|
|
8473
|
+
float(_x) for _x in os.environ.get(
|
|
8474
|
+
"AIDOT_SDES_PLI_GAPS", "0,1.5,2,3").split(",")
|
|
8475
|
+
if _x.strip()
|
|
8476
|
+
) or (0.0, 1.5, 2.0, 3.0)
|
|
8477
|
+
except ValueError:
|
|
8478
|
+
_pli_gaps = (0.0, 1.5, 2.0, 3.0)
|
|
8479
|
+
_bridge_fn._pli_gaps = _pli_gaps
|
|
8408
8480
|
_pli_done = getattr(_bridge_fn, '_pli_count', 0)
|
|
8409
|
-
_pli_interval =
|
|
8481
|
+
_pli_interval = (_pli_gaps[_pli_done]
|
|
8482
|
+
if _pli_done < len(_pli_gaps) else 30.0)
|
|
8410
8483
|
if (hasattr(_bridge_fn, '_cam_video_ssrc')
|
|
8411
8484
|
and hasattr(_bridge_fn, '_cam_srtp_sock')
|
|
8412
8485
|
and _time_br.time() - getattr(
|
|
@@ -8455,7 +8528,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
8455
8528
|
_bridge_fn._last_pli_ts = _time_br.time()
|
|
8456
8529
|
_pli_n = getattr(_bridge_fn, '_pli_count', 0) + 1
|
|
8457
8530
|
_bridge_fn._pli_count = _pli_n
|
|
8458
|
-
if _pli_n <=
|
|
8531
|
+
if _pli_n <= len(_pli_gaps):
|
|
8459
8532
|
_status(
|
|
8460
8533
|
f"SDES: sent RTCP PLI #{_pli_n}"
|
|
8461
8534
|
f" → SSRC=0x{_pli_media_ssrc:08x}"
|
|
@@ -9334,6 +9407,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
9334
9407
|
_bridge_fn._srtp_rx_sess = _plsrtp_rx.Session(
|
|
9335
9408
|
policy=_rx_pol
|
|
9336
9409
|
)
|
|
9410
|
+
self._cold_phase("first-media")
|
|
9337
9411
|
_status("bridge: SRTP RX session ready (cam→us)")
|
|
9338
9412
|
except Exception as _srx_e:
|
|
9339
9413
|
_status(f"bridge: SRTP RX init failed: {_srx_e}")
|
|
@@ -9740,6 +9814,7 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
9740
9814
|
await asyncio.sleep(0.05)
|
|
9741
9815
|
|
|
9742
9816
|
_bind_ms = int((time.monotonic() - _t0) * 1000)
|
|
9817
|
+
self._cold_phase("serving (sdes ffmpeg bound)")
|
|
9743
9818
|
if _bound:
|
|
9744
9819
|
_status(
|
|
9745
9820
|
f"SDES ffmpeg ready - loopback audio={_lo_audio_port}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.19
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Unit tests for _resolve_idle_secs - the serve idle-release policy (P1).
|
|
2
|
+
|
|
3
|
+
The no-viewer idle window decides when a warm WebRTC session is torn down. A
|
|
4
|
+
per-camera ``stream_idle_s`` (from start_keepalive) overrides AIDOT_STREAM_IDLE_S
|
|
5
|
+
(default 120 s); <= 0 means never release (keep mains cameras warm for instant
|
|
6
|
+
re-views). No network/camera: drives a bare instance.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
12
|
+
|
|
13
|
+
import aidot.camera.client as cc
|
|
14
|
+
|
|
15
|
+
_CAM = next(v for v in vars(cc).values()
|
|
16
|
+
if isinstance(v, type) and "_resolve_idle_secs" in v.__dict__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _cam():
|
|
20
|
+
return _CAM.__new__(_CAM)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_default_is_120_when_unset(monkeypatch):
|
|
24
|
+
monkeypatch.delenv("AIDOT_STREAM_IDLE_S", raising=False)
|
|
25
|
+
assert _cam()._resolve_idle_secs() == 120.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_env_override(monkeypatch):
|
|
29
|
+
monkeypatch.setenv("AIDOT_STREAM_IDLE_S", "300")
|
|
30
|
+
assert _cam()._resolve_idle_secs() == 300.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_kwarg_option_wins_over_env(monkeypatch):
|
|
34
|
+
monkeypatch.setenv("AIDOT_STREAM_IDLE_S", "300")
|
|
35
|
+
cam = _cam()
|
|
36
|
+
cam._stream_idle_opt = 45.0 # what start_keepalive(stream_idle_s=45) sets
|
|
37
|
+
assert cam._resolve_idle_secs() == 45.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_zero_means_never_release():
|
|
41
|
+
cam = _cam()
|
|
42
|
+
cam._stream_idle_opt = 0.0
|
|
43
|
+
# The serve loops treat <= 0 as "never release"; the resolver just returns it.
|
|
44
|
+
assert cam._resolve_idle_secs() == 0.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_bad_env_falls_back_to_120(monkeypatch):
|
|
48
|
+
monkeypatch.setenv("AIDOT_STREAM_IDLE_S", "not-a-number")
|
|
49
|
+
assert _cam()._resolve_idle_secs() == 120.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
import traceback
|
|
54
|
+
|
|
55
|
+
class _MP: # minimal monkeypatch stand-in for direct `python` runs
|
|
56
|
+
def setenv(self, k, v): os.environ[k] = v
|
|
57
|
+
def delenv(self, k, raising=False): os.environ.pop(k, None)
|
|
58
|
+
|
|
59
|
+
_fail = 0
|
|
60
|
+
for _k, _v in sorted(globals().items()):
|
|
61
|
+
if _k.startswith("test_"):
|
|
62
|
+
try:
|
|
63
|
+
_v(_MP()) if _v.__code__.co_argcount else _v()
|
|
64
|
+
print(f"PASS {_k}")
|
|
65
|
+
except Exception:
|
|
66
|
+
_fail += 1
|
|
67
|
+
print(f"FAIL {_k}")
|
|
68
|
+
traceback.print_exc()
|
|
69
|
+
raise SystemExit(1 if _fail else 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/device_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/src/aidot/models/discover_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.18 → python_aidot_cameras-0.7.19}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|