python-aidot-cameras 1.0.0b30__tar.gz → 1.0.0b31__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.0b30/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b31}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/README.md +1 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/client.py +39 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/hwaccel.py +3 -3
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/protocol.py +17 -7
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sdes.py +10 -1
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sdes_open.py +144 -52
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_env_seams.py +2 -1
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_hwaccel_probe.py +1 -1
- python_aidot_cameras-1.0.0b31/tests/test_media_path_is_reported.py +88 -0
- python_aidot_cameras-1.0.0b31/tests/test_sdes_connection_mode.py +221 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_sprop.py +24 -5
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_security_hardening.py +2 -2
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sprop_unstable_public.py +3 -3
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sprop_write_is_reported_truthfully.py +1 -1
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/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.0b31
|
|
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
|
|
@@ -415,6 +415,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
415
415
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
416
416
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
417
417
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
418
|
+
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
418
419
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
419
420
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
420
421
|
| `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) |
|
|
@@ -375,6 +375,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
375
375
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
376
376
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
377
377
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
378
|
+
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
378
379
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
379
380
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
380
381
|
| `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) |
|
{python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/client.py
RENAMED
|
@@ -3540,6 +3540,7 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
3540
3540
|
stream_idle_s: Optional[float] = None,
|
|
3541
3541
|
sdes_fast_liveplay: Optional[bool] = None,
|
|
3542
3542
|
sdes_skip_turn: Optional[bool] = None,
|
|
3543
|
+
sdes_connection_mode: Optional[str] = None,
|
|
3543
3544
|
sdes_adaptive: Optional[bool] = None,
|
|
3544
3545
|
sdes_audio_gain_db: Optional[float] = None,
|
|
3545
3546
|
) -> None:
|
|
@@ -3616,6 +3617,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
3616
3617
|
self._sdes_fast_liveplay_opt = sdes_fast_liveplay
|
|
3617
3618
|
if sdes_skip_turn is not None:
|
|
3618
3619
|
self._sdes_skip_turn_opt = sdes_skip_turn
|
|
3620
|
+
if sdes_connection_mode is not None:
|
|
3621
|
+
self._sdes_connection_mode_opt = sdes_connection_mode
|
|
3619
3622
|
if sdes_adaptive is not None:
|
|
3620
3623
|
self._sdes_adaptive_opt = sdes_adaptive
|
|
3621
3624
|
if self._stream_task is not None and not self._stream_task.done():
|
|
@@ -4701,6 +4704,33 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
4701
4704
|
)
|
|
4702
4705
|
return False
|
|
4703
4706
|
|
|
4707
|
+
def _resolve_sdes_connection_mode(self) -> str:
|
|
4708
|
+
"""Which media path the session should offer: auto, lan, or relay.
|
|
4709
|
+
|
|
4710
|
+
``auto`` (default) offers every reachable candidate and lets ICE
|
|
4711
|
+
priority prefer the LAN, with the relay as last resort - the shipped
|
|
4712
|
+
behaviour, verified on the full fleet (six of seven direct, only the
|
|
4713
|
+
unreachable unit relayed). ``lan`` drops the relay entirely (routed
|
|
4714
|
+
through _resolve_sdes_skip_turn so the battery force-keep applies).
|
|
4715
|
+
``relay`` offers only the relay candidate and force-keeps the
|
|
4716
|
+
pre-allocation.
|
|
4717
|
+
|
|
4718
|
+
Per-open ``sdes_connection_mode`` wins; else AIDOT_SDES_CONNECTION_MODE;
|
|
4719
|
+
else auto. Anything unrecognised is auto - this is read while opening
|
|
4720
|
+
a stream, and a typo must not stop video.
|
|
4721
|
+
"""
|
|
4722
|
+
for raw in (getattr(self, "_sdes_connection_mode_opt", None),
|
|
4723
|
+
os.environ.get("AIDOT_SDES_CONNECTION_MODE")):
|
|
4724
|
+
if raw is None:
|
|
4725
|
+
continue
|
|
4726
|
+
mode = str(raw).strip().lower()
|
|
4727
|
+
if mode in ("auto", "lan", "relay"):
|
|
4728
|
+
return mode
|
|
4729
|
+
_LOGGER.warning(
|
|
4730
|
+
"camera %s: unknown sdes_connection_mode %r; using auto",
|
|
4731
|
+
getattr(self, "device_id", "?"), raw)
|
|
4732
|
+
return "auto"
|
|
4733
|
+
|
|
4704
4734
|
def _resolve_sdes_skip_turn(self) -> bool:
|
|
4705
4735
|
"""EXPERIMENTAL (opt-in, default off): skip the blocking SDES TURN relay
|
|
4706
4736
|
pre-allocation, for cameras reachable LAN-direct.
|
|
@@ -4730,6 +4760,15 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
4730
4760
|
candidate on the HA segment."""
|
|
4731
4761
|
if getattr(self, "is_battery_camera", False):
|
|
4732
4762
|
return False
|
|
4763
|
+
# The connection mode composes here rather than in a parallel path, so
|
|
4764
|
+
# everything hanging off this resolver (instrumentation, the adaptive
|
|
4765
|
+
# interplay) sees one answer. relay force-keeps the pre-allocation -
|
|
4766
|
+
# forcing the relay while skipping its allocation would offer nothing.
|
|
4767
|
+
_mode = self._resolve_sdes_connection_mode()
|
|
4768
|
+
if _mode == "lan":
|
|
4769
|
+
return True
|
|
4770
|
+
if _mode == "relay":
|
|
4771
|
+
return False
|
|
4733
4772
|
opt = getattr(self, "_sdes_skip_turn_opt", None)
|
|
4734
4773
|
if opt is not None:
|
|
4735
4774
|
return bool(opt)
|
{python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/hwaccel.py
RENAMED
|
@@ -40,7 +40,7 @@ import threading
|
|
|
40
40
|
import time
|
|
41
41
|
from typing import List, Optional
|
|
42
42
|
|
|
43
|
-
from .protocol import
|
|
43
|
+
from .protocol import _sprop_dir # reuse the same per-user cache root
|
|
44
44
|
|
|
45
45
|
_LOGGER = logging.getLogger(__name__)
|
|
46
46
|
|
|
@@ -139,7 +139,7 @@ def _ffmpeg_identity() -> str:
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def _cache_path() -> str:
|
|
142
|
-
return os.path.join(
|
|
142
|
+
return os.path.join(_sprop_dir(), "decoders.json")
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
def _load_cache() -> dict:
|
|
@@ -152,7 +152,7 @@ def _load_cache() -> dict:
|
|
|
152
152
|
|
|
153
153
|
def _save_cache(data: dict) -> None:
|
|
154
154
|
try:
|
|
155
|
-
os.makedirs(
|
|
155
|
+
os.makedirs(_sprop_dir(), exist_ok=True)
|
|
156
156
|
tmp = _cache_path() + ".tmp"
|
|
157
157
|
with open(tmp, "w") as fh:
|
|
158
158
|
json.dump(data, fh)
|
{python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -684,8 +684,18 @@ _XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
|
|
|
684
684
|
os.path.expanduser("~"), ".config")
|
|
685
685
|
|
|
686
686
|
|
|
687
|
-
|
|
688
|
-
|
|
687
|
+
def _sprop_dir() -> str:
|
|
688
|
+
"""The sprop cache directory - AIDOT_SPROP_DIR read at CALL time.
|
|
689
|
+
|
|
690
|
+
A consumer embedded in a larger process (the Home Assistant integration)
|
|
691
|
+
can only learn its persistent storage path at setup, after this module is
|
|
692
|
+
long imported; a module-level snapshot of the env var silently ignored
|
|
693
|
+
that setting and the cache landed back in the container-ephemeral home
|
|
694
|
+
directory, so every container recreation cost a startup-churn window.
|
|
695
|
+
Same lesson as cloud_auth's country default: bind config at call time.
|
|
696
|
+
"""
|
|
697
|
+
return os.environ.get("AIDOT_SPROP_DIR") or os.path.join(
|
|
698
|
+
_XDG_CONFIG_HOME, "aidot", "sprop")
|
|
689
699
|
|
|
690
700
|
|
|
691
701
|
def _extract_param_sets_from_rtp(pkt: bytes) -> dict:
|
|
@@ -740,12 +750,12 @@ def _build_sprop(sps: bytes, pps: bytes) -> str:
|
|
|
740
750
|
def _sprop_cache_path(devid: str) -> str:
|
|
741
751
|
# `devid` originates from the cloud/device and is interpolated into a
|
|
742
752
|
# filesystem path, so a value containing "/", "\\", ".." or an absolute path
|
|
743
|
-
# could escape
|
|
753
|
+
# could escape the sprop dir (arbitrary read/write). Reduce it to a safe,
|
|
744
754
|
# collision-resistant filename component.
|
|
745
755
|
safe = re.sub(r"[^A-Za-z0-9_-]", "_", devid or "")
|
|
746
756
|
if not safe or len(devid or "") > 128:
|
|
747
757
|
safe = hashlib.sha256((devid or "").encode("utf-8", "replace")).hexdigest()
|
|
748
|
-
return os.path.join(
|
|
758
|
+
return os.path.join(_sprop_dir(), f"{safe}.sprop")
|
|
749
759
|
|
|
750
760
|
|
|
751
761
|
def _sprop_unstable_path(devid: str) -> str:
|
|
@@ -834,14 +844,14 @@ def _save_sprop(devid: str, sprop: str) -> bool:
|
|
|
834
844
|
devid,
|
|
835
845
|
)
|
|
836
846
|
try:
|
|
837
|
-
os.makedirs(
|
|
847
|
+
os.makedirs(_sprop_dir(), exist_ok=True)
|
|
838
848
|
with open(_sprop_unstable_path(devid), "w") as fh:
|
|
839
849
|
fh.write("parameter sets changed between sessions\n")
|
|
840
850
|
os.remove(_sprop_cache_path(devid))
|
|
841
851
|
except OSError:
|
|
842
852
|
pass
|
|
843
853
|
return False
|
|
844
|
-
os.makedirs(
|
|
854
|
+
os.makedirs(_sprop_dir(), exist_ok=True)
|
|
845
855
|
tmp = _sprop_cache_path(devid) + ".tmp"
|
|
846
856
|
with open(tmp, "w") as fh:
|
|
847
857
|
fh.write(sprop)
|
|
@@ -851,7 +861,7 @@ def _save_sprop(devid: str, sprop: str) -> bool:
|
|
|
851
861
|
# Surface (don't swallow): if the cache dir isn't writable the whole
|
|
852
862
|
# sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
|
|
853
863
|
_LOGGER.warning("sprop cache write failed (%s): %s - set AIDOT_SPROP_DIR "
|
|
854
|
-
"to a writable path",
|
|
864
|
+
"to a writable path", _sprop_dir(), exc)
|
|
855
865
|
return False
|
|
856
866
|
|
|
857
867
|
|
{python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sdes.py
RENAMED
|
@@ -53,6 +53,7 @@ class SdesSession(AvioRequestMixin):
|
|
|
53
53
|
teardown_requested=None,
|
|
54
54
|
first_video_pt=None,
|
|
55
55
|
first_audio_pt=None,
|
|
56
|
+
media_path=None,
|
|
56
57
|
device_id=None,
|
|
57
58
|
responses=None,
|
|
58
59
|
abort_chan=None,
|
|
@@ -81,6 +82,12 @@ class SdesSession(AvioRequestMixin):
|
|
|
81
82
|
# sent (video 96/97/98, audio 0/8), or None before first media.
|
|
82
83
|
self._first_video_pt = first_video_pt if first_video_pt is not None else [None]
|
|
83
84
|
self._first_audio_pt = first_audio_pt if first_audio_pt is not None else [None]
|
|
85
|
+
# Shared with the bridge thread: [0] becomes "direct" or "relay" when
|
|
86
|
+
# the first media packet arrives, classified by its source address
|
|
87
|
+
# against the session's TURN servers. The connection mode is only a
|
|
88
|
+
# preference - the camera decides where to send from - and this is the
|
|
89
|
+
# receipt that says what actually happened.
|
|
90
|
+
self._media_path = media_path if media_path is not None else [None]
|
|
84
91
|
# Mutable one-element list shared with the bridge thread: [0] flips True
|
|
85
92
|
# the moment ANY locally-initiated ffmpeg-kill path fires (this stop(),
|
|
86
93
|
# the cold-open _reap(), the key-restart proc replace, or the DTLS-
|
|
@@ -229,7 +236,8 @@ class SdesSession(AvioRequestMixin):
|
|
|
229
236
|
the DTLS path allows. These counters are the supported substitute:
|
|
230
237
|
non-zero ``packets``/``bytes`` means real media reached the consumer.
|
|
231
238
|
|
|
232
|
-
Returns ``{packets, bytes, last_media_monotonic, video_pt, audio_pt
|
|
239
|
+
Returns ``{packets, bytes, last_media_monotonic, video_pt, audio_pt,
|
|
240
|
+
media_path}``;
|
|
233
241
|
the payload types are ``None`` until the camera's first packet of that
|
|
234
242
|
kind arrives.
|
|
235
243
|
"""
|
|
@@ -239,6 +247,7 @@ class SdesSession(AvioRequestMixin):
|
|
|
239
247
|
"last_media_monotonic": self._media_progress[0],
|
|
240
248
|
"video_pt": self._first_video_pt[0],
|
|
241
249
|
"audio_pt": self._first_audio_pt[0],
|
|
250
|
+
"media_path": self._media_path[0],
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
def _drained_stderr_tail(self) -> bytes:
|
{python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b31}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -260,6 +260,98 @@ def _env_positive(name: str, cast=int, off=None):
|
|
|
260
260
|
return value if value > 0 else off
|
|
261
261
|
|
|
262
262
|
|
|
263
|
+
def _turn_entry_ips(entries) -> set:
|
|
264
|
+
"""The server addresses named by ICE entries' ``Uris`` lists.
|
|
265
|
+
|
|
266
|
+
Entries look like ``{"Uris": ["stun:3.230.182.123:3478",
|
|
267
|
+
"turn:3.230.182.123:5349?transport=udp"], ...}``. The address is what
|
|
268
|
+
media source classification needs; scheme, port and query are noise.
|
|
269
|
+
"""
|
|
270
|
+
ips = set()
|
|
271
|
+
for entry in entries or ():
|
|
272
|
+
for uri in (entry.get("Uris") or ()):
|
|
273
|
+
body = str(uri).split(":", 1)[-1]
|
|
274
|
+
host = body.split("?", 1)[0].rsplit(":", 1)[0].strip("[]")
|
|
275
|
+
if host:
|
|
276
|
+
ips.add(host)
|
|
277
|
+
return ips
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _classify_media_path(src_ip, turn_ips) -> "Optional[str]":
|
|
281
|
+
""""direct" or "relay" for a media source address, or None when unknown.
|
|
282
|
+
|
|
283
|
+
Media arriving FROM a TURN server address is relayed - that covers both
|
|
284
|
+
shapes on this fleet: the camera's own allocation on the vendor TURN (the
|
|
285
|
+
relayed A001513's media arrives from the TURN address on the camera's
|
|
286
|
+
allocation port, so the PORT must not participate) and a Data Indication
|
|
287
|
+
through ours. Anything else came to us directly.
|
|
288
|
+
"""
|
|
289
|
+
if not src_ip:
|
|
290
|
+
return None
|
|
291
|
+
return "relay" if src_ip in turn_ips else "direct"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _sdes_offer_media_endpoint(mode: str, default_ip: str, default_port: int,
|
|
295
|
+
relay_addr, public_ip):
|
|
296
|
+
"""Where the offer's c=/m= points for one media section: (ip, port, is_relay).
|
|
297
|
+
|
|
298
|
+
This fleet's firmware nominates by dialing c=/m=, not by reading candidate
|
|
299
|
+
lines - measured 2026-08-24, an A001064 and an A001513 both dialed direct
|
|
300
|
+
with only the relay candidate on offer. So forcing the relay means moving
|
|
301
|
+
c=/m= to the allocation, which was this path's ORIGINAL design (see the
|
|
302
|
+
allocation helper's comment) before it was backed out over dropped media.
|
|
303
|
+
|
|
304
|
+
The missing piece back then was the permission: the camera's packets reach
|
|
305
|
+
the TURN server from the house WAN address - OUR srflx address - and
|
|
306
|
+
without a CreatePermission for it the relay drops everything. Relay mode
|
|
307
|
+
therefore requires a known public_ip; without one (or without an
|
|
308
|
+
allocation) it falls back to the direct endpoint, because a stream on the
|
|
309
|
+
wrong path beats no stream.
|
|
310
|
+
"""
|
|
311
|
+
if mode == "relay" and relay_addr and public_ip:
|
|
312
|
+
return relay_addr[0], relay_addr[1], True
|
|
313
|
+
return default_ip, default_port, False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _sdes_offer_candidate_lines(mode: str, local_ip: str, port: int,
|
|
317
|
+
public_ip, relay_addr) -> str:
|
|
318
|
+
"""The a=candidate block for one media section, shaped by the mode.
|
|
319
|
+
|
|
320
|
+
``auto`` - host + srflx (when a public IP is known) + relay (when the
|
|
321
|
+
pre-allocation succeeded), in that priority order. This is the
|
|
322
|
+
shipped behaviour: the LAN wins by ICE priority (host
|
|
323
|
+
2130706431 > srflx 1694498815 > relay 16777215) and the relay
|
|
324
|
+
is the last resort - measured on the full fleet 2026-08-24,
|
|
325
|
+
six of seven cameras direct and only the unit with no route to
|
|
326
|
+
us on the relay.
|
|
327
|
+
``lan`` - host + srflx only. Normally moot (lan mode also skips the
|
|
328
|
+
relay pre-allocation, so there is no relay_addr to omit), but
|
|
329
|
+
enforced here too so the two levers cannot disagree.
|
|
330
|
+
``relay`` - the relay candidate ONLY, so an ICE-speaking camera has
|
|
331
|
+
nothing else to nominate. The relay address must still never
|
|
332
|
+
appear in c=/m= - TURN drops every camera packet without a
|
|
333
|
+
CreatePermission for the camera's public address, which is
|
|
334
|
+
unknown - so candidate lines are the entire steering surface,
|
|
335
|
+
and firmware that ignores ICE and dials c=/m= directly cannot
|
|
336
|
+
be steered. With no allocation to offer this falls back to
|
|
337
|
+
the auto block: an offer with zero candidates is a session
|
|
338
|
+
that cannot start, and a stream on the wrong path beats no
|
|
339
|
+
stream.
|
|
340
|
+
"""
|
|
341
|
+
host = f"a=candidate:1 1 udp 2130706431 {local_ip} {port} typ host\r\n"
|
|
342
|
+
srflx = (f"a=candidate:2 1 udp 1694498815 {public_ip} {port}"
|
|
343
|
+
f" typ srflx raddr {local_ip} rport {port}\r\n"
|
|
344
|
+
if public_ip else "")
|
|
345
|
+
relay = (f"a=candidate:3 1 udp 16777215 {relay_addr[0]} {relay_addr[1]}"
|
|
346
|
+
f" typ relay raddr {local_ip} rport {port}\r\n"
|
|
347
|
+
if relay_addr else "")
|
|
348
|
+
if mode == "relay" and relay:
|
|
349
|
+
return relay
|
|
350
|
+
if mode == "lan":
|
|
351
|
+
return host + srflx
|
|
352
|
+
return host + srflx + relay
|
|
353
|
+
|
|
354
|
+
|
|
263
355
|
def _sdes_offer_bandwidth_kbps():
|
|
264
356
|
"""A receive-bandwidth ceiling for the offer, in kbps, or None for none."""
|
|
265
357
|
return _env_positive("AIDOT_SDES_OFFER_BANDWIDTH_KBPS")
|
|
@@ -1864,6 +1956,13 @@ class _SdesOpenMixin:
|
|
|
1864
1956
|
# _resolve_sdes_skip_turn). Either way the cost is instrumented below so
|
|
1865
1957
|
# the saving is measurable: grep ``signaling-wait[`` for sdes-turn-prealloc.
|
|
1866
1958
|
_skip_turn_prealloc = self._resolve_sdes_skip_turn()
|
|
1959
|
+
# Which media path to offer (auto | lan | relay). Resolved once per
|
|
1960
|
+
# open; the candidate blocks below are built from it. A receipt goes
|
|
1961
|
+
# out whenever it is not the default, because a run that cannot show
|
|
1962
|
+
# the knob was applied cannot tell a result from a coincidence.
|
|
1963
|
+
_conn_mode = self._resolve_sdes_connection_mode()
|
|
1964
|
+
if _conn_mode != "auto" and _status:
|
|
1965
|
+
_status(f"SDES: connection mode {_conn_mode}")
|
|
1867
1966
|
_turn_t0 = time.monotonic()
|
|
1868
1967
|
_turn_did = False
|
|
1869
1968
|
if _sdes_turn_entries and not _fast_connect and not _skip_turn_prealloc:
|
|
@@ -1990,10 +2089,18 @@ class _SdesOpenMixin:
|
|
|
1990
2089
|
# for the camera's public IP which is unknown; relay in c= causes TURN to
|
|
1991
2090
|
# drop every camera packet. Relay is still in a=candidate: for ICE.
|
|
1992
2091
|
# For LAN cameras (_public_ip is None) fall back to local_ip directly.
|
|
1993
|
-
_offer_audio_ip
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
_offer_video_port =
|
|
2092
|
+
_offer_audio_ip, _offer_audio_port, _relay_in_c_a = _sdes_offer_media_endpoint(
|
|
2093
|
+
_conn_mode, _public_ip or local_ip, audio_port,
|
|
2094
|
+
_relay_addrs.get(_audio_sock), _public_ip)
|
|
2095
|
+
_offer_video_ip, _offer_video_port, _relay_in_c_v = _sdes_offer_media_endpoint(
|
|
2096
|
+
_conn_mode, _public_ip or local_ip, video_port,
|
|
2097
|
+
_relay_addrs.get(_video_sock), _public_ip)
|
|
2098
|
+
_relay_in_c = _relay_in_c_a or _relay_in_c_v
|
|
2099
|
+
if _conn_mode == "relay" and not _relay_in_c and _status:
|
|
2100
|
+
_status("SDES: relay mode fell back to the direct endpoint"
|
|
2101
|
+
" (no allocation or no public ip)")
|
|
2102
|
+
elif _relay_in_c and _status:
|
|
2103
|
+
_status("SDES: c=/m= at the relay allocation (relay mode)")
|
|
1997
2104
|
_bundle_hdr_line = (
|
|
1998
2105
|
"a=group:BUNDLE 0 1 2\r\n" if _dc_probe_fp else "a=group:BUNDLE 0 1\r\n"
|
|
1999
2106
|
)
|
|
@@ -2063,18 +2170,9 @@ class _SdesOpenMixin:
|
|
|
2063
2170
|
# the SDP body (confirmed from logcat ground truth 2026-05-22).
|
|
2064
2171
|
+ f"a=ice-ufrag:{_ufrag_a}\r\n"
|
|
2065
2172
|
f"a=ice-pwd:{_pwd_a}\r\n"
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
f" typ srflx raddr {local_ip} rport {audio_port}\r\n"
|
|
2070
|
-
if _public_ip else ""
|
|
2071
|
-
)
|
|
2072
|
-
+ (
|
|
2073
|
-
f"a=candidate:3 1 udp 16777215 {_relay_addrs[_audio_sock][0]}"
|
|
2074
|
-
f" {_relay_addrs[_audio_sock][1]}"
|
|
2075
|
-
f" typ relay raddr {local_ip} rport {audio_port}\r\n"
|
|
2076
|
-
if _audio_sock in _relay_addrs else ""
|
|
2077
|
-
)
|
|
2173
|
+
+ _sdes_offer_candidate_lines(
|
|
2174
|
+
_conn_mode, local_ip, audio_port, _public_ip,
|
|
2175
|
+
_relay_addrs.get(_audio_sock))
|
|
2078
2176
|
# video m-section
|
|
2079
2177
|
+ f"m=video {_offer_video_port} RTP/SAVPF {_video_pt_list}\r\n"
|
|
2080
2178
|
f"c=IN IP4 {_offer_video_ip}\r\n"
|
|
@@ -2086,18 +2184,9 @@ class _SdesOpenMixin:
|
|
|
2086
2184
|
+ "a=rtcp-mux\r\n"
|
|
2087
2185
|
f"a=ice-ufrag:{_ufrag_v}\r\n"
|
|
2088
2186
|
f"a=ice-pwd:{_pwd_v}\r\n"
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
f" typ srflx raddr {local_ip} rport {video_port}\r\n"
|
|
2093
|
-
if _public_ip else ""
|
|
2094
|
-
)
|
|
2095
|
-
+ (
|
|
2096
|
-
f"a=candidate:3 1 udp 16777215 {_relay_addrs[_video_sock][0]}"
|
|
2097
|
-
f" {_relay_addrs[_video_sock][1]}"
|
|
2098
|
-
f" typ relay raddr {local_ip} rport {video_port}\r\n"
|
|
2099
|
-
if _video_sock in _relay_addrs else ""
|
|
2100
|
-
)
|
|
2187
|
+
+ _sdes_offer_candidate_lines(
|
|
2188
|
+
_conn_mode, local_ip, video_port, _public_ip,
|
|
2189
|
+
_relay_addrs.get(_video_sock))
|
|
2101
2190
|
# m=application SCTP DataChannel section for SDES cameras.
|
|
2102
2191
|
# Ground truth from real Leedarson app logcat (2026-05-22):
|
|
2103
2192
|
# m=application 9 SCTP webrtc-datachannel
|
|
@@ -2563,18 +2652,9 @@ class _SdesOpenMixin:
|
|
|
2563
2652
|
"a=rtcp-mux\r\n"
|
|
2564
2653
|
f"a=ice-ufrag:{_ufrag_a}\r\n"
|
|
2565
2654
|
f"a=ice-pwd:{_pwd_a}\r\n"
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
f" typ srflx raddr {local_ip} rport {audio_port}\r\n"
|
|
2570
|
-
if _public_ip else ""
|
|
2571
|
-
)
|
|
2572
|
-
+ (
|
|
2573
|
-
f"a=candidate:3 1 udp 16777215 {_relay_addrs[_audio_sock][0]}"
|
|
2574
|
-
f" {_relay_addrs[_audio_sock][1]}"
|
|
2575
|
-
f" typ relay raddr {local_ip} rport {audio_port}\r\n"
|
|
2576
|
-
if _audio_sock in _relay_addrs else ""
|
|
2577
|
-
)
|
|
2655
|
+
+ _sdes_offer_candidate_lines(
|
|
2656
|
+
_conn_mode, local_ip, audio_port, _public_ip,
|
|
2657
|
+
_relay_addrs.get(_audio_sock))
|
|
2578
2658
|
+ f"m=video {_ans_video_port} RTP/SAVPF 96 97\r\n"
|
|
2579
2659
|
f"c=IN IP4 {_ans_video_ip}\r\n"
|
|
2580
2660
|
"a=sendonly\r\n"
|
|
@@ -2588,18 +2668,9 @@ class _SdesOpenMixin:
|
|
|
2588
2668
|
"a=rtcp-mux\r\n"
|
|
2589
2669
|
f"a=ice-ufrag:{_ufrag_v}\r\n"
|
|
2590
2670
|
f"a=ice-pwd:{_pwd_v}\r\n"
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
f" typ srflx raddr {local_ip} rport {video_port}\r\n"
|
|
2595
|
-
if _public_ip else ""
|
|
2596
|
-
)
|
|
2597
|
-
+ (
|
|
2598
|
-
f"a=candidate:3 1 udp 16777215 {_relay_addrs[_video_sock][0]}"
|
|
2599
|
-
f" {_relay_addrs[_video_sock][1]}"
|
|
2600
|
-
f" typ relay raddr {local_ip} rport {video_port}\r\n"
|
|
2601
|
-
if _video_sock in _relay_addrs else ""
|
|
2602
|
-
)
|
|
2671
|
+
+ _sdes_offer_candidate_lines(
|
|
2672
|
+
_conn_mode, local_ip, video_port, _public_ip,
|
|
2673
|
+
_relay_addrs.get(_video_sock))
|
|
2603
2674
|
)
|
|
2604
2675
|
_compressed_sdp_ans = _compress_sdp_req(_relay_answer_sdp)
|
|
2605
2676
|
|
|
@@ -3777,6 +3848,17 @@ class _SdesOpenMixin:
|
|
|
3777
3848
|
)
|
|
3778
3849
|
return _perm_ok
|
|
3779
3850
|
|
|
3851
|
+
# Relay mode only: the camera dials our relay allocation, and its
|
|
3852
|
+
# packets arrive at the TURN server from the house WAN address - OUR
|
|
3853
|
+
# public address - so that address needs a permission or the path is a
|
|
3854
|
+
# black hole. Deliberately NOT done outside relay mode: a permission
|
|
3855
|
+
# for our own srflx caused TURN self-loop Data Indications and STUN
|
|
3856
|
+
# echo storms when it was installed indiscriminately (see the warning
|
|
3857
|
+
# in the allocation helper). In relay mode nothing of ours is sent to
|
|
3858
|
+
# our own allocation, so the loop has no driver.
|
|
3859
|
+
if _relay_in_c and _public_ip:
|
|
3860
|
+
_turn_install_permissions([(_public_ip, 9)], "relay-mode WAN")
|
|
3861
|
+
|
|
3780
3862
|
if _cam_ice_ufrag and _cam_ice_pwd and _cam_ice_cands:
|
|
3781
3863
|
_turn_install_permissions(_cam_ice_cands, "setup")
|
|
3782
3864
|
|
|
@@ -3909,6 +3991,12 @@ class _SdesOpenMixin:
|
|
|
3909
3991
|
# bridge on the first video RTP packet so the ffmpeg SDP can be narrowed
|
|
3910
3992
|
# to the matching single codec before ffmpeg is launched.
|
|
3911
3993
|
_first_video_pt: list = [None]
|
|
3994
|
+
_media_path: list = [None]
|
|
3995
|
+
# Every TURN server address this session could be relayed through:
|
|
3996
|
+
# our allocations plus the ICE entries themselves - the camera can
|
|
3997
|
+
# arrive via its OWN vendor allocation even when ours was skipped.
|
|
3998
|
+
_bridge_turn_ips = ({a[0] for a in _relay_addrs.values()}
|
|
3999
|
+
| _turn_entry_ips(_sdes_turn_entries))
|
|
3912
4000
|
# Fallback for the above, filled from the camera's negotiated answer SDP
|
|
3913
4001
|
# when the wait below expires without a single video packet. Kept in a
|
|
3914
4002
|
# list for the same reason as _first_video_pt: the serve-restart path
|
|
@@ -5504,6 +5592,9 @@ class _SdesOpenMixin:
|
|
|
5504
5592
|
if len(_bpkt) >= 12:
|
|
5505
5593
|
_bridge_fn._cam_video_ssrc = _st_br.unpack_from(
|
|
5506
5594
|
'!I', _bpkt, 8)[0]
|
|
5595
|
+
if _media_path[0] is None:
|
|
5596
|
+
_media_path[0] = _classify_media_path(
|
|
5597
|
+
_bsrc[0], _bridge_turn_ips)
|
|
5507
5598
|
_bridge_fn._cam_srtp_src = _bsrc
|
|
5508
5599
|
_bridge_fn._cam_srtp_sock = _bs
|
|
5509
5600
|
if getattr(_bridge_fn, '_first_video_ts',
|
|
@@ -6618,6 +6709,7 @@ class _SdesOpenMixin:
|
|
|
6618
6709
|
talk_state=_talk_state,
|
|
6619
6710
|
media_progress=_media_progress,
|
|
6620
6711
|
media_counts=_media_counts,
|
|
6712
|
+
media_path=_media_path,
|
|
6621
6713
|
teardown_requested=_teardown_holder,
|
|
6622
6714
|
first_video_pt=_first_video_pt,
|
|
6623
6715
|
first_audio_pt=_first_audio_pt,
|
|
@@ -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.0b31"
|
|
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.0b31
|
|
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
|
|
@@ -415,6 +415,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
415
415
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
416
416
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
417
417
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
418
|
+
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
418
419
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
419
420
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
420
421
|
| `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) |
|
|
@@ -145,6 +145,7 @@ tests/test_logging_caps.py
|
|
|
145
145
|
tests/test_login_connect_deadline.py
|
|
146
146
|
tests/test_login_reconnect_backoff.py
|
|
147
147
|
tests/test_media_counted_only_when_plaintext.py
|
|
148
|
+
tests/test_media_path_is_reported.py
|
|
148
149
|
tests/test_media_socket_rcvbuf.py
|
|
149
150
|
tests/test_media_wait_floor.py
|
|
150
151
|
tests/test_motion_poll.py
|
|
@@ -191,6 +192,7 @@ tests/test_sd_playback.py
|
|
|
191
192
|
tests/test_sdes_adaptive.py
|
|
192
193
|
tests/test_sdes_avio_dispatch.py
|
|
193
194
|
tests/test_sdes_bridge_break_gate.py
|
|
195
|
+
tests/test_sdes_connection_mode.py
|
|
194
196
|
tests/test_sdes_echo_wait_timeout.py
|
|
195
197
|
tests/test_sdes_fast_liveplay.py
|
|
196
198
|
tests/test_sdes_ffmpeg_stderr_log.py
|
|
@@ -128,11 +128,12 @@ def test_sdes_media_stats_defaults_and_counts():
|
|
|
128
128
|
sess._media_progress = progress
|
|
129
129
|
sess._first_video_pt = vpt
|
|
130
130
|
sess._first_audio_pt = apt
|
|
131
|
+
sess._media_path = [None]
|
|
131
132
|
|
|
132
133
|
stats = sess.media_stats()
|
|
133
134
|
assert stats == {
|
|
134
135
|
"packets": 0, "bytes": 0, "last_media_monotonic": 0.0,
|
|
135
|
-
"video_pt": None, "audio_pt": None,
|
|
136
|
+
"video_pt": None, "audio_pt": None, "media_path": None,
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
# bridge-thread updates are visible through the shared lists
|
|
@@ -29,7 +29,7 @@ from aidot_cameras.camera import hwaccel
|
|
|
29
29
|
@pytest.fixture(autouse=True)
|
|
30
30
|
def _isolate(tmp_path, monkeypatch):
|
|
31
31
|
"""Per-test cache dir and clean memo state."""
|
|
32
|
-
monkeypatch.
|
|
32
|
+
monkeypatch.setenv("AIDOT_SPROP_DIR", str(tmp_path))
|
|
33
33
|
monkeypatch.setattr(hwaccel, "_cache_mem", {})
|
|
34
34
|
monkeypatch.setattr(hwaccel, "_identity_memo", None)
|
|
35
35
|
monkeypatch.setattr(hwaccel, "_decoders_memo", None)
|