python-aidot-cameras 0.12.16__tar.gz → 0.12.17__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.16/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.17}/PKG-INFO +3 -3
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/README.md +2 -2
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/client.py +253 -20
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/constants.py +8 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/sdes_open.py +509 -24
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/webrtc_open.py +64 -29
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/discover.py +2 -2
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/pyproject.toml +3 -2
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17/python_aidot_cameras.egg-info}/PKG-INFO +3 -3
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/SOURCES.txt +4 -1
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_aioice_compat.py +2 -2
- python_aidot_cameras-0.12.17/tests/test_battery_detection.py +206 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_device_login_guard.py +3 -3
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_mux_terminal_write.py +2 -1
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_egress_guard.py +3 -3
- python_aidot_cameras-0.12.17/tests/test_live_stream_param.py +145 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_no_self_referential_go2rtc.py +2 -2
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_session_stats.py +3 -3
- python_aidot_cameras-0.12.17/tests/test_turn_create_permission.py +291 -0
- python_aidot_cameras-0.12.17/tests/test_wake_readiness_retry.py +167 -0
- python_aidot_cameras-0.12.16/tests/test_live_stream_param.py +0 -123
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/LICENSE +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.16/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.17}/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.17
|
|
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
|
|
@@ -171,12 +171,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
171
171
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
172
172
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
173
173
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
174
|
-
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
|
|
174
|
+
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
175
175
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
176
176
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
177
177
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
178
178
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
179
|
-
| `AIDOT_LIVESTREAM_PARAM` |
|
|
179
|
+
| `AIDOT_LIVESTREAM_PARAM` | **No-op (ignored).** Formerly requested the cloud `liveStreamParam` pre-connect for battery cameras. That call provisions the camera toward AWS KVS, so it sends its media there instead of to this library and the live view negotiates and then shows nothing - and battery cameras were the only ones it applied to. Setting it (or `start_keepalive(live_stream_param=True)`) logs one warning and changes nothing. | ignored |
|
|
180
180
|
|
|
181
181
|
### Security hardening
|
|
182
182
|
|
|
@@ -140,12 +140,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
140
140
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
141
141
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
142
142
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
143
|
-
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
|
|
143
|
+
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
144
144
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
145
145
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
146
146
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
147
147
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
148
|
-
| `AIDOT_LIVESTREAM_PARAM` |
|
|
148
|
+
| `AIDOT_LIVESTREAM_PARAM` | **No-op (ignored).** Formerly requested the cloud `liveStreamParam` pre-connect for battery cameras. That call provisions the camera toward AWS KVS, so it sends its media there instead of to this library and the live view negotiates and then shows nothing - and battery cameras were the only ones it applied to. Setting it (or `start_keepalive(live_stream_param=True)`) logs one warning and changes nothing. | ignored |
|
|
149
149
|
|
|
150
150
|
### Security hardening
|
|
151
151
|
|
{python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/client.py
RENAMED
|
@@ -21,6 +21,7 @@ from ..const import (
|
|
|
21
21
|
# in-module references (and any external importers) keep resolving.
|
|
22
22
|
from .constants import (
|
|
23
23
|
_LEEDARSON_APP_ID,
|
|
24
|
+
_LIVE_PLAY_NOT_READY,
|
|
24
25
|
_SMARTHOME_URL_TEMPLATE,
|
|
25
26
|
GETSTREAMCTRL_CMD, # noqa: F401 - re-exported for back-compat (public pair; unused in-module)
|
|
26
27
|
SDES_SPEAKERSTART_DELAY, # noqa: F401 - used by the sdes_open mixin; kept re-exported
|
|
@@ -33,6 +34,7 @@ from .models import ( # re-exported: data models split into models.py
|
|
|
33
34
|
CameraDeviceInformation,
|
|
34
35
|
CameraStatusData,
|
|
35
36
|
VideoFrame,
|
|
37
|
+
_as_int,
|
|
36
38
|
)
|
|
37
39
|
from .tutk import TutkStreamSession # re-exported (split into tutk.py)
|
|
38
40
|
from .playback import ( # re-exported (split into playback.py)
|
|
@@ -906,6 +908,15 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
906
908
|
# A001513 battery cameras). Exclude role-reversal models from the flag.
|
|
907
909
|
_NO_FAST_LIVEPLAY_MODELS: frozenset = frozenset({"LK.IPC.A001064"})
|
|
908
910
|
|
|
911
|
+
# Models confirmed (2026-05-02) to send TUTK-framed data instead of standard
|
|
912
|
+
# SRTP: the camera announces an ASCII-printable fake SDES key and sends
|
|
913
|
+
# packets with a TUTK SFrame header (0xC8 audio / 0xC9 video) and an SSRC
|
|
914
|
+
# that differs from the one advertised in the SDP. For these the ffmpeg SDP
|
|
915
|
+
# uses plain RTP/AVP (no SRTP decryption attempt) and the bridge strips the
|
|
916
|
+
# TUTK header before forwarding. Matched by SUBSTRING (see _use_plain_rtp in
|
|
917
|
+
# sdes_open) so a revision suffix is covered.
|
|
918
|
+
_PLAIN_RTP_MODELS: frozenset = frozenset({"A001064", "A001513"})
|
|
919
|
+
|
|
909
920
|
@property
|
|
910
921
|
def _offer_should_include_datachannel(self) -> bool:
|
|
911
922
|
model = getattr(getattr(self, "info", None), "model_id", None)
|
|
@@ -917,13 +928,111 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
917
928
|
props = getattr(self, "_raw_device", {}).get("properties") or {}
|
|
918
929
|
return str(props.get("enableSdes", "0")) == "1"
|
|
919
930
|
|
|
931
|
+
# Battery / low-power camera models. ONE list, consumed by
|
|
932
|
+
# is_battery_camera and (through it) by every battery guard and by the
|
|
933
|
+
# livePlayReq/webrtcReq ``powerType`` field - it used to be re-typed inline in
|
|
934
|
+
# three files, which is how a guard drifts out of step with the wire payload.
|
|
935
|
+
# Source: IpcServiceImpl.java B() - powerType=2 for these models, 1 for wired.
|
|
936
|
+
# Substring-matched, so a firmware/hardware revision suffix
|
|
937
|
+
# ("LK.IPC.A001513-1") is covered the way A000088-1 already is on the DTLS
|
|
938
|
+
# side rather than silently reading as a different, mains-powered model.
|
|
939
|
+
_BATTERY_MODELS: frozenset = frozenset({"A001513", "A001108", "A001360"})
|
|
940
|
+
|
|
941
|
+
def _battery_evidence(self) -> "Optional[bool]":
|
|
942
|
+
"""Does this camera's OWN cloud data say it runs on a battery?
|
|
943
|
+
|
|
944
|
+
True when the cloud reports a battery for it, else None ("no evidence" -
|
|
945
|
+
NOT "mains", since the device-list payload may simply not carry the field
|
|
946
|
+
yet). Same signal ``lan_control.is_mains_powered`` inverts: a camera that
|
|
947
|
+
reports a battery level has a battery. Never raises - it is read from
|
|
948
|
+
hot paths - and never returns False, so it can only ever ADD a camera to
|
|
949
|
+
the battery set, never take a known model out of it.
|
|
950
|
+
"""
|
|
951
|
+
_raw = getattr(self, "_raw_device", None)
|
|
952
|
+
_props = _raw.get("properties") if isinstance(_raw, dict) else None
|
|
953
|
+
if not isinstance(_props, dict):
|
|
954
|
+
_props = {}
|
|
955
|
+
# A reported battery level means there is a battery to report.
|
|
956
|
+
for _key in ("Battery_remaining", "batteryRemaining", "batteryLevel"):
|
|
957
|
+
if _as_int(_props.get(_key)) is not None:
|
|
958
|
+
return True
|
|
959
|
+
# The low-power flag: batteryMode=2 is what a camera that sleeps between
|
|
960
|
+
# sessions reports (see _async_send_camera_command). Deliberately NOT
|
|
961
|
+
# keyed on ``powerType``: that name is also the value we SEND in
|
|
962
|
+
# livePlayReq, we have never observed the cloud returning it as a device
|
|
963
|
+
# property, and p2pCache - the field right next to it - reads 2 on every
|
|
964
|
+
# camera including mains ones. Reading it here would risk classifying a
|
|
965
|
+
# mains camera as battery, which would put powerType=2 on ITS wire payload
|
|
966
|
+
# and can leave an A000088 un-armed. Evidence has to be unambiguous.
|
|
967
|
+
if _as_int(_props.get("batteryMode")) == 2:
|
|
968
|
+
return True
|
|
969
|
+
# Already-parsed status, for a camera refreshed from an attribute push
|
|
970
|
+
# rather than from a device-list dict.
|
|
971
|
+
_status = getattr(self, "status", None)
|
|
972
|
+
if _as_int(getattr(_status, "battery_remaining", None)) is not None:
|
|
973
|
+
return True
|
|
974
|
+
return None
|
|
975
|
+
|
|
920
976
|
@property
|
|
921
977
|
def is_battery_camera(self) -> bool:
|
|
922
|
-
"""True for battery/low-power
|
|
923
|
-
|
|
978
|
+
"""True for battery/low-power cameras (powerType=2 in the app's B()).
|
|
979
|
+
|
|
980
|
+
Resolved from the camera's own cloud data FIRST (see
|
|
981
|
+
[[_battery_evidence]]), then from the known-model list
|
|
982
|
+
([[_BATTERY_MODELS]]). Evidence leads because a hardcoded list can only
|
|
983
|
+
ever name the revisions we have already seen, and a battery camera that
|
|
984
|
+
is not recognized as one loses every battery protection at once: the SDES
|
|
985
|
+
TURN relay pre-allocation (its ONLY return path - a camera woken through
|
|
986
|
+
the cloud has no LAN host candidate), the cloud keep-alive renew, the
|
|
987
|
+
HTTP wake, and the powerType=2 the camera is told to expect.
|
|
988
|
+
|
|
989
|
+
That misdetection presents as "streams from a standalone run but never
|
|
990
|
+
under Home Assistant", because HA is where the LAN-direct options that
|
|
991
|
+
strip the relay actually get set - a one-off script leaves them at their
|
|
992
|
+
(relay-keeping) defaults, so it never trips the guard it is missing.
|
|
924
993
|
"""
|
|
994
|
+
if self._battery_evidence():
|
|
995
|
+
return True
|
|
925
996
|
model = getattr(getattr(self, "info", None), "model_id", None) or ""
|
|
926
|
-
return any(m in model for m in
|
|
997
|
+
return any(m in model for m in self._BATTERY_MODELS)
|
|
998
|
+
|
|
999
|
+
@property
|
|
1000
|
+
def live_power_type(self) -> int:
|
|
1001
|
+
"""The ``powerType`` field for livePlayReq / webrtcReq: 2 battery, 1 wired.
|
|
1002
|
+
|
|
1003
|
+
Source: IpcServiceImpl.java B(). Derived from [[is_battery_camera]] so
|
|
1004
|
+
the value on the wire and the battery guards can never disagree.
|
|
1005
|
+
"""
|
|
1006
|
+
return 2 if self.is_battery_camera else 1
|
|
1007
|
+
|
|
1008
|
+
def _note_live_play_resp(self, payload) -> None:
|
|
1009
|
+
"""Record the camera's livePlayResp code for this open (loop thread).
|
|
1010
|
+
|
|
1011
|
+
Called from the signaling dispatcher for every livePlayResp matched to
|
|
1012
|
+
this open, whether or not anything is waiting on the future - which
|
|
1013
|
+
matters because ``sdes_fast_liveplay`` is on by default and skips that
|
|
1014
|
+
wait, so this is the only place the code is seen on the SDES path.
|
|
1015
|
+
Best-effort: a malformed payload leaves the record untouched rather than
|
|
1016
|
+
raising into the dispatcher.
|
|
1017
|
+
"""
|
|
1018
|
+
try:
|
|
1019
|
+
self._last_live_play_code = _as_int(payload.get("code"))
|
|
1020
|
+
except (AttributeError, TypeError):
|
|
1021
|
+
_LOGGER.debug("camera %s: unparseable livePlayResp payload",
|
|
1022
|
+
getattr(self, "device_id", "?"), exc_info=True)
|
|
1023
|
+
|
|
1024
|
+
def _live_play_not_ready(self) -> bool:
|
|
1025
|
+
"""Did this open's livePlayResp say the camera is not ready to stream?
|
|
1026
|
+
|
|
1027
|
+
``-50019`` is NOT a refusal on its own - a camera emits it and then
|
|
1028
|
+
recovers via ICE, which is why 0.12.15 stopped treating it as fatal and
|
|
1029
|
+
why nothing here aborts an open on it. Its value is post-hoc: pair it
|
|
1030
|
+
with "the session then delivered no media" and you have a waking battery
|
|
1031
|
+
camera that was not ready yet, as distinct from a generic failure. Only
|
|
1032
|
+
the retry delay keys on it (see the SDES keepalive loop) - never the
|
|
1033
|
+
decision to proceed.
|
|
1034
|
+
"""
|
|
1035
|
+
return getattr(self, "_last_live_play_code", None) == _LIVE_PLAY_NOT_READY
|
|
927
1036
|
|
|
928
1037
|
def _next_dseq(self) -> int:
|
|
929
1038
|
"""Next livePlayReq dseq (app parity: Q0() starts at 100, increments)."""
|
|
@@ -1204,15 +1313,19 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1204
1313
|
async def _async_fetch_live_stream_param(self) -> bool:
|
|
1205
1314
|
"""Provision the camera's live-stream session via the cloud (app parity).
|
|
1206
1315
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
credentials
|
|
1316
|
+
**Not called** - [[_resolve_live_stream_param]] gates this and always says
|
|
1317
|
+
no; see there for why. Kept, with its request shape locked down by
|
|
1318
|
+
tests/test_live_stream_param.py, so re-enabling it for a camera that
|
|
1319
|
+
demonstrably needs it is a one-line change rather than a rediscovery.
|
|
1320
|
+
|
|
1321
|
+
The official app (``KVSPreConnectStrategy.fetchKvsParams``) POSTs the
|
|
1322
|
+
device id to ``/api/ipc/liveStream/liveStreamParam``; the cloud provisions
|
|
1323
|
+
the session (and brings the camera online) and returns AWS KVS
|
|
1324
|
+
credentials. Only the provisioning side effect was ever wanted here - the
|
|
1325
|
+
library streams over the proprietary MQTT/SDES path, not AWS KVS - but
|
|
1326
|
+
that side effect is precisely the problem: the camera then sends its media
|
|
1327
|
+
toward KVS and our bridge receives no video RTP. Best-effort: returns True
|
|
1328
|
+
on HTTP 200.
|
|
1216
1329
|
"""
|
|
1217
1330
|
url, headers, body = self._live_stream_param_request()
|
|
1218
1331
|
return await self._async_post_ok(url, headers, body, label="liveStreamParam")
|
|
@@ -2700,12 +2813,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2700
2813
|
to the ``AIDOT_FAST_CONNECT`` env var; ``True``/``False`` override it (e.g.
|
|
2701
2814
|
from a Home Assistant config-entry option, since HA OS can't set env vars).
|
|
2702
2815
|
|
|
2703
|
-
``live_stream_param``
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2816
|
+
``live_stream_param`` is **accepted but ignored** (a no-op kept so an
|
|
2817
|
+
existing caller doesn't break; it logs one warning if set). It used to
|
|
2818
|
+
request the battery-camera cloud pre-connect, which provisions the camera
|
|
2819
|
+
toward AWS KVS so its media goes there instead of to the SDES bridge - the
|
|
2820
|
+
session negotiates and then serves no video at all. Battery cameras were
|
|
2821
|
+
the only ones it applied to, i.e. exactly the ones it breaks, so the
|
|
2822
|
+
decision is now made once in ``_resolve_live_stream_param`` and is "no".
|
|
2823
|
+
Stop passing it; live video does not depend on it.
|
|
2709
2824
|
|
|
2710
2825
|
``serve_relay`` toggles the cold-start serve-port relay (holds the public
|
|
2711
2826
|
serve port connectable through the WebRTC handshake so an eager go2rtc
|
|
@@ -3071,6 +3186,19 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3071
3186
|
sdes=True)
|
|
3072
3187
|
_peer_reuses = 0
|
|
3073
3188
|
|
|
3189
|
+
# Wake-readiness retries. A battery camera that answers livePlayResp with
|
|
3190
|
+
# -50019 and then sends no media was not refusing - it had not finished
|
|
3191
|
+
# waking. The pacer cannot tell that apart from a degraded camera, so it
|
|
3192
|
+
# escalated the backoff (10s -> 300s) on a camera that would have been
|
|
3193
|
+
# ready seconds later; under a consumer that re-opens per view, that is the
|
|
3194
|
+
# difference between a slow first frame and a live view that never fills
|
|
3195
|
+
# in. Give it the same bounded fast burst the DTLS serve loop already has
|
|
3196
|
+
# (_retry_policy), then fall back to the pacer so a persistently-cold
|
|
3197
|
+
# camera is still not hammered. Bounded to _PEERID_MAX_REUSE so the whole
|
|
3198
|
+
# burst stays on ONE peerid: a fresh peerid registers another camera-side
|
|
3199
|
+
# session, which is what wedged the L2 in the first place (0.12.16).
|
|
3200
|
+
_not_ready_burst = 0
|
|
3201
|
+
|
|
3074
3202
|
while self._streaming_active:
|
|
3075
3203
|
if self._serve_relay is not None:
|
|
3076
3204
|
# Clear any stale backend from a prior session; the open below
|
|
@@ -3257,8 +3385,23 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3257
3385
|
# (camera refused / degraded on a rapid reconnect); a session
|
|
3258
3386
|
# that streamed fine and then ended (battery teardown, consumer
|
|
3259
3387
|
# gone) is a normal lifecycle event and resets to the base interval.
|
|
3388
|
+
# Exception: a battery camera that answered -50019 and then sent
|
|
3389
|
+
# nothing was still waking, not degraded - retry it fast instead.
|
|
3390
|
+
_not_ready_burst = self._next_not_ready_burst(
|
|
3391
|
+
_healthy, _not_ready_burst)
|
|
3392
|
+
_delay, _fast_retry = self._not_ready_retry_delay(
|
|
3393
|
+
_not_ready_burst, burst_max=_PEERID_MAX_REUSE)
|
|
3394
|
+
if _fast_retry:
|
|
3395
|
+
_LOGGER.info(
|
|
3396
|
+
"SDES %s: camera not ready (waking, livePlayResp %d) and "
|
|
3397
|
+
"sent no media - fast retry in %.0fs [%d/%d]",
|
|
3398
|
+
self.device_id, _LIVE_PLAY_NOT_READY, _delay,
|
|
3399
|
+
_not_ready_burst, _PEERID_MAX_REUSE,
|
|
3400
|
+
)
|
|
3401
|
+
else:
|
|
3402
|
+
_delay = _pacer.session_end_delay(healthy=_healthy)
|
|
3260
3403
|
try:
|
|
3261
|
-
await asyncio.sleep(
|
|
3404
|
+
await asyncio.sleep(_delay)
|
|
3262
3405
|
except asyncio.CancelledError:
|
|
3263
3406
|
return
|
|
3264
3407
|
|
|
@@ -3609,6 +3752,54 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3609
3752
|
except (ValueError, TypeError):
|
|
3610
3753
|
return -8.0
|
|
3611
3754
|
|
|
3755
|
+
def _resolve_live_stream_param(self) -> bool:
|
|
3756
|
+
"""Whether to make the cloud ``liveStreamParam`` KVS pre-connect before
|
|
3757
|
+
signaling. **Always False** - see below; the flag survives only so an
|
|
3758
|
+
existing caller doesn't break.
|
|
3759
|
+
|
|
3760
|
+
The call provisions the camera's live session toward AWS KVS, and the
|
|
3761
|
+
camera then sends its media THERE instead of to our SDES bridge: the
|
|
3762
|
+
session negotiates, the bridge binds, and no video RTP ever arrives. It
|
|
3763
|
+
is also made for battery cameras ONLY, so the set of cameras it can
|
|
3764
|
+
affect is exactly the set it is known to break. It was added to cure a
|
|
3765
|
+
``-50019`` ("not ready") livePlayResp, which 0.12.15 established is
|
|
3766
|
+
benign - mains cameras emit it too and recover via ICE.
|
|
3767
|
+
|
|
3768
|
+
0.12.15 turned it off by default but left the option/env able to turn it
|
|
3769
|
+
back on, which is a foot-gun aimed at one foot: a consumer that surfaces
|
|
3770
|
+
it as a setting (the Home Assistant integration surfaces these options
|
|
3771
|
+
because HA OS cannot set env vars) can re-break every battery camera on
|
|
3772
|
+
the account, and the symptom - a live view that negotiates and then shows
|
|
3773
|
+
nothing, forever - looks nothing like a settings problem. A standalone
|
|
3774
|
+
run never passes the option, which is exactly why the same library
|
|
3775
|
+
streams an L2 from the CLI and not under HA.
|
|
3776
|
+
|
|
3777
|
+
So the decision now lives here, in one place, and is "no". Re-enabling
|
|
3778
|
+
it needs a code change plus fresh on-hardware evidence that some camera
|
|
3779
|
+
actually requires it - not a flag flip. A caller that still asks gets
|
|
3780
|
+
one warning naming the camera, so a stuck install says why in the log
|
|
3781
|
+
instead of silently serving nothing.
|
|
3782
|
+
"""
|
|
3783
|
+
if not self.is_battery_camera:
|
|
3784
|
+
# The pre-connect was never made for a mains camera in the first
|
|
3785
|
+
# place; nothing to decide.
|
|
3786
|
+
return False
|
|
3787
|
+
asked = getattr(self, "_live_stream_param_opt", None)
|
|
3788
|
+
if asked is None:
|
|
3789
|
+
asked = os.environ.get("AIDOT_LIVESTREAM_PARAM", "0") != "0"
|
|
3790
|
+
if asked and not getattr(self, "_live_stream_param_warned", False):
|
|
3791
|
+
self._live_stream_param_warned = True
|
|
3792
|
+
_LOGGER.warning(
|
|
3793
|
+
"camera %s: ignoring the liveStreamParam pre-connect request "
|
|
3794
|
+
"(live_stream_param / AIDOT_LIVESTREAM_PARAM). On a battery "
|
|
3795
|
+
"camera it provisions the session toward AWS KVS and the camera "
|
|
3796
|
+
"sends its media there instead of to this library, so the live "
|
|
3797
|
+
"view negotiates and then serves no video at all. The setting "
|
|
3798
|
+
"is a no-op - remove it; live video does not depend on it.",
|
|
3799
|
+
self.device_id,
|
|
3800
|
+
)
|
|
3801
|
+
return False
|
|
3802
|
+
|
|
3612
3803
|
def _resolve_sdes_skip_turn(self) -> bool:
|
|
3613
3804
|
"""EXPERIMENTAL (opt-in, default off): skip the blocking SDES TURN relay
|
|
3614
3805
|
pre-allocation, for cameras reachable LAN-direct.
|
|
@@ -3665,7 +3856,20 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3665
3856
|
``AIDOT_SDES_ADAPTIVE`` env (truthy = 1/true/yes/on), default off. When
|
|
3666
3857
|
off, the per-attempt override is never set, so the explicit
|
|
3667
3858
|
``sdes_fast_liveplay`` / ``sdes_skip_turn`` opts (or their envs) decide -
|
|
3668
|
-
exactly the pre-adaptive behaviour.
|
|
3859
|
+
exactly the pre-adaptive behaviour.
|
|
3860
|
+
|
|
3861
|
+
NEVER on for a battery camera, whatever the opt/env say - for those it is
|
|
3862
|
+
all cost and no benefit. The saving adaptive chases is the TURN relay
|
|
3863
|
+
pre-allocation, and [[_resolve_sdes_skip_turn]] force-KEEPS that for a
|
|
3864
|
+
battery camera (it is the only return path to a cloud-woken camera), while
|
|
3865
|
+
fast-liveplay is already on by default. So a battery "fast" attempt runs
|
|
3866
|
+
the very same handshake as the patient one and differs only in being given
|
|
3867
|
+
45 s to open and a 40 s media grace - inside the documented 25-70 s battery
|
|
3868
|
+
cold-start window. A slow-but-healthy wake is then scored as a fast-path
|
|
3869
|
+
failure: it latches ``_fast_path_unavailable``, escalates the backoff, and
|
|
3870
|
+
burns a camera-side session on a device that frees them slowly."""
|
|
3871
|
+
if getattr(self, "is_battery_camera", False):
|
|
3872
|
+
return False
|
|
3669
3873
|
opt = getattr(self, "_sdes_adaptive_opt", None)
|
|
3670
3874
|
if opt is not None:
|
|
3671
3875
|
return bool(opt)
|
|
@@ -3789,6 +3993,35 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3789
3993
|
adaptive mode is on and the fast path has not already failed this loop."""
|
|
3790
3994
|
return bool(adaptive) and not bool(fast_failed)
|
|
3791
3995
|
|
|
3996
|
+
def _next_not_ready_burst(self, healthy: bool, burst: int) -> int:
|
|
3997
|
+
"""Updated wake-readiness burst counter after a session ended.
|
|
3998
|
+
|
|
3999
|
+
Counts up only while a BATTERY camera keeps answering ``-50019`` and then
|
|
4000
|
+
delivering no media; any other outcome resets it to 0. Scoped to battery
|
|
4001
|
+
cameras because they are the only ones with a wake-readiness state - on a
|
|
4002
|
+
mains camera ``-50019`` is transient noise and the pacer's judgement
|
|
4003
|
+
(something is degraded) is the right one.
|
|
4004
|
+
"""
|
|
4005
|
+
if healthy or not self.is_battery_camera:
|
|
4006
|
+
return 0
|
|
4007
|
+
return burst + 1 if self._live_play_not_ready() else 0
|
|
4008
|
+
|
|
4009
|
+
@staticmethod
|
|
4010
|
+
def _not_ready_retry_delay(burst: int, *,
|
|
4011
|
+
burst_max: int = 3) -> "tuple[float, bool]":
|
|
4012
|
+
"""``(delay, is_fast_retry)`` for a wake-readiness retry.
|
|
4013
|
+
|
|
4014
|
+
Shares [[_retry_policy]] with the DTLS serve loop so both paths agree on
|
|
4015
|
+
what "the camera is still warming up" is worth waiting. Returns
|
|
4016
|
+
``(0.0, False)`` when the counter is 0 (not a wake-readiness failure) and
|
|
4017
|
+
``(_, False)`` once the burst is spent, in both cases handing the decision
|
|
4018
|
+
back to the caller's normal pacer.
|
|
4019
|
+
"""
|
|
4020
|
+
if burst <= 0:
|
|
4021
|
+
return (0.0, False)
|
|
4022
|
+
delay, fast = _retry_policy("not_ready", burst, burst_max=burst_max)
|
|
4023
|
+
return (delay, bool(fast))
|
|
4024
|
+
|
|
3792
4025
|
@staticmethod
|
|
3793
4026
|
def _adaptive_after_attempt(use_fast: bool, healthy: bool, fast_failed: bool) -> bool:
|
|
3794
4027
|
"""Updated ``fast_failed`` after an attempt: latch it once a fast attempt
|
{python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/constants.py
RENAMED
|
@@ -88,6 +88,14 @@ GETSTREAMCTRL_CMD = 802
|
|
|
88
88
|
_STREAM_QUALITY: dict = {"hd": 1, "sd": 5} # AVIOCTRL_QUALITY_MAX / _MIDDLE
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
# livePlayResp code for "not ready": the camera accepted the request but cannot
|
|
92
|
+
# stream yet (a battery camera still waking). Explicitly NOT a refusal - mains
|
|
93
|
+
# cameras emit it too and recover via ICE - so no path aborts an open on it. It
|
|
94
|
+
# is only read after the fact, to tell a waking camera apart from a generically
|
|
95
|
+
# failed session when choosing a retry delay (see _live_play_not_ready).
|
|
96
|
+
_LIVE_PLAY_NOT_READY = -50019
|
|
97
|
+
|
|
98
|
+
|
|
91
99
|
# Two-way-audio (talk) / SDES timing constants (moved from client.py)
|
|
92
100
|
TALK_PCM_RATE = 8000
|
|
93
101
|
TALK_PCM_FRAME_BYTES = 320 # 160 samples (20 ms @ 8 kHz) x 2 bytes (s16)
|