python-aidot-cameras 1.0.0rc8__tar.gz → 1.0.0rc9__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-1.0.0rc8/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc9}/PKG-INFO +2 -2
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/README.md +1 -1
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sdes_open.py +77 -14
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/webrtc_open.py +31 -15
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9/python_aidot_cameras.egg-info}/PKG-INFO +2 -2
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_nominates_as_soon_as_the_answer_lands.py +124 -1
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_echo_wait_is_not_dead_time.py +58 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_wake_gate_app_parity.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_webrtc_answer_race.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc9
|
|
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
|
|
@@ -572,7 +572,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
572
572
|
| `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
|
|
573
573
|
| `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
|
|
574
574
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
575
|
-
| `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway.
|
|
575
|
+
| `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway. Measured on an A001064: time to first media 4195 ms -> 2489 ms. **This value is the wait for a camera never seen to echo.** An echo observed at any point, including after the wait has already expired, is remembered for that camera and every later open on it waits `2.0` s again - so a fleet whose echo band is not empty pays the miss once, says so in the log, and is never shortened again. Setting this explicitly overrides both. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. | `0.25` (`2.0` once an echo has been seen) |
|
|
576
576
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
577
577
|
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
578
578
|
| `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
|
|
@@ -532,7 +532,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
532
532
|
| `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
|
|
533
533
|
| `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
|
|
534
534
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
535
|
-
| `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway.
|
|
535
|
+
| `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway. Measured on an A001064: time to first media 4195 ms -> 2489 ms. **This value is the wait for a camera never seen to echo.** An echo observed at any point, including after the wait has already expired, is remembered for that camera and every later open on it waits `2.0` s again - so a fleet whose echo band is not empty pays the miss once, says so in the log, and is never shortened again. Setting this explicitly overrides both. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. | `0.25` (`2.0` once an echo has been seen) |
|
|
536
536
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
537
537
|
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
538
538
|
| `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
|
{python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -1167,13 +1167,17 @@ def _parse_answer_ice(sdp):
|
|
|
1167
1167
|
order offered, and the first ``typ host`` candidate - which is the one SCTP
|
|
1168
1168
|
is addressed to, and is not necessarily the first candidate listed.
|
|
1169
1169
|
|
|
1170
|
-
Taking the FIRST credential pair is
|
|
1171
|
-
offer carries distinct per-media credentials
|
|
1172
|
-
USE-CANDIDATE on both sockets with the single pair this returns
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1170
|
+
Taking the FIRST credential pair is now measured, not assumed. Our own
|
|
1171
|
+
offer carries distinct per-media credentials and the nomination sends
|
|
1172
|
+
USE-CANDIDATE on both sockets with the single pair this returns, which is
|
|
1173
|
+
only correct if the camera answers one pair for the whole session. Logging
|
|
1174
|
+
every a=ice-ufrag / a=ice-pwd line of the answer per m-section across four
|
|
1175
|
+
cold opens on two models (A001064 mains, A001513 battery) on 2026-09-04:
|
|
1176
|
+
every answer carried ONE pair, repeated identically in the audio, video and
|
|
1177
|
+
application sections. The shape is pinned in
|
|
1178
|
+
test_the_first_credential_pair_is_the_only_pair; a camera that answered
|
|
1179
|
+
per-media credentials would need this split before it could be nominated on
|
|
1180
|
+
both sockets.
|
|
1177
1181
|
|
|
1178
1182
|
One parser, used by both the nomination and the window's exit predicate.
|
|
1179
1183
|
The exit means "the nomination this is about to reach will succeed", and it
|
|
@@ -1210,6 +1214,19 @@ def _answer_sdp_can_nominate(sdp) -> bool:
|
|
|
1210
1214
|
return bool(ufrag and pwd and cands)
|
|
1211
1215
|
|
|
1212
1216
|
|
|
1217
|
+
def _answer_ready_for_this_open(stamp, open_started_at) -> bool:
|
|
1218
|
+
"""Whether the answer-ready marker was set by the open now running.
|
|
1219
|
+
|
|
1220
|
+
The marker is per-camera state driving a per-open decision. It is cleared
|
|
1221
|
+
at the start of every open, which covers the ordinary sequential case, but
|
|
1222
|
+
two opens overlapping on one camera object would otherwise let one leave its
|
|
1223
|
+
STUN window on the other's answer - or lose its own stamp to the other's
|
|
1224
|
+
reset. Comparing against this open's own start makes a stale stamp inert
|
|
1225
|
+
instead.
|
|
1226
|
+
"""
|
|
1227
|
+
return stamp is not None and stamp >= open_started_at
|
|
1228
|
+
|
|
1229
|
+
|
|
1213
1230
|
def _stun_window_answer_exit_due(*, stun_seen: bool,
|
|
1214
1231
|
answer_ready: bool) -> bool:
|
|
1215
1232
|
"""Whether the STUN responder window has nothing left to wait for.
|
|
@@ -1798,11 +1815,23 @@ async def _sdes_await_answer_or_terminal(
|
|
|
1798
1815
|
# end of the STUN window rather than passing unnoticed, so an operator on a
|
|
1799
1816
|
# fleet whose band is not empty gets a line naming the knob.
|
|
1800
1817
|
_SDES_ECHO_WAIT_S = 0.25
|
|
1818
|
+
#: What the wait was before it was measured, and what a device that has actually
|
|
1819
|
+
#: produced an echo goes back to.
|
|
1820
|
+
_SDES_ECHO_WAIT_LEGACY_S = 2.0
|
|
1801
1821
|
|
|
1802
1822
|
|
|
1803
|
-
def _sdes_echo_wait_timeout(skip_liveplay: bool
|
|
1823
|
+
def _sdes_echo_wait_timeout(skip_liveplay: bool,
|
|
1824
|
+
echo_seen: bool = False) -> float:
|
|
1804
1825
|
"""Seconds to block on the camera's webrtcReq echo. Never negative.
|
|
1805
1826
|
|
|
1827
|
+
``echo_seen`` is this device's own history: once an echo has been observed
|
|
1828
|
+
from it - during the wait or, more usefully, after the wait had already
|
|
1829
|
+
expired - every later open on that device waits the full
|
|
1830
|
+
``_SDES_ECHO_WAIT_LEGACY_S`` again. That is what keeps the shortened wait
|
|
1831
|
+
honest on a fleet the 17/17 measurement never saw: such a camera pays the
|
|
1832
|
+
miss once, says so in the log, and is never shortened again. An explicit
|
|
1833
|
+
env value still wins over both.
|
|
1834
|
+
|
|
1806
1835
|
The echo only ever arrives for role-reversal models (e.g. A001064), which
|
|
1807
1836
|
build a webrtcResp from it and run with ``skip_liveplay`` False (they are
|
|
1808
1837
|
hard-excluded from sdes_fast_liveplay). For A001513-class cameras
|
|
@@ -1823,7 +1852,9 @@ def _sdes_echo_wait_timeout(skip_liveplay: bool) -> float:
|
|
|
1823
1852
|
val = None
|
|
1824
1853
|
if val is not None and val >= 0.0:
|
|
1825
1854
|
return 0.0 if skip_liveplay else val
|
|
1826
|
-
|
|
1855
|
+
if skip_liveplay:
|
|
1856
|
+
return 0.0
|
|
1857
|
+
return _SDES_ECHO_WAIT_LEGACY_S if echo_seen else _SDES_ECHO_WAIT_S
|
|
1827
1858
|
|
|
1828
1859
|
|
|
1829
1860
|
# ffmpeg returns AVERROR(EPIPE) = -32 when its output consumer disappears, and a
|
|
@@ -2315,6 +2346,10 @@ class _SdesOpenMixin:
|
|
|
2315
2346
|
SDP answer, writes it to a temp file, and launches ffmpeg to receive and
|
|
2316
2347
|
record the SRTP stream.
|
|
2317
2348
|
"""
|
|
2349
|
+
# When this open began. The answer-ready marker lives on the camera
|
|
2350
|
+
# object, not the open, so a marker older than this belongs to a
|
|
2351
|
+
# previous open and must not fire this one's early exit.
|
|
2352
|
+
_open_started_at = time.monotonic()
|
|
2318
2353
|
from .client import CameraMixin, _build_sdes_serve_cmd, _ffmpeg_path, _resolve_serve_input_timeout_s, _spawn_bg # lazy: break client<->sdes_open cycle
|
|
2319
2354
|
import base64
|
|
2320
2355
|
import subprocess
|
|
@@ -3269,7 +3304,8 @@ class _SdesOpenMixin:
|
|
|
3269
3304
|
# Only role-reversal models (A001064, _skip_lp False) echo our webrtcReq
|
|
3270
3305
|
# and need the webrtcResp built below; for A001513-class (_skip_lp True,
|
|
3271
3306
|
# default) the echo never arrives, so don't block ~2s on it.
|
|
3272
|
-
_echo_wait_s = _sdes_echo_wait_timeout(
|
|
3307
|
+
_echo_wait_s = _sdes_echo_wait_timeout(
|
|
3308
|
+
_skip_lp, echo_seen=bool(getattr(self, "_sdes_echo_seen", False)))
|
|
3273
3309
|
_echo_wait_t0 = time.monotonic()
|
|
3274
3310
|
try:
|
|
3275
3311
|
await _asyncio.wait_for(
|
|
@@ -3277,6 +3313,7 @@ class _SdesOpenMixin:
|
|
|
3277
3313
|
timeout=_echo_wait_s,
|
|
3278
3314
|
)
|
|
3279
3315
|
_cam_echo_received = True
|
|
3316
|
+
self._sdes_echo_seen = True
|
|
3280
3317
|
_status("camera webrtcReq echo received - building webrtcResp")
|
|
3281
3318
|
# Seed _sdes_turn_entries from the echo's IceServerList if the HTTP
|
|
3282
3319
|
# ice_config fetch returned nothing (empty list). The echo carries
|
|
@@ -3703,8 +3740,9 @@ class _SdesOpenMixin:
|
|
|
3703
3740
|
# still probing hard, so no idle threshold would ever be crossed.
|
|
3704
3741
|
if _stun_window_answer_exit_due(
|
|
3705
3742
|
stun_seen=_stun_seen,
|
|
3706
|
-
answer_ready=
|
|
3707
|
-
self, "_camera_answer_ice_ready_ts", None)
|
|
3743
|
+
answer_ready=_answer_ready_for_this_open(
|
|
3744
|
+
getattr(self, "_camera_answer_ice_ready_ts", None),
|
|
3745
|
+
_open_started_at)):
|
|
3708
3746
|
_status(
|
|
3709
3747
|
"STUN window: leaving early after %.2fs - answer ICE"
|
|
3710
3748
|
" credentials in hand and camera probing; nominating now"
|
|
@@ -3866,6 +3904,30 @@ class _SdesOpenMixin:
|
|
|
3866
3904
|
break # inner per-socket loop
|
|
3867
3905
|
if _srtp_detected:
|
|
3868
3906
|
break # outer while loop
|
|
3907
|
+
# An echo that missed the shortened wait but turned up anyway. Recorded,
|
|
3908
|
+
# not acted on: the webrtcResp this would have built has to be sent
|
|
3909
|
+
# BEFORE the relay allocation and the ICE window, so building it now
|
|
3910
|
+
# would be a reordering with no camera to measure it against. What it
|
|
3911
|
+
# does do is make the NEXT open on this device wait the full legacy
|
|
3912
|
+
# window - so a fleet whose echo band is not empty pays the miss once
|
|
3913
|
+
# and says so, instead of losing the branch silently forever.
|
|
3914
|
+
if (not _cam_echo_received and _echo_fut is not None
|
|
3915
|
+
and not getattr(self, "_sdes_echo_seen", False)):
|
|
3916
|
+
try:
|
|
3917
|
+
_late_echo = _echo_fut.done() and not _echo_fut.cancelled()
|
|
3918
|
+
except Exception:
|
|
3919
|
+
_late_echo = False
|
|
3920
|
+
if _late_echo:
|
|
3921
|
+
self._sdes_echo_seen = True
|
|
3922
|
+
_LOGGER.warning(
|
|
3923
|
+
"camera %s: the webrtcReq echo arrived %.2fs after a %.2fs"
|
|
3924
|
+
" wait expired, so no webrtcResp was built for this open."
|
|
3925
|
+
" Later opens on this camera will wait %.1fs again. Set"
|
|
3926
|
+
" AIDOT_SDES_ECHO_WAIT_S=%.1f to make that permanent.",
|
|
3927
|
+
getattr(self, "device_id", "?"),
|
|
3928
|
+
time.monotonic() - _echo_wait_t0, _echo_wait_s,
|
|
3929
|
+
_SDES_ECHO_WAIT_LEGACY_S, _SDES_ECHO_WAIT_LEGACY_S,
|
|
3930
|
+
)
|
|
3869
3931
|
for _rsock in (_audio_sock, _video_sock):
|
|
3870
3932
|
try:
|
|
3871
3933
|
_rsock.setblocking(True)
|
|
@@ -3952,8 +4014,9 @@ class _SdesOpenMixin:
|
|
|
3952
4014
|
# window was fixed for, in the window the fix had not reached.
|
|
3953
4015
|
if _stun_window_answer_exit_due(
|
|
3954
4016
|
stun_seen=_stun_seen,
|
|
3955
|
-
answer_ready=
|
|
3956
|
-
self, "_camera_answer_ice_ready_ts", None)
|
|
4017
|
+
answer_ready=_answer_ready_for_this_open(
|
|
4018
|
+
getattr(self, "_camera_answer_ice_ready_ts", None),
|
|
4019
|
+
_open_started_at)):
|
|
3957
4020
|
_status(
|
|
3958
4021
|
"STUN window (retry %d): leaving early after %.2fs -"
|
|
3959
4022
|
" answer ICE credentials in hand and camera probing"
|
{python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
@@ -76,12 +76,12 @@ def _answer_is_from_the_camera(*, src_addr, user_id) -> bool:
|
|
|
76
76
|
would set the answer-is-ready marker and the STUN window would leave having
|
|
77
77
|
heard nothing from the camera at all.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
It gates delivery as well as the marker. ``answer_fut`` is where the
|
|
80
|
+
nomination reads its SDP, so an echo that resolves it makes the nomination
|
|
81
|
+
address our own host and srflx candidates with our own credentials - a
|
|
82
|
+
session that can never be nominated, whether the window leaves early or
|
|
83
|
+
late. Nothing downstream wants our own SDP under any circumstance: the
|
|
84
|
+
SDES path built that answer itself and already holds every value in it.
|
|
85
85
|
|
|
86
86
|
``srcAddr`` carries the account's prefix convention - ``2.`` device,
|
|
87
87
|
``0.`` app, ``9.`` server - and our own messages go out as ``0.{userId}``,
|
|
@@ -94,12 +94,19 @@ def _answer_is_from_the_camera(*, src_addr, user_id) -> bool:
|
|
|
94
94
|
return not str(src_addr).startswith(f"0.{user_id}")
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer
|
|
97
|
+
def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer,
|
|
98
|
+
from_camera: bool = True) -> None:
|
|
98
99
|
"""Route a webrtcResp answer to the first-answer or second-answer future.
|
|
99
100
|
|
|
100
|
-
Called from the MQTT (non-loop) thread. The first accepted answer
|
|
101
|
-
``answer_fut``; a later one
|
|
102
|
-
|
|
101
|
+
Called from the MQTT (non-loop) thread. The first accepted answer from the
|
|
102
|
+
CAMERA resolves ``answer_fut``; a later one resolves ``second_answer_fut``,
|
|
103
|
+
which the late-ICE-credential recovery reads.
|
|
104
|
+
|
|
105
|
+
``from_camera`` False means the broker echoed our own webrtcResp back at us.
|
|
106
|
+
It is dropped rather than routed: it carries our SDP, so resolving
|
|
107
|
+
``answer_fut`` with it makes the nomination address our own candidates, and
|
|
108
|
+
resolving ``second_answer_fut`` feeds the same values to the late-credential
|
|
109
|
+
recovery. See :func:`_answer_is_from_the_camera`.
|
|
103
110
|
|
|
104
111
|
One hop, for the reason in ``_resolve_future_threadsafe``: two answers
|
|
105
112
|
drained inside one loop stall would otherwise both take the first branch,
|
|
@@ -108,6 +115,13 @@ def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer) -> None:
|
|
|
108
115
|
about ``answer_fut`` is true.
|
|
109
116
|
"""
|
|
110
117
|
def _deliver() -> None:
|
|
118
|
+
if not from_camera:
|
|
119
|
+
_LOGGER.debug(
|
|
120
|
+
"SDES: dropping the broker echo of our own webrtcResp"
|
|
121
|
+
" (len=%d) - it carries our SDP, not the camera's",
|
|
122
|
+
len(answer.get("sdp", "")),
|
|
123
|
+
)
|
|
124
|
+
return
|
|
111
125
|
if not answer_fut.done():
|
|
112
126
|
answer_fut.set_result(answer)
|
|
113
127
|
return
|
|
@@ -1032,15 +1046,17 @@ class _WebRTCOpenMixin:
|
|
|
1032
1046
|
)
|
|
1033
1047
|
)
|
|
1034
1048
|
return
|
|
1049
|
+
_from_cam = _answer_is_from_the_camera(
|
|
1050
|
+
src_addr=(inner.get("srcAddr")
|
|
1051
|
+
or msg.get("srcAddr") or ""),
|
|
1052
|
+
user_id=user_id)
|
|
1035
1053
|
if (self._camera_answer_ice_ready_ts is None
|
|
1036
|
-
and
|
|
1037
|
-
src_addr=(inner.get("srcAddr")
|
|
1038
|
-
or msg.get("srcAddr") or ""),
|
|
1039
|
-
user_id=user_id)
|
|
1054
|
+
and _from_cam
|
|
1040
1055
|
and _answer_sdp_can_nominate(answer.get("sdp", ""))):
|
|
1041
1056
|
self._camera_answer_ice_ready_ts = time.monotonic()
|
|
1042
1057
|
_deliver_webrtc_answer(
|
|
1043
|
-
loop, answer_fut, second_answer_fut, answer
|
|
1058
|
+
loop, answer_fut, second_answer_fut, answer,
|
|
1059
|
+
from_camera=_from_cam,
|
|
1044
1060
|
)
|
|
1045
1061
|
elif method == "iceCandidateReq":
|
|
1046
1062
|
resp_pid = inner.get("peerid")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0rc9"
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc9
|
|
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
|
|
@@ -572,7 +572,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
572
572
|
| `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
|
|
573
573
|
| `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
|
|
574
574
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
575
|
-
| `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway.
|
|
575
|
+
| `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway. Measured on an A001064: time to first media 4195 ms -> 2489 ms. **This value is the wait for a camera never seen to echo.** An echo observed at any point, including after the wait has already expired, is remembered for that camera and every later open on it waits `2.0` s again - so a fleet whose echo band is not empty pays the miss once, says so in the log, and is never shortened again. Setting this explicitly overrides both. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. | `0.25` (`2.0` once an echo has been seen) |
|
|
576
576
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
577
577
|
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
578
578
|
| `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
|
|
@@ -17,8 +17,12 @@ import inspect
|
|
|
17
17
|
|
|
18
18
|
import pytest
|
|
19
19
|
|
|
20
|
-
from aidot_cameras.camera.webrtc_open import
|
|
20
|
+
from aidot_cameras.camera.webrtc_open import (
|
|
21
|
+
_answer_is_from_the_camera,
|
|
22
|
+
_deliver_webrtc_answer,
|
|
23
|
+
)
|
|
21
24
|
from aidot_cameras.camera.sdes_open import (
|
|
25
|
+
_answer_ready_for_this_open,
|
|
22
26
|
_answer_sdp_can_nominate,
|
|
23
27
|
_parse_answer_ice,
|
|
24
28
|
_stun_window_answer_exit_due,
|
|
@@ -279,3 +283,122 @@ def test_the_candidate_parse_accepts_a_line_that_ends_at_typ():
|
|
|
279
283
|
_u, _p, cands, host = _parse_answer_ice(sdp)
|
|
280
284
|
assert cands == [("192.168.0.124", 35488)]
|
|
281
285
|
assert host == ()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# --------------------------------------------------------------------------- #
|
|
289
|
+
# Our own echo must not reach the future the nomination reads
|
|
290
|
+
# --------------------------------------------------------------------------- #
|
|
291
|
+
|
|
292
|
+
def test_our_own_echo_is_dropped_rather_than_routed():
|
|
293
|
+
"""Gating only the readiness marker left the failure it was written to
|
|
294
|
+
prevent fully reachable: `answer_fut` is where the nomination reads its SDP,
|
|
295
|
+
so an echo that resolves it makes USE-CANDIDATE address our own host and
|
|
296
|
+
srflx candidates with our own credentials. Routing it to second_answer_fut
|
|
297
|
+
instead just feeds the same values to the late-credential recovery."""
|
|
298
|
+
import asyncio
|
|
299
|
+
|
|
300
|
+
async def _run():
|
|
301
|
+
loop = asyncio.get_running_loop()
|
|
302
|
+
first, second = loop.create_future(), loop.create_future()
|
|
303
|
+
_deliver_webrtc_answer(loop, first, second,
|
|
304
|
+
{"sdp": _OUR_ANSWER}, from_camera=False)
|
|
305
|
+
await asyncio.sleep(0)
|
|
306
|
+
assert not first.done(), "our own SDP must not resolve answer_fut"
|
|
307
|
+
assert not second.done(), "nor the future the late recovery reads"
|
|
308
|
+
|
|
309
|
+
_deliver_webrtc_answer(loop, first, second,
|
|
310
|
+
{"sdp": _ANSWER}, from_camera=True)
|
|
311
|
+
await asyncio.sleep(0)
|
|
312
|
+
assert first.done() and first.result()["sdp"] == _ANSWER
|
|
313
|
+
|
|
314
|
+
_deliver_webrtc_answer(loop, first, second,
|
|
315
|
+
{"sdp": _ANSWER}, from_camera=True)
|
|
316
|
+
await asyncio.sleep(0)
|
|
317
|
+
assert second.done(), "a later camera answer still reaches the recovery"
|
|
318
|
+
|
|
319
|
+
asyncio.run(_run())
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_delivery_defaults_to_treating_an_answer_as_the_cameras():
|
|
323
|
+
"""Every other caller predates the flag; none of them must start dropping
|
|
324
|
+
answers because a keyword was added."""
|
|
325
|
+
import inspect
|
|
326
|
+
|
|
327
|
+
sig = inspect.signature(_deliver_webrtc_answer)
|
|
328
|
+
assert sig.parameters["from_camera"].default is True
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# --------------------------------------------------------------------------- #
|
|
332
|
+
# The marker belongs to one open
|
|
333
|
+
# --------------------------------------------------------------------------- #
|
|
334
|
+
|
|
335
|
+
def test_a_marker_from_this_open_arms_the_exit():
|
|
336
|
+
assert _answer_ready_for_this_open(100.0, 99.0) is True
|
|
337
|
+
assert _answer_ready_for_this_open(99.0, 99.0) is True
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_a_marker_left_by_an_earlier_open_is_inert():
|
|
341
|
+
"""The marker is per-camera state driving a per-open decision. The per-open
|
|
342
|
+
reset covers the sequential case; two opens overlapping on one camera object
|
|
343
|
+
would otherwise let one leave its window on the other's answer."""
|
|
344
|
+
assert _answer_ready_for_this_open(98.9, 99.0) is False
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_no_marker_at_all_is_not_ready():
|
|
348
|
+
assert _answer_ready_for_this_open(None, 99.0) is False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_the_window_checks_the_marker_belongs_to_this_open():
|
|
352
|
+
src = _open_source()
|
|
353
|
+
assert "_answer_ready_for_this_open" in src
|
|
354
|
+
assert src.count("_answer_ready_for_this_open") >= 2, (
|
|
355
|
+
"both STUN windows must use it, not just the first")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# --------------------------------------------------------------------------- #
|
|
359
|
+
# One credential pair, measured rather than assumed
|
|
360
|
+
# --------------------------------------------------------------------------- #
|
|
361
|
+
#
|
|
362
|
+
# The nomination sends USE-CANDIDATE on BOTH sockets with the single ufrag/pwd
|
|
363
|
+
# this parser returns, computing the video socket's MESSAGE-INTEGRITY with what
|
|
364
|
+
# the parser found first. That is only correct if the camera answers one pair
|
|
365
|
+
# for the whole session, which nothing here proved.
|
|
366
|
+
#
|
|
367
|
+
# Measured 2026-09-04 by logging every a=ice-ufrag / a=ice-pwd line of the
|
|
368
|
+
# answer, per m-section, across four cold opens on two models (A001064 mains,
|
|
369
|
+
# A001513 battery): every answer carried ONE pair, repeated identically in the
|
|
370
|
+
# audio, video and application sections. Values below are synthesised - the real
|
|
371
|
+
# ones are per-session credentials and do not belong in a repo - but the shape
|
|
372
|
+
# is the shape that was captured.
|
|
373
|
+
|
|
374
|
+
_ONE_PAIR_ANSWER = "\r\n".join([
|
|
375
|
+
"v=0",
|
|
376
|
+
"m=video 9 RTP/SAVP 96",
|
|
377
|
+
"a=ice-ufrag:9z7F",
|
|
378
|
+
"a=ice-pwd:mDe5uxBe1msnVNMlu8BwzQPy",
|
|
379
|
+
"a=candidate:1 1 udp 2130706431 192.168.0.124 33912 typ host",
|
|
380
|
+
"m=audio 9 RTP/SAVP 8",
|
|
381
|
+
"a=ice-ufrag:9z7F",
|
|
382
|
+
"a=ice-pwd:mDe5uxBe1msnVNMlu8BwzQPy",
|
|
383
|
+
"m=application 9 SCTP webrtc-datachannel",
|
|
384
|
+
"a=ice-ufrag:9z7F",
|
|
385
|
+
"a=ice-pwd:mDe5uxBe1msnVNMlu8BwzQPy",
|
|
386
|
+
])
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_the_first_credential_pair_is_the_only_pair():
|
|
390
|
+
"""So nominating both sockets with it is right, not merely lucky."""
|
|
391
|
+
ufrag, pwd, cands, _host = _parse_answer_ice(_ONE_PAIR_ANSWER)
|
|
392
|
+
assert ufrag == "9z7F"
|
|
393
|
+
assert pwd == "mDe5uxBe1msnVNMlu8BwzQPy"
|
|
394
|
+
assert cands == [("192.168.0.124", 33912)]
|
|
395
|
+
|
|
396
|
+
pairs = {
|
|
397
|
+
(ln[len("a=ice-ufrag:"):].strip() if ln.startswith("a=ice-ufrag:")
|
|
398
|
+
else ln[len("a=ice-pwd:"):].strip())
|
|
399
|
+
for ln in _ONE_PAIR_ANSWER.splitlines()
|
|
400
|
+
if ln.startswith(("a=ice-ufrag:", "a=ice-pwd:"))
|
|
401
|
+
}
|
|
402
|
+
assert pairs == {ufrag, pwd}, (
|
|
403
|
+
"this answer carries more than one credential pair, so the single pair"
|
|
404
|
+
" the nomination uses on both sockets is no longer the whole story")
|
|
@@ -94,3 +94,61 @@ def test_the_timeout_is_reported_rather_than_swallowed():
|
|
|
94
94
|
|
|
95
95
|
src = inspect.getsource(CameraMixin._open_sdes_stream_impl)
|
|
96
96
|
assert "webrtcReq-echo elapsed" in src
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --------------------------------------------------------------------------- #
|
|
100
|
+
# A fleet whose echo band is not empty
|
|
101
|
+
# --------------------------------------------------------------------------- #
|
|
102
|
+
#
|
|
103
|
+
# The 17/17 measurement is one deployment. An echo landing between the new wait
|
|
104
|
+
# and the old 2.0 s would miss it, and that flag is a branch selector: it
|
|
105
|
+
# decides whether the webrtcResp is built, whether the ICE window runs 20 s or
|
|
106
|
+
# 2.5 s, and whether the quickConn reconnect retry is armed. So the shortened
|
|
107
|
+
# wait is not unconditional - it is the wait for a device that has never been
|
|
108
|
+
# seen to echo.
|
|
109
|
+
|
|
110
|
+
def test_a_device_that_has_echoed_goes_back_to_the_full_wait():
|
|
111
|
+
assert _sdes_echo_wait_timeout(False, echo_seen=True) == 2.0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_a_device_that_has_never_echoed_keeps_the_short_wait():
|
|
115
|
+
assert _sdes_echo_wait_timeout(False, echo_seen=False) == 0.25
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_history_does_not_resurrect_the_wait_for_cameras_that_never_take_it():
|
|
119
|
+
"""A001513-class cameras wait 0.0 s and are not part of this at all."""
|
|
120
|
+
assert _sdes_echo_wait_timeout(True, echo_seen=True) == 0.0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_an_explicit_override_still_wins_over_the_history(monkeypatch):
|
|
124
|
+
monkeypatch.setenv("AIDOT_SDES_ECHO_WAIT_S", "0.5")
|
|
125
|
+
assert _sdes_echo_wait_timeout(False, echo_seen=True) == 0.5
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_the_history_defaults_off_so_the_first_open_is_still_fast():
|
|
129
|
+
"""Callers that predate the flag must not start paying 2.0 s."""
|
|
130
|
+
assert _sdes_echo_wait_timeout(False) == 0.25
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_a_late_echo_is_recorded_so_the_next_open_waits_for_it():
|
|
134
|
+
"""Recorded, not acted on: the webrtcResp has to be sent before the relay
|
|
135
|
+
allocation and the ICE window, so building it after the window would be a
|
|
136
|
+
reordering with no camera to measure it against. Making the NEXT open wait
|
|
137
|
+
is the part that can be done safely."""
|
|
138
|
+
import inspect
|
|
139
|
+
|
|
140
|
+
from aidot_cameras.camera.client import CameraMixin
|
|
141
|
+
|
|
142
|
+
src = inspect.getsource(CameraMixin._open_sdes_stream_impl)
|
|
143
|
+
assert "_sdes_echo_seen" in src
|
|
144
|
+
assert "AIDOT_SDES_ECHO_WAIT_S=%.1f" in src, (
|
|
145
|
+
"the line must name the knob that makes it permanent")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_the_wait_reads_the_devices_history():
|
|
149
|
+
import inspect
|
|
150
|
+
|
|
151
|
+
from aidot_cameras.camera.client import CameraMixin
|
|
152
|
+
|
|
153
|
+
src = inspect.getsource(CameraMixin._open_sdes_stream_impl)
|
|
154
|
+
assert "echo_seen=" in src
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/LICENSE
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/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
|