python-aidot-cameras 1.0.0rc7__tar.gz → 1.0.0rc8__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.0rc7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc8}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/README.md +1 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sdes_open.py +284 -102
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/webrtc_open.py +45 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/SOURCES.txt +2 -1
- python_aidot_cameras-1.0.0rc8/tests/test_nominates_as_soon_as_the_answer_lands.py +281 -0
- python_aidot_cameras-1.0.0rc8/tests/test_sdes_echo_wait_is_not_dead_time.py +96 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_turn_create_permission.py +17 -11
- python_aidot_cameras-1.0.0rc7/tests/test_sdes_echo_wait_timeout.py +0 -20
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_wake_gate_app_parity.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/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.0rc8
|
|
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,6 +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. Shortened rather than removed, so a broker that does echo still short-circuits it and gets its `webrtcResp`. Measured on an A001064: time to first media 4195 ms -> 2489 ms. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. **Caveat:** the 17/17 evidence is one fleet, and an echo landing between 0.25 s and 2.0 s would now miss the wait - that flag also selects whether the `webrtcResp` is built, whether the ICE window runs 20 s or 2.5 s, and whether the quickConn reconnect retry is armed. A late echo is logged, so set this to `2.0` if you see one. | `0.25` |
|
|
575
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) |
|
|
576
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`) |
|
|
577
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,6 +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. Shortened rather than removed, so a broker that does echo still short-circuits it and gets its `webrtcResp`. Measured on an A001064: time to first media 4195 ms -> 2489 ms. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. **Caveat:** the 17/17 evidence is one fleet, and an echo landing between 0.25 s and 2.0 s would now miss the wait - that flag also selects whether the `webrtcResp` is built, whether the ICE window runs 20 s or 2.5 s, and whether the quickConn reconnect retry is armed. A late echo is logged, so set this to `2.0` if you see one. | `0.25` |
|
|
535
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) |
|
|
536
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`) |
|
|
537
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.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import random
|
|
15
|
+
import re
|
|
15
16
|
import time
|
|
16
17
|
from typing import Callable, Optional # noqa: F401 - method annotations
|
|
17
18
|
|
|
@@ -1110,6 +1111,133 @@ _PRE_LAUNCH_ANSWER_WAIT_S = 8.0
|
|
|
1110
1111
|
_AUDIO_PT_GRACE_S = 1.0
|
|
1111
1112
|
|
|
1112
1113
|
|
|
1114
|
+
# --------------------------------------------------------------------------- #
|
|
1115
|
+
# Reading the camera's answer, and leaving the STUN window once it is readable
|
|
1116
|
+
# --------------------------------------------------------------------------- #
|
|
1117
|
+
#
|
|
1118
|
+
# The ICE responder window below runs synchronously and blocks the event loop,
|
|
1119
|
+
# so it was written on the assumption stated above the answer harvest: that it
|
|
1120
|
+
# "closes ~2.4 s before the camera answers". Cutting the livePlayReq echo wait
|
|
1121
|
+
# from 5.0 s to 0.25 s inverted that. Measured on a cold A001064 open:
|
|
1122
|
+
#
|
|
1123
|
+
# +0.45 s webrtcReq
|
|
1124
|
+
# +0.72 s the camera's webrtcResp answer <- credentials in hand
|
|
1125
|
+
# +5.05 s window closes on 1.5 s of ICE silence
|
|
1126
|
+
# +5.06 s USE-CANDIDATE
|
|
1127
|
+
# +6.66 s first SRTP
|
|
1128
|
+
#
|
|
1129
|
+
# 4.3 s of a 6.7 s connect spent holding an answer nothing was looking at.
|
|
1130
|
+
# Leaving early costs nothing: the bridge thread takes over responding to the
|
|
1131
|
+
# camera's binding requests the moment the window closes, and it is the bridge -
|
|
1132
|
+
# not the window - that learns peer-reflexive candidates and re-nominates every
|
|
1133
|
+
# 2.5 s, so the relay-only and off-subnet recovery paths are untouched.
|
|
1134
|
+
# Two of the five parses this replaced ended at a bare ``typ`` and captured no
|
|
1135
|
+
# type token, and one accepted a line with no ``a=`` prefix (trickled candidates
|
|
1136
|
+
# arrive bare). Both shapes are accepted so the collapse cannot reject a line
|
|
1137
|
+
# one of the old copies would have taken.
|
|
1138
|
+
#
|
|
1139
|
+
# The type token is optional but still anchored: ``typ ?(\w*)`` also matched
|
|
1140
|
+
# ``typo host`` and ``typhoon``, which every parse it replaced rejected, and a
|
|
1141
|
+
# corrupt candidate line reaching the nomination gets a TURN permission and a
|
|
1142
|
+
# USE-CANDIDATE probe. ``(?!\w)`` keeps the widening to the one direction the
|
|
1143
|
+
# collapse needs.
|
|
1144
|
+
_ICE_CAND_RE = re.compile(
|
|
1145
|
+
r"(?:a=)?candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ(?: (\w+))?(?!\w)"
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def _parse_candidate_line(line):
|
|
1150
|
+
"""One ``a=candidate:`` line as ``(ip, port, typ)``, or None.
|
|
1151
|
+
|
|
1152
|
+
Shared with the trickle path, which reads candidates one at a time off
|
|
1153
|
+
``iceCandidateReq`` rather than out of an SDP, and which kept its own copy
|
|
1154
|
+
of this pattern until the two could disagree about the same line.
|
|
1155
|
+
"""
|
|
1156
|
+
m = _ICE_CAND_RE.match((line or "").strip())
|
|
1157
|
+
if not m:
|
|
1158
|
+
return None
|
|
1159
|
+
return m.group(1), int(m.group(2)), (m.group(3) or "")
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _parse_answer_ice(sdp):
|
|
1163
|
+
"""Pull ICE credentials and UDP candidates out of a camera's answer SDP.
|
|
1164
|
+
|
|
1165
|
+
Returns ``(ufrag, pwd, candidates, host)``: the first ice-ufrag and
|
|
1166
|
+
ice-pwd in the SDP, every udp candidate as an ``(ip, port)`` tuple in the
|
|
1167
|
+
order offered, and the first ``typ host`` candidate - which is the one SCTP
|
|
1168
|
+
is addressed to, and is not necessarily the first candidate listed.
|
|
1169
|
+
|
|
1170
|
+
Taking the FIRST credential pair is an observation, not a contract: 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. That works
|
|
1173
|
+
on the whole reference fleet, so those cameras evidently answer with one
|
|
1174
|
+
pair - but no captured answer in this repo proves it, and a camera that
|
|
1175
|
+
answered per-media credentials would need this split before it could be
|
|
1176
|
+
nominated on both.
|
|
1177
|
+
|
|
1178
|
+
One parser, used by both the nomination and the window's exit predicate.
|
|
1179
|
+
The exit means "the nomination this is about to reach will succeed", and it
|
|
1180
|
+
only means that while both sides read the same SDP the same way.
|
|
1181
|
+
"""
|
|
1182
|
+
ufrag = ""
|
|
1183
|
+
pwd = ""
|
|
1184
|
+
cands: list = []
|
|
1185
|
+
host: tuple = ()
|
|
1186
|
+
for line in (sdp or "").splitlines():
|
|
1187
|
+
if line.startswith("a=ice-ufrag:") and not ufrag:
|
|
1188
|
+
ufrag = line[len("a=ice-ufrag:"):].strip()
|
|
1189
|
+
elif line.startswith("a=ice-pwd:") and not pwd:
|
|
1190
|
+
pwd = line[len("a=ice-pwd:"):].strip()
|
|
1191
|
+
elif line.startswith("a=candidate:"):
|
|
1192
|
+
parsed = _parse_candidate_line(line)
|
|
1193
|
+
if parsed:
|
|
1194
|
+
addr = (parsed[0], parsed[1])
|
|
1195
|
+
cands.append(addr)
|
|
1196
|
+
if parsed[2] == "host" and not host:
|
|
1197
|
+
host = addr
|
|
1198
|
+
return ufrag, pwd, cands, host
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _answer_sdp_can_nominate(sdp) -> bool:
|
|
1202
|
+
"""Whether this answer carries everything USE-CANDIDATE needs.
|
|
1203
|
+
|
|
1204
|
+
Deliberately the whole precondition and not a proxy for it: an answer with
|
|
1205
|
+
credentials but no candidate, or candidates but no credentials, leaves the
|
|
1206
|
+
nomination with nothing to address, and leaving the window on it would
|
|
1207
|
+
trade 4.3 s of responding for a session that never nominates at all.
|
|
1208
|
+
"""
|
|
1209
|
+
ufrag, pwd, cands, _host = _parse_answer_ice(sdp)
|
|
1210
|
+
return bool(ufrag and pwd and cands)
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _stun_window_answer_exit_due(*, stun_seen: bool,
|
|
1214
|
+
answer_ready: bool) -> bool:
|
|
1215
|
+
"""Whether the STUN responder window has nothing left to wait for.
|
|
1216
|
+
|
|
1217
|
+
Every branch of the window carries the delay, so this is not scoped to one.
|
|
1218
|
+
Measured on two cold A001064 opens that took different branches:
|
|
1219
|
+
|
|
1220
|
+
* echo received, webrtcResp sent (the 20 s window): answer at +0.72 s,
|
|
1221
|
+
window ran to +5.05 s and left on 1.5 s of ICE silence.
|
|
1222
|
+
* no echo (the 2.5 s window): answer at +0.22 s, window ran the full
|
|
1223
|
+
2.5 s cap and left on the deadline.
|
|
1224
|
+
|
|
1225
|
+
Both held the answer for the whole window. Nothing else in the window is
|
|
1226
|
+
load-bearing by then either: an SRTP packet already breaks out of it, a
|
|
1227
|
+
camera that never probes already leaves on ``_pre_stun_idle``, and the
|
|
1228
|
+
bridge thread picks the responder role back up ~30 ms later.
|
|
1229
|
+
|
|
1230
|
+
``stun_seen`` is the camera's own evidence that it is running ICE: a probe
|
|
1231
|
+
this host actually answered, which is why it is set where the reply is sent
|
|
1232
|
+
and not where the request is parsed - a self-loop Data Indication is a
|
|
1233
|
+
binding request too, and it is our own address coming back through the
|
|
1234
|
+
relay. Without such a probe there is nothing to nominate a pair *with*, and
|
|
1235
|
+
a relay-only camera that answers early and probes late keeps the behaviour
|
|
1236
|
+
it has today.
|
|
1237
|
+
"""
|
|
1238
|
+
return bool(stun_seen and answer_ready)
|
|
1239
|
+
|
|
1240
|
+
|
|
1113
1241
|
def video_pt_from_answer_sdp(sdp_text: str) -> Optional[int]:
|
|
1114
1242
|
"""Which video payload type to narrow to, taken from the camera's answer SDP.
|
|
1115
1243
|
|
|
@@ -1343,13 +1471,23 @@ def _serve_video_pt(observed, answer, pinned) -> "Optional[int]":
|
|
|
1343
1471
|
|
|
1344
1472
|
An observed payload type is fact -- it is what is arriving. A pin is our
|
|
1345
1473
|
own constraint on the offer, so narrowing past it can only produce a stream
|
|
1346
|
-
the camera was never asked to send. The answer is the weakest of the three
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1474
|
+
the camera was never asked to send. The answer is the weakest of the three
|
|
1475
|
+
because it can be absent or stale, not because it lies: trusting it when
|
|
1476
|
+
video had not yet been observed once built an hevc-only SDP that no hevc
|
|
1477
|
+
ever filled -- ffmpeg could not determine dimensions, could not write the
|
|
1478
|
+
RTSP header, and the serve died at startup. That was invisible until the
|
|
1479
|
+
exit reason stopped being flushed out of the stderr tail by Non-monotonic
|
|
1480
|
+
DTS noise.
|
|
1481
|
+
|
|
1482
|
+
This docstring used to say the reference A001064 "ANSWERS 97 (H.265) and
|
|
1483
|
+
SENDS 96 (H.264) in every observed session". That is RETRACTED. Measured on
|
|
1484
|
+
2026-09-04 by reading the first decrypted NAL of 44 cold opens: the answer
|
|
1485
|
+
and the wire agree every time. The camera simply picks a codec per session
|
|
1486
|
+
-- 40 of the 44 negotiated H.264 (payload begins 0x67, an H.264 SPS with
|
|
1487
|
+
profile_idc 0x4D level 0x1F, matching the avc1.4D001F go2rtc reports) and 4
|
|
1488
|
+
negotiated H.265 (0x40/0x42/0x44 -- VPS, SPS, PPS -- then 0x62 FU
|
|
1489
|
+
fragments). Both are served correctly today. The retracted claim reads as a
|
|
1490
|
+
demux bug and very nearly bought a fix for one that does not exist.
|
|
1353
1491
|
|
|
1354
1492
|
Returns None when nothing is known; the caller keeps its own "could not
|
|
1355
1493
|
narrow" path rather than having a payload type invented for it.
|
|
@@ -1637,16 +1775,55 @@ async def _sdes_await_answer_or_terminal(
|
|
|
1637
1775
|
raise TimeoutError()
|
|
1638
1776
|
|
|
1639
1777
|
|
|
1778
|
+
# Seconds a role-reversal camera gets to echo our webrtcReq back.
|
|
1779
|
+
#
|
|
1780
|
+
# This was 2.0 s on the reading that A001064 "needs the resulting webrtcResp".
|
|
1781
|
+
# Measured over 18 hours of logs on 2026-09-03/04: of 61 SDES opens, the 17 that
|
|
1782
|
+
# took this wait timed out 17/17 at a mean of 2.086 s, and neither `camera
|
|
1783
|
+
# webrtcReq echo received` nor `webrtcResp sent (SDES` appears once in the whole
|
|
1784
|
+
# log. The webrtcResp was never built, and all 17 streamed regardless - so the
|
|
1785
|
+
# 2.0 s was 45 percent of a 4.2 s cold connect, spent on nothing.
|
|
1786
|
+
#
|
|
1787
|
+
# Shortened rather than removed, exactly as the livePlayReq echo wait was
|
|
1788
|
+
# (5.0 -> 0.25 s, 169/169 timeouts): the evidence says the wait times out, not
|
|
1789
|
+
# that the branch is wrong, so a camera that does echo promptly still gets its
|
|
1790
|
+
# webrtcResp. Raise AIDOT_SDES_ECHO_WAIT_S to put the old behaviour back
|
|
1791
|
+
# without a release.
|
|
1792
|
+
#
|
|
1793
|
+
# The measurement is one fleet. An echo that lands between this value and the
|
|
1794
|
+
# old 2.0 s would now miss the wait, and _cam_echo_received is a branch
|
|
1795
|
+
# selector, not just a log line: it decides whether the webrtcResp is built,
|
|
1796
|
+
# whether the ICE window runs 20 s or 2.5 s, and whether the quickConn
|
|
1797
|
+
# reconnect retry is armed. A late echo is therefore reported loudly at the
|
|
1798
|
+
# end of the STUN window rather than passing unnoticed, so an operator on a
|
|
1799
|
+
# fleet whose band is not empty gets a line naming the knob.
|
|
1800
|
+
_SDES_ECHO_WAIT_S = 0.25
|
|
1801
|
+
|
|
1802
|
+
|
|
1640
1803
|
def _sdes_echo_wait_timeout(skip_liveplay: bool) -> float:
|
|
1641
|
-
"""Seconds to block on the camera's webrtcReq echo
|
|
1804
|
+
"""Seconds to block on the camera's webrtcReq echo. Never negative.
|
|
1642
1805
|
|
|
1643
1806
|
The echo only ever arrives for role-reversal models (e.g. A001064), which
|
|
1644
|
-
|
|
1645
|
-
hard-excluded from sdes_fast_liveplay) -
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1807
|
+
build a webrtcResp from it and run with ``skip_liveplay`` False (they are
|
|
1808
|
+
hard-excluded from sdes_fast_liveplay). For A001513-class cameras
|
|
1809
|
+
(``skip_liveplay`` True, the default) the echo never arrives at all, so they
|
|
1810
|
+
do not block on it.
|
|
1811
|
+
|
|
1812
|
+
Read per call and clamped, like its sibling
|
|
1813
|
+
:func:`_sdes_liveplay_echo_timeout`: a malformed env value is ignored rather
|
|
1814
|
+
than raising, because parsing it at import time makes one typo on an HA box
|
|
1815
|
+
an ImportError that takes down every camera, and freezing it at import means
|
|
1816
|
+
an operator changing it sees nothing until a full restart.
|
|
1817
|
+
"""
|
|
1818
|
+
raw = _os.environ.get("AIDOT_SDES_ECHO_WAIT_S")
|
|
1819
|
+
if raw is not None:
|
|
1820
|
+
try:
|
|
1821
|
+
val = float(raw)
|
|
1822
|
+
except (TypeError, ValueError):
|
|
1823
|
+
val = None
|
|
1824
|
+
if val is not None and val >= 0.0:
|
|
1825
|
+
return 0.0 if skip_liveplay else val
|
|
1826
|
+
return 0.0 if skip_liveplay else _SDES_ECHO_WAIT_S
|
|
1650
1827
|
|
|
1651
1828
|
|
|
1652
1829
|
# ffmpeg returns AVERROR(EPIPE) = -32 when its output consumer disappears, and a
|
|
@@ -3092,10 +3269,12 @@ class _SdesOpenMixin:
|
|
|
3092
3269
|
# Only role-reversal models (A001064, _skip_lp False) echo our webrtcReq
|
|
3093
3270
|
# and need the webrtcResp built below; for A001513-class (_skip_lp True,
|
|
3094
3271
|
# default) the echo never arrives, so don't block ~2s on it.
|
|
3272
|
+
_echo_wait_s = _sdes_echo_wait_timeout(_skip_lp)
|
|
3273
|
+
_echo_wait_t0 = time.monotonic()
|
|
3095
3274
|
try:
|
|
3096
3275
|
await _asyncio.wait_for(
|
|
3097
3276
|
_asyncio.shield(_echo_fut),
|
|
3098
|
-
timeout=
|
|
3277
|
+
timeout=_echo_wait_s,
|
|
3099
3278
|
)
|
|
3100
3279
|
_cam_echo_received = True
|
|
3101
3280
|
_status("camera webrtcReq echo received - building webrtcResp")
|
|
@@ -3252,7 +3431,17 @@ class _SdesOpenMixin:
|
|
|
3252
3431
|
f" video={_ans_video_ip}:{_ans_video_port})"
|
|
3253
3432
|
)
|
|
3254
3433
|
except TimeoutError:
|
|
3255
|
-
|
|
3434
|
+
# No echo - camera uses a different signalling variant; proceed.
|
|
3435
|
+
# Logged rather than swallowed: `except TimeoutError: pass` is how
|
|
3436
|
+
# 2.0 s of dead time here stayed invisible, and the elapsed line is
|
|
3437
|
+
# what will show it if this wait ever starts being used.
|
|
3438
|
+
_LOGGER.info(
|
|
3439
|
+
"signaling-wait[%s] webrtcReq-echo elapsed=%dms"
|
|
3440
|
+
" (timeout=%.2fs)",
|
|
3441
|
+
getattr(self, "device_id", "?"),
|
|
3442
|
+
int((time.monotonic() - _echo_wait_t0) * 1000),
|
|
3443
|
+
_echo_wait_s,
|
|
3444
|
+
)
|
|
3256
3445
|
|
|
3257
3446
|
# --- Announce our ICE candidates via MQTT (iceCandidateReq) ----------- #
|
|
3258
3447
|
# The iOS app always sends iceCandidateReq after webrtcReq/webrtcResp.
|
|
@@ -3498,12 +3687,31 @@ class _SdesOpenMixin:
|
|
|
3498
3687
|
_pre_stun_idle = 0.5 # exit early if no packet at all (non-ICE camera)
|
|
3499
3688
|
_stun_deadline = time.monotonic() + _stun_max
|
|
3500
3689
|
_last_pkt_t = time.monotonic()
|
|
3690
|
+
_stun_window_t0 = time.monotonic()
|
|
3501
3691
|
for _rsock in (_audio_sock, _video_sock):
|
|
3502
3692
|
try:
|
|
3503
3693
|
_rsock.setblocking(False)
|
|
3504
3694
|
except Exception:
|
|
3505
3695
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_is_self_peer_ip', exc_info=True)
|
|
3506
3696
|
while time.monotonic() < _stun_deadline:
|
|
3697
|
+
# Answer-exit: the only reason to sit here is that the credentials
|
|
3698
|
+
# the nomination needs have not arrived yet. Once they have - and
|
|
3699
|
+
# the camera has proved it is running ICE by probing us - waiting
|
|
3700
|
+
# longer just delays USE-CANDIDATE, and the bridge thread below
|
|
3701
|
+
# picks the responder role straight back up. Checked before the
|
|
3702
|
+
# idle test because the answer usually lands while the camera is
|
|
3703
|
+
# still probing hard, so no idle threshold would ever be crossed.
|
|
3704
|
+
if _stun_window_answer_exit_due(
|
|
3705
|
+
stun_seen=_stun_seen,
|
|
3706
|
+
answer_ready=getattr(
|
|
3707
|
+
self, "_camera_answer_ice_ready_ts", None) is not None):
|
|
3708
|
+
_status(
|
|
3709
|
+
"STUN window: leaving early after %.2fs - answer ICE"
|
|
3710
|
+
" credentials in hand and camera probing; nominating now"
|
|
3711
|
+
" (%d binding request(s) answered)"
|
|
3712
|
+
% (time.monotonic() - _stun_window_t0, _stun_count)
|
|
3713
|
+
)
|
|
3714
|
+
break
|
|
3507
3715
|
# Idle-exit: threshold depends on whether we've seen any STUN yet
|
|
3508
3716
|
idle = time.monotonic() - _last_pkt_t
|
|
3509
3717
|
if _stun_seen:
|
|
@@ -3575,8 +3783,15 @@ class _SdesOpenMixin:
|
|
|
3575
3783
|
# replies) must NOT set _stun_seen or they'd trigger the 1.5s
|
|
3576
3784
|
# idle-exit prematurely, before camera probes arrive.
|
|
3577
3785
|
if _pkt[:2] == b'\x00\x01':
|
|
3578
|
-
# Binding Request - reply with Binding Success Response
|
|
3579
|
-
_stun_seen
|
|
3786
|
+
# Binding Request - reply with Binding Success Response.
|
|
3787
|
+
# _stun_seen is NOT set here. A self-loop Data
|
|
3788
|
+
# Indication - our own address echoed back through the
|
|
3789
|
+
# relay - is a Binding Request too, and the branch below
|
|
3790
|
+
# drops it precisely because it is not the camera.
|
|
3791
|
+
# Setting the flag here made one of those count as "the
|
|
3792
|
+
# camera has started ICE", which is the one thing the
|
|
3793
|
+
# answer-exit is gated on. It is set where a probe is
|
|
3794
|
+
# actually answered instead.
|
|
3580
3795
|
if _turn_peer_ip_sw is None and _src[0] != _hp_host:
|
|
3581
3796
|
_prefer_direct_stun[_sock] = True
|
|
3582
3797
|
_tid = _pkt[8:20]
|
|
@@ -3624,6 +3839,7 @@ class _SdesOpenMixin:
|
|
|
3624
3839
|
+ _STUN_MAGIC + os.urandom(12)
|
|
3625
3840
|
+ _si_body)
|
|
3626
3841
|
_sock.sendto(_send_ind, (_t_host_sw, _t_port_sw))
|
|
3842
|
+
_stun_seen = True
|
|
3627
3843
|
elif _turn_peer_ip_sw and _is_self_peer_ip(
|
|
3628
3844
|
_turn_peer_ip_sw, _turn_peer_port_sw):
|
|
3629
3845
|
# Self-loop Data Indication (peer == our own
|
|
@@ -3640,6 +3856,7 @@ class _SdesOpenMixin:
|
|
|
3640
3856
|
)
|
|
3641
3857
|
else:
|
|
3642
3858
|
_sock.sendto(_resp, _src)
|
|
3859
|
+
_stun_seen = True
|
|
3643
3860
|
_stun_count += 1
|
|
3644
3861
|
except Exception:
|
|
3645
3862
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_is_self_peer_ip', exc_info=True)
|
|
@@ -3729,6 +3946,20 @@ class _SdesOpenMixin:
|
|
|
3729
3946
|
_last_pkt_t = time.monotonic()
|
|
3730
3947
|
_stun_seen = False
|
|
3731
3948
|
while time.monotonic() < _stun_deadline:
|
|
3949
|
+
# Same exit as the first window. A camera that quickConns after
|
|
3950
|
+
# signalling re-runs ICE here, and without this it sat the full
|
|
3951
|
+
# 8 s on credentials it already had - the same delay the first
|
|
3952
|
+
# window was fixed for, in the window the fix had not reached.
|
|
3953
|
+
if _stun_window_answer_exit_due(
|
|
3954
|
+
stun_seen=_stun_seen,
|
|
3955
|
+
answer_ready=getattr(
|
|
3956
|
+
self, "_camera_answer_ice_ready_ts", None) is not None):
|
|
3957
|
+
_status(
|
|
3958
|
+
"STUN window (retry %d): leaving early after %.2fs -"
|
|
3959
|
+
" answer ICE credentials in hand and camera probing"
|
|
3960
|
+
% (_sdes_retries,
|
|
3961
|
+
time.monotonic() - (_stun_deadline - 8.0)))
|
|
3962
|
+
break
|
|
3732
3963
|
if _stun_seen and time.monotonic() - _last_pkt_t > _idle_limit:
|
|
3733
3964
|
break
|
|
3734
3965
|
try:
|
|
@@ -3747,7 +3978,6 @@ class _SdesOpenMixin:
|
|
|
3747
3978
|
and _pkt_r[4:8] == _STUN_MAGIC):
|
|
3748
3979
|
if _pkt_r[:2] != b'\x00\x01':
|
|
3749
3980
|
continue # not a Binding Request; don't trigger idle-exit
|
|
3750
|
-
_stun_seen = True
|
|
3751
3981
|
_tid_r = _pkt_r[8:20]
|
|
3752
3982
|
try:
|
|
3753
3983
|
_resp_r = _build_stun_binding_success_response(
|
|
@@ -3760,6 +3990,7 @@ class _SdesOpenMixin:
|
|
|
3760
3990
|
magic_cookie=_STUN_MAGIC,
|
|
3761
3991
|
)
|
|
3762
3992
|
_sk_r.sendto(_resp_r, _src_r)
|
|
3993
|
+
_stun_seen = True
|
|
3763
3994
|
_stun_count += 1
|
|
3764
3995
|
except Exception:
|
|
3765
3996
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_is_self_peer_ip', exc_info=True)
|
|
@@ -3784,12 +4015,17 @@ class _SdesOpenMixin:
|
|
|
3784
4015
|
# call_soon_threadsafe(answer_fut.set_result, ...) from the MQTT thread is
|
|
3785
4016
|
# queued but hasn't fired yet. One asyncio cycle resolves it.
|
|
3786
4017
|
await asyncio.sleep(0)
|
|
3787
|
-
# ...but one cycle only helps if the answer had already ARRIVED
|
|
3788
|
-
# has
|
|
3789
|
-
#
|
|
3790
|
-
#
|
|
3791
|
-
#
|
|
3792
|
-
#
|
|
4018
|
+
# ...but one cycle only helps if the answer had already ARRIVED. When it
|
|
4019
|
+
# has, the window above now leaves as soon as it is readable and this
|
|
4020
|
+
# harvest is a formality; when it has not, the window ran to its own
|
|
4021
|
+
# deadline and this wait is the whole story. Either way, wait for it:
|
|
4022
|
+
# the ICE credentials it carries are what the nomination below needs -
|
|
4023
|
+
# without them the camera is never nominated, stays in ICE "Checking",
|
|
4024
|
+
# and never sends SRTP no matter how long the first-media wait runs.
|
|
4025
|
+
# (This comment used to assert the window "closes ~2.4 s before the
|
|
4026
|
+
# camera answers". That stopped being true when the livePlayReq echo
|
|
4027
|
+
# wait was cut to 0.25 s, and the window then sat on the answer for
|
|
4028
|
+
# seconds - see _stun_window_answer_exit_due.)
|
|
3793
4029
|
# shield() so a timeout here cannot cancel the future the real answer
|
|
3794
4030
|
# await (and the DTLS-fallback path) still consume further down.
|
|
3795
4031
|
# A TERMINAL refusal races the harvest. A camera at its viewer cap acks
|
|
@@ -4075,31 +4311,19 @@ class _SdesOpenMixin:
|
|
|
4075
4311
|
# sending a binding request with the USE-CANDIDATE attribute (0x0025).
|
|
4076
4312
|
# Without this, the camera stays in ICE "Checking" state indefinitely
|
|
4077
4313
|
# (hence the continuous duplicate STUN probe log lines) and never streams.
|
|
4078
|
-
import re as _re_ice
|
|
4079
4314
|
|
|
4080
4315
|
# Parse camera's ICE credentials and UDP candidates from its answer SDP.
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
elif _ice_ln.startswith("a=candidate:"):
|
|
4093
|
-
_cand_m = _re_ice.match(
|
|
4094
|
-
r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ (\w+)",
|
|
4095
|
-
_ice_ln,
|
|
4096
|
-
)
|
|
4097
|
-
if _cand_m:
|
|
4098
|
-
_cip, _cport = _cand_m.group(1), int(_cand_m.group(2))
|
|
4099
|
-
_ctyp = _cand_m.group(3)
|
|
4100
|
-
_cam_ice_cands.append((_cip, _cport))
|
|
4101
|
-
if _ctyp == "host" and not _cam_ice_host:
|
|
4102
|
-
_cam_ice_host = (_cip, _cport)
|
|
4316
|
+
# Credentials and candidates from the camera's answer. _cam_ice_host is
|
|
4317
|
+
# the typ host candidate, which is what SCTP is addressed to.
|
|
4318
|
+
_cam_ice_ufrag: str
|
|
4319
|
+
_cam_ice_pwd: str
|
|
4320
|
+
_cam_ice_cands: list # list of (ip, port) tuples
|
|
4321
|
+
_cam_ice_host: tuple
|
|
4322
|
+
# Through the shared parser, not a second copy: the STUN window's early
|
|
4323
|
+
# exit above means "the nomination below will succeed", and it only
|
|
4324
|
+
# means that while both read the same SDP the same way.
|
|
4325
|
+
(_cam_ice_ufrag, _cam_ice_pwd, _cam_ice_cands,
|
|
4326
|
+
_cam_ice_host) = _parse_answer_ice(_pre_launch_answer_sdp)
|
|
4103
4327
|
|
|
4104
4328
|
def _send_use_candidate(sock, our_ufrag, our_pwd, cam_ufrag, cam_pwd, cam_addr):
|
|
4105
4329
|
"""Send a STUN Binding Request with ICE-CONTROLLING + USE-CANDIDATE."""
|
|
@@ -4181,20 +4405,7 @@ class _SdesOpenMixin:
|
|
|
4181
4405
|
"""
|
|
4182
4406
|
if not sdp_text:
|
|
4183
4407
|
return 0
|
|
4184
|
-
_u
|
|
4185
|
-
_cands: list = []
|
|
4186
|
-
for _ln in sdp_text.splitlines():
|
|
4187
|
-
if _ln.startswith("a=ice-ufrag:") and not _u:
|
|
4188
|
-
_u = _ln[len("a=ice-ufrag:"):].strip()
|
|
4189
|
-
elif _ln.startswith("a=ice-pwd:") and not _p:
|
|
4190
|
-
_p = _ln[len("a=ice-pwd:"):].strip()
|
|
4191
|
-
elif _ln.startswith("a=candidate:"):
|
|
4192
|
-
_m = _re_ice.match(
|
|
4193
|
-
r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ (\w+)",
|
|
4194
|
-
_ln,
|
|
4195
|
-
)
|
|
4196
|
-
if _m:
|
|
4197
|
-
_cands.append((_m.group(1), int(_m.group(2))))
|
|
4408
|
+
_u, _p, _cands, _ = _parse_answer_ice(sdp_text)
|
|
4198
4409
|
if not (_u and _p and _cands):
|
|
4199
4410
|
return 0
|
|
4200
4411
|
# Open the relay's door before probing, same invariant the setup and
|
|
@@ -6690,15 +6901,14 @@ class _SdesOpenMixin:
|
|
|
6690
6901
|
while _tk_next < len(ice_cands_seen):
|
|
6691
6902
|
_tk_line = (ice_cands_seen[_tk_next].get("candidate") or "")
|
|
6692
6903
|
_tk_next += 1
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
if not _tk_m:
|
|
6904
|
+
# The shared parser, so a candidate the answer path
|
|
6905
|
+
# nominates cannot be one this path silently skips.
|
|
6906
|
+
_tk_parsed = _parse_candidate_line(_tk_line)
|
|
6907
|
+
if not _tk_parsed:
|
|
6698
6908
|
continue
|
|
6699
|
-
_tk_ip =
|
|
6700
|
-
_tk_port =
|
|
6701
|
-
_tk_typ =
|
|
6909
|
+
_tk_ip = _tk_parsed[0]
|
|
6910
|
+
_tk_port = _tk_parsed[1]
|
|
6911
|
+
_tk_typ = _tk_parsed[2]
|
|
6702
6912
|
if (_tk_ip, _tk_port) in _tk_seen:
|
|
6703
6913
|
continue
|
|
6704
6914
|
_tk_seen.add((_tk_ip, _tk_port))
|
|
@@ -7242,22 +7452,8 @@ class _SdesOpenMixin:
|
|
|
7242
7452
|
# parse the camera's ICE credentials now and populate _bridge_uc_info
|
|
7243
7453
|
# so the bridge can send USE-CANDIDATE on the next camera BindingReq.
|
|
7244
7454
|
if not _bridge_uc_info["sent"] and not _bridge_uc_info["ufrag"] and _ans_sdp:
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
_late_cands: list = []
|
|
7248
|
-
for _al in _ans_sdp.splitlines():
|
|
7249
|
-
if _al.startswith("a=ice-ufrag:") and not _late_ufrag:
|
|
7250
|
-
_late_ufrag = _al[len("a=ice-ufrag:"):].strip()
|
|
7251
|
-
elif _al.startswith("a=ice-pwd:") and not _late_pwd:
|
|
7252
|
-
_late_pwd = _al[len("a=ice-pwd:"):].strip()
|
|
7253
|
-
elif _al.startswith("a=candidate:"):
|
|
7254
|
-
_cm = _re_late_ice.match(
|
|
7255
|
-
r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ",
|
|
7256
|
-
_al,
|
|
7257
|
-
)
|
|
7258
|
-
if _cm:
|
|
7259
|
-
_late_cands.append(
|
|
7260
|
-
(_cm.group(1), int(_cm.group(2))))
|
|
7455
|
+
_late_ufrag, _late_pwd, _late_cands, _ = _parse_answer_ice(
|
|
7456
|
+
_ans_sdp)
|
|
7261
7457
|
if _late_ufrag and _late_pwd and _late_cands:
|
|
7262
7458
|
_bridge_uc_info["ufrag"] = _late_ufrag
|
|
7263
7459
|
_bridge_uc_info["pwd"] = _late_pwd
|
|
@@ -7460,7 +7656,6 @@ class _SdesOpenMixin:
|
|
|
7460
7656
|
# Spawn a background task to pick it up and inject ICE credentials
|
|
7461
7657
|
# into _bridge_uc_info so the bridge can send USE-CANDIDATE.
|
|
7462
7658
|
if second_answer_fut is not None:
|
|
7463
|
-
import re as _re_sa
|
|
7464
7659
|
async def _late_second_answer_task():
|
|
7465
7660
|
nonlocal _dc_answer_has_app
|
|
7466
7661
|
try:
|
|
@@ -7471,21 +7666,8 @@ class _SdesOpenMixin:
|
|
|
7471
7666
|
if (_la_sdp
|
|
7472
7667
|
and not _bridge_uc_info["sent"]
|
|
7473
7668
|
and not _bridge_uc_info["ufrag"]):
|
|
7474
|
-
_la_ufrag
|
|
7475
|
-
|
|
7476
|
-
for _ll in _la_sdp.splitlines():
|
|
7477
|
-
if _ll.startswith("a=ice-ufrag:") and not _la_ufrag:
|
|
7478
|
-
_la_ufrag = _ll[len("a=ice-ufrag:"):].strip()
|
|
7479
|
-
elif _ll.startswith("a=ice-pwd:") and not _la_pwd:
|
|
7480
|
-
_la_pwd = _ll[len("a=ice-pwd:"):].strip()
|
|
7481
|
-
elif _ll.startswith("a=candidate:"):
|
|
7482
|
-
_cm2 = _re_sa.match(
|
|
7483
|
-
r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ",
|
|
7484
|
-
_ll,
|
|
7485
|
-
)
|
|
7486
|
-
if _cm2:
|
|
7487
|
-
_la_cands.append(
|
|
7488
|
-
(_cm2.group(1), int(_cm2.group(2))))
|
|
7669
|
+
(_la_ufrag, _la_pwd, _la_cands,
|
|
7670
|
+
_) = _parse_answer_ice(_la_sdp)
|
|
7489
7671
|
if _la_ufrag and _la_pwd and _la_cands:
|
|
7490
7672
|
_bridge_uc_info["ufrag"] = _la_ufrag
|
|
7491
7673
|
_bridge_uc_info["pwd"] = _la_pwd
|
{python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
@@ -21,6 +21,7 @@ if TYPE_CHECKING: # import cost stays out of the runtime path
|
|
|
21
21
|
|
|
22
22
|
from ..exceptions import AidotCameraBusy, AidotCameraNotReady
|
|
23
23
|
from .constants import _LIVE_PLAY_NOT_READY, fallback_turn_uris, stun_server_uris
|
|
24
|
+
from .sdes_open import _answer_sdp_can_nominate
|
|
24
25
|
from .webrtc import WebRTCSession
|
|
25
26
|
from .protocol import (
|
|
26
27
|
AvioResponseRouter,
|
|
@@ -65,6 +66,34 @@ def _resolve_future_threadsafe(loop, fut, value) -> None:
|
|
|
65
66
|
loop.call_soon_threadsafe(_resolve)
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
def _answer_is_from_the_camera(*, src_addr, user_id) -> bool:
|
|
70
|
+
"""Whether a webrtcResp came from the camera rather than from us.
|
|
71
|
+
|
|
72
|
+
The broker echoes our own SDES webrtcResp back on the account topic, it
|
|
73
|
+
carries our own peerid so the accept filter admits it, and its SDP has our
|
|
74
|
+
ice-ufrag, ice-pwd and candidates - so it satisfies the nomination
|
|
75
|
+
precondition just as the camera's answer does. Without this, our own echo
|
|
76
|
+
would set the answer-is-ready marker and the STUN window would leave having
|
|
77
|
+
heard nothing from the camera at all.
|
|
78
|
+
|
|
79
|
+
Scope, stated precisely: this gates only the marker. It does NOT stop the
|
|
80
|
+
echo resolving ``answer_fut``, which is where the nomination reads its SDP -
|
|
81
|
+
that routing is unchanged, and a session whose ``answer_fut`` takes the echo
|
|
82
|
+
nominates our own addresses whether the window leaves early or late. Fixing
|
|
83
|
+
that means applying this predicate at delivery, which changes established
|
|
84
|
+
behaviour this release has no measurement for.
|
|
85
|
+
|
|
86
|
+
``srcAddr`` carries the account's prefix convention - ``2.`` device,
|
|
87
|
+
``0.`` app, ``9.`` server - and our own messages go out as ``0.{userId}``,
|
|
88
|
+
which is the same test the webrtcReq echo already uses. Device-channel
|
|
89
|
+
messages that omit srcAddr are treated as the camera's: absent evidence,
|
|
90
|
+
keep the behaviour that existed before this gate.
|
|
91
|
+
"""
|
|
92
|
+
if not src_addr:
|
|
93
|
+
return True
|
|
94
|
+
return not str(src_addr).startswith(f"0.{user_id}")
|
|
95
|
+
|
|
96
|
+
|
|
68
97
|
def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer) -> None:
|
|
69
98
|
"""Route a webrtcResp answer to the first-answer or second-answer future.
|
|
70
99
|
|
|
@@ -811,6 +840,15 @@ class _WebRTCOpenMixin:
|
|
|
811
840
|
# MQTT thread and read from the loop: a lone float rebind, never
|
|
812
841
|
# read-modify-write, so no lock is needed.
|
|
813
842
|
self._camera_device_seen_ts = None
|
|
843
|
+
# When this open first saw an answer carrying the ICE credentials and
|
|
844
|
+
# candidates the nomination needs (monotonic). Read by the SDES STUN
|
|
845
|
+
# responder window, which blocks the event loop and so cannot see
|
|
846
|
+
# answer_fut resolve - call_soon_threadsafe only QUEUES that. Stamped
|
|
847
|
+
# here, on the MQTT thread, so the window can leave the moment there is
|
|
848
|
+
# nothing left to wait for. Cleared per open, or the second open would
|
|
849
|
+
# leave its window instantly on the first open's answer. Same lone
|
|
850
|
+
# float rebind as above, so no lock.
|
|
851
|
+
self._camera_answer_ice_ready_ts = None
|
|
814
852
|
# One-shot per open: the keep-alive re-assert fired when the camera
|
|
815
853
|
# first announces itself. See the dispatcher below.
|
|
816
854
|
_wake_keepalive_sent = [False]
|
|
@@ -994,6 +1032,13 @@ class _WebRTCOpenMixin:
|
|
|
994
1032
|
)
|
|
995
1033
|
)
|
|
996
1034
|
return
|
|
1035
|
+
if (self._camera_answer_ice_ready_ts is None
|
|
1036
|
+
and _answer_is_from_the_camera(
|
|
1037
|
+
src_addr=(inner.get("srcAddr")
|
|
1038
|
+
or msg.get("srcAddr") or ""),
|
|
1039
|
+
user_id=user_id)
|
|
1040
|
+
and _answer_sdp_can_nominate(answer.get("sdp", ""))):
|
|
1041
|
+
self._camera_answer_ice_ready_ts = time.monotonic()
|
|
997
1042
|
_deliver_webrtc_answer(
|
|
998
1043
|
loop, answer_fut, second_answer_fut, answer
|
|
999
1044
|
)
|