python-aidot-cameras 0.12.10__tar.gz → 0.12.12__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.12.10/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.12}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/client.py +58 -2
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/pyproject.toml +1 -1
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- python_aidot_cameras-0.12.12/tests/test_no_self_referential_go2rtc.py +47 -0
- python_aidot_cameras-0.12.12/tests/test_viewer_check_throttle.py +71 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/LICENSE +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/README.md +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.10/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.12}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.12
|
|
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
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/client.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
from urllib.parse import urlparse
|
|
5
6
|
import os
|
|
6
7
|
import random
|
|
7
8
|
import time
|
|
@@ -147,6 +148,13 @@ _BG_TASKS: set = set()
|
|
|
147
148
|
# means refetching is not helping, so back off rather than loop.
|
|
148
149
|
_MQTT_CREDENTIAL_REFETCH_FLOOR = 30.0
|
|
149
150
|
|
|
151
|
+
# How often the "is anyone watching" question may actually reach go2rtc. The
|
|
152
|
+
# watchdog loops tick twice a second; asking that often opens a fresh HTTP
|
|
153
|
+
# session per camera per tick against the service that also has to serve the
|
|
154
|
+
# video. Idle release is measured in minutes, so seconds of staleness here costs
|
|
155
|
+
# nothing.
|
|
156
|
+
_VIEWER_CHECK_INTERVAL_S = 10.0
|
|
157
|
+
|
|
150
158
|
|
|
151
159
|
def _spawn_bg(coro):
|
|
152
160
|
_t = asyncio.ensure_future(coro)
|
|
@@ -337,6 +345,22 @@ def _save_frame_as_jpeg(image_data: Any, output_path: str) -> bool:
|
|
|
337
345
|
return False
|
|
338
346
|
|
|
339
347
|
|
|
348
|
+
def _is_self_referential_source(source: str, stream_name: str) -> bool:
|
|
349
|
+
"""True when registering ``source`` under ``stream_name`` would loop.
|
|
350
|
+
|
|
351
|
+
A go2rtc stream whose source is its own RTSP address becomes its own
|
|
352
|
+
producer: the stream lists a producer, nothing feeds it, and every consumer
|
|
353
|
+
gets a connection with no media. That is exactly the shape of a PUSH URL,
|
|
354
|
+
``rtsp://host:8554/<stream name>`` - the address the keepalive publishes to.
|
|
355
|
+
"""
|
|
356
|
+
if not source or not stream_name:
|
|
357
|
+
return False
|
|
358
|
+
if not source.lower().startswith(("rtsp://", "rtsps://")):
|
|
359
|
+
return False
|
|
360
|
+
path = urlparse(source).path.strip("/")
|
|
361
|
+
return path == stream_name
|
|
362
|
+
|
|
363
|
+
|
|
340
364
|
def _build_sdes_serve_cmd(
|
|
341
365
|
*,
|
|
342
366
|
sdp_path: str,
|
|
@@ -2763,6 +2787,18 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2763
2787
|
if not (self._streaming_active and self._go2rtc_url and self._keepalive_rtsp_url):
|
|
2764
2788
|
return
|
|
2765
2789
|
name = self._go2rtc_stream_name()
|
|
2790
|
+
if _is_self_referential_source(self._keepalive_rtsp_url, name):
|
|
2791
|
+
# PUSH mode: the keepalive publishes INTO go2rtc, so this URL is
|
|
2792
|
+
# go2rtc's own address for this very stream. Registering it as the
|
|
2793
|
+
# stream's source makes go2rtc its own producer - a loop that yields
|
|
2794
|
+
# a stream with a producer attached and no media coming out of it.
|
|
2795
|
+
# In push mode the publisher already feeds the stream; there is
|
|
2796
|
+
# nothing to register.
|
|
2797
|
+
_LOGGER.debug(
|
|
2798
|
+
"camera %s: not registering %s with go2rtc - the push URL is that "
|
|
2799
|
+
"stream's own address", self.device_id, name,
|
|
2800
|
+
)
|
|
2801
|
+
return
|
|
2766
2802
|
try:
|
|
2767
2803
|
async with aiohttp.ClientSession() as _s2:
|
|
2768
2804
|
url = await prefer_go2rtc(
|
|
@@ -2908,6 +2944,10 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2908
2944
|
register, deregister and viewer-count paths cannot drift apart."""
|
|
2909
2945
|
return f"aidot_{self.device_id[:12]}"
|
|
2910
2946
|
|
|
2947
|
+
# Cached answer to "is anyone watching", so the watchdog loops can ask on
|
|
2948
|
+
# every tick without hammering go2rtc.
|
|
2949
|
+
_viewer_cache: "tuple[float, Optional[bool]]" = (0.0, None)
|
|
2950
|
+
|
|
2911
2951
|
async def _viewer_present(self, port: int) -> "Optional[bool]":
|
|
2912
2952
|
"""Is anyone ACTUALLY watching? True / False / None (unknown).
|
|
2913
2953
|
|
|
@@ -2923,6 +2963,17 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2923
2963
|
Falls back to the TCP-table check when go2rtc is not in use, and returns
|
|
2924
2964
|
None (never release) if neither can answer.
|
|
2925
2965
|
"""
|
|
2966
|
+
# THROTTLE FIRST. The DTLS watchdog calls this every 0.5s and the SDES
|
|
2967
|
+
# one on a similar cadence, and the go2rtc query below opens a fresh HTTP
|
|
2968
|
+
# session each time - so an unthrottled check is two requests per second
|
|
2969
|
+
# per camera, forever, against the very service that has to serve the
|
|
2970
|
+
# video. On a small fleet that is enough load to stop go2rtc answering
|
|
2971
|
+
# at all, which looks exactly like "no video anywhere".
|
|
2972
|
+
_now_mono = time.monotonic()
|
|
2973
|
+
_last, _cached = self._viewer_cache
|
|
2974
|
+
if _now_mono - _last < _VIEWER_CHECK_INTERVAL_S:
|
|
2975
|
+
return _cached
|
|
2976
|
+
|
|
2926
2977
|
name = self._go2rtc_stream_name()
|
|
2927
2978
|
base = self._go2rtc_url
|
|
2928
2979
|
if not base:
|
|
@@ -2939,6 +2990,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2939
2990
|
"no go2rtc url and the serve is an RTSP push to a shared "
|
|
2940
2991
|
"port; cannot tell whether anyone is watching",
|
|
2941
2992
|
)
|
|
2993
|
+
self._viewer_cache = (_now_mono, None)
|
|
2942
2994
|
return None
|
|
2943
2995
|
if base and name:
|
|
2944
2996
|
try:
|
|
@@ -2948,10 +3000,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2948
3000
|
async with aiohttp.ClientSession() as _s:
|
|
2949
3001
|
viewers = await Go2rtcClient(_s, base).viewer_count(name)
|
|
2950
3002
|
if viewers is not None:
|
|
2951
|
-
|
|
3003
|
+
_answer = viewers > 0
|
|
3004
|
+
self._viewer_cache = (_now_mono, _answer)
|
|
3005
|
+
return _answer
|
|
2952
3006
|
except Exception as exc:
|
|
2953
3007
|
_LOGGER.debug("go2rtc viewer check failed: %s", exc)
|
|
2954
|
-
|
|
3008
|
+
_answer = self._sdes_serve_consumer_present(port)
|
|
3009
|
+
self._viewer_cache = (_now_mono, _answer)
|
|
3010
|
+
return _answer
|
|
2955
3011
|
|
|
2956
3012
|
async def _sdes_keepalive_loop(self) -> None:
|
|
2957
3013
|
"""Keep the cold-start serve relay alive around the SDES keepalive loop.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.12"
|
|
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"
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.12
|
|
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
|
|
@@ -98,6 +98,7 @@ tests/test_mqtt_credential_self_heal.py
|
|
|
98
98
|
tests/test_mqtt_password_not_persisted.py
|
|
99
99
|
tests/test_mqtt_session_reconnect.py
|
|
100
100
|
tests/test_narrow_pc_ice.py
|
|
101
|
+
tests/test_no_self_referential_go2rtc.py
|
|
101
102
|
tests/test_no_undefined_names.py
|
|
102
103
|
tests/test_offline_keepalive_pause.py
|
|
103
104
|
tests/test_open_fail_logger.py
|
|
@@ -136,5 +137,6 @@ tests/test_terminal_ack.py
|
|
|
136
137
|
tests/test_token_file_robustness.py
|
|
137
138
|
tests/test_token_refresh.py
|
|
138
139
|
tests/test_upstream_compat.py
|
|
140
|
+
tests/test_viewer_check_throttle.py
|
|
139
141
|
tests/test_viewer_presence.py
|
|
140
142
|
tests/test_wait_or_event.py
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""A go2rtc stream must never be registered as its own source.
|
|
2
|
+
|
|
3
|
+
In PUSH mode the keepalive publishes INTO go2rtc, so the "keepalive rtsp url" is
|
|
4
|
+
go2rtc's own address for that stream. Registering it as the stream's source makes
|
|
5
|
+
go2rtc its own producer: the stream lists a producer, nothing feeds it, and every
|
|
6
|
+
consumer gets a connection with no media.
|
|
7
|
+
|
|
8
|
+
Seen live after the integration began passing go2rtc_url: one camera's stream had
|
|
9
|
+
two producers, one of them `rtsp://127.0.0.1:8554/aidot_12b144cb12da` - its own
|
|
10
|
+
address - and go2rtc returned HTTP 200 with a zero-byte frame for it.
|
|
11
|
+
"""
|
|
12
|
+
from aidot_cameras.camera.client import _is_self_referential_source as loops
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_push_url_for_the_same_stream_is_self_referential():
|
|
16
|
+
assert loops("rtsp://127.0.0.1:8554/aidot_12b144cb12da",
|
|
17
|
+
"aidot_12b144cb12da") is True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_push_url_for_a_different_stream_is_fine():
|
|
21
|
+
# Publishing camera A into stream B is odd but not a loop.
|
|
22
|
+
assert loops("rtsp://127.0.0.1:8554/aidot_other", "aidot_12b144cb12da") is False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_the_http_serve_url_is_a_real_source():
|
|
26
|
+
# PULL mode: go2rtc fetches from the library's own serve. Must still register.
|
|
27
|
+
assert loops("http://127.0.0.1:18931/ecf4937b640c4c96a102ffdc9b51c2a0.ts",
|
|
28
|
+
"aidot_ecf4937b640c") is False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_rtsps_is_covered_too():
|
|
32
|
+
assert loops("rtsps://127.0.0.1:8554/aidot_x", "aidot_x") is True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_trailing_slash_and_case_do_not_hide_it():
|
|
36
|
+
assert loops("RTSP://127.0.0.1:8554/aidot_x/", "aidot_x") is True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_missing_values_are_not_self_referential():
|
|
40
|
+
assert loops("", "aidot_x") is False
|
|
41
|
+
assert loops("rtsp://127.0.0.1:8554/aidot_x", "") is False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_a_remote_go2rtc_push_url_still_counts():
|
|
45
|
+
# The host does not matter; publishing into the stream you are registering is
|
|
46
|
+
# a loop wherever go2rtc lives.
|
|
47
|
+
assert loops("rtsp://go2rtc.lan:8554/aidot_x", "aidot_x") is True
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""The viewer check must not be asked of go2rtc on every watchdog tick.
|
|
2
|
+
|
|
3
|
+
The DTLS watchdog loops twice a second, and the go2rtc query opens a fresh HTTP
|
|
4
|
+
session per call - so an unthrottled check is two requests per second per camera,
|
|
5
|
+
forever, aimed at the very service that also has to serve the video. On a small
|
|
6
|
+
fleet that was enough to stop go2rtc answering at all, which presents as no video
|
|
7
|
+
anywhere. Idle release is measured in minutes, so a few seconds of staleness in
|
|
8
|
+
the answer costs nothing.
|
|
9
|
+
"""
|
|
10
|
+
import asyncio
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
import aidot_cameras.camera.client as cc
|
|
14
|
+
from aidot_cameras.device_client import CameraDeviceClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _client(answer):
|
|
18
|
+
c = CameraDeviceClient.__new__(CameraDeviceClient)
|
|
19
|
+
c._go2rtc_url = None
|
|
20
|
+
c._keepalive_rtsp_url = "http://127.0.0.1:18931/x.ts"
|
|
21
|
+
c.device_id = "abc123abc123def"
|
|
22
|
+
c._viewer_cache = (0.0, None)
|
|
23
|
+
calls = []
|
|
24
|
+
|
|
25
|
+
def _probe(port):
|
|
26
|
+
calls.append(port)
|
|
27
|
+
return answer
|
|
28
|
+
|
|
29
|
+
c._sdes_serve_consumer_present = _probe
|
|
30
|
+
return c, calls
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_repeated_calls_hit_the_backend_once():
|
|
34
|
+
c, calls = _client(True)
|
|
35
|
+
|
|
36
|
+
async def go():
|
|
37
|
+
for _ in range(40): # 40 watchdog ticks
|
|
38
|
+
await c._viewer_present(18931)
|
|
39
|
+
asyncio.run(go())
|
|
40
|
+
assert len(calls) == 1, f"backend hit {len(calls)} times, expected 1"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_the_cached_answer_is_returned_verbatim():
|
|
44
|
+
for answer in (True, False, None):
|
|
45
|
+
c, _ = _client(answer)
|
|
46
|
+
|
|
47
|
+
async def go():
|
|
48
|
+
first = await c._viewer_present(18931)
|
|
49
|
+
second = await c._viewer_present(18931)
|
|
50
|
+
return first, second
|
|
51
|
+
first, second = asyncio.run(go())
|
|
52
|
+
assert first == answer and second == answer
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_the_cache_expires_so_the_answer_can_change():
|
|
56
|
+
c, calls = _client(True)
|
|
57
|
+
|
|
58
|
+
async def go():
|
|
59
|
+
await c._viewer_present(18931)
|
|
60
|
+
# Pretend the interval has elapsed.
|
|
61
|
+
stamp, cached = c._viewer_cache
|
|
62
|
+
c._viewer_cache = (stamp - cc._VIEWER_CHECK_INTERVAL_S - 1, cached)
|
|
63
|
+
await c._viewer_present(18931)
|
|
64
|
+
asyncio.run(go())
|
|
65
|
+
assert len(calls) == 2
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_the_interval_is_long_enough_to_matter():
|
|
69
|
+
# The watchdog ticks every 0.5s; anything under a couple of seconds would
|
|
70
|
+
# still be a request storm.
|
|
71
|
+
assert cc._VIEWER_CHECK_INTERVAL_S >= 5.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/clock.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
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rate.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
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/go2rtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/models.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/sdes_open.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/webrtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/aidot_cameras/device_client.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
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_serve_open_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_ice_config_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_idle_release_both_loops.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_keyframe_prompter.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_light_active_color_mode.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_live_stream_param.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_mqtt_credential_refetch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_open_fail_logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_bridge_break_gate.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_ffmpeg_stderr_log.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_idle_release.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_open_cleanup.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_serve_audio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_stream_drain_ownership.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.10 → python_aidot_cameras-0.12.12}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|