python-aidot-cameras 1.0.0rc2__tar.gz → 1.0.0rc4__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.0rc2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc4}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/README.md +1 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/client.py +127 -4
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/controls.py +127 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/protocol.py +71 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/sdes_open.py +66 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/python_aidot_cameras.egg-info/SOURCES.txt +4 -0
- python_aidot_cameras-1.0.0rc4/tests/test_camera_properties.py +106 -0
- python_aidot_cameras-1.0.0rc4/tests/test_direct_ts_keyframe_signal.py +140 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_direct_ts_serve.py +11 -0
- python_aidot_cameras-1.0.0rc4/tests/test_publish_target_readiness.py +74 -0
- python_aidot_cameras-1.0.0rc4/tests/test_reboot_action.py +67 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/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.0rc4
|
|
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
|
|
@@ -418,6 +418,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
418
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`) |
|
|
419
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) |
|
|
420
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) |
|
|
421
|
+
| `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
|
|
421
422
|
| `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) |
|
|
422
423
|
| `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) |
|
|
423
424
|
| `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`) |
|
|
@@ -378,6 +378,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
378
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`) |
|
|
379
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) |
|
|
380
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) |
|
|
381
|
+
| `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
|
|
381
382
|
| `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) |
|
|
382
383
|
| `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) |
|
|
383
384
|
| `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`) |
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/client.py
RENAMED
|
@@ -2376,9 +2376,15 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
2376
2376
|
timeout: float = 8.0,
|
|
2377
2377
|
ack_keyword: str = "setDevAttr",
|
|
2378
2378
|
seq: "Optional[str]" = None,
|
|
2379
|
+
expect_ack: bool = True,
|
|
2379
2380
|
) -> bool:
|
|
2380
2381
|
"""Connect MQTT, optionally wake battery camera, publish command, wait for ack.
|
|
2381
2382
|
|
|
2383
|
+
``expect_ack=False`` reports whether the command was PUBLISHED, not
|
|
2384
|
+
whether the camera answered. That is the honest result for an action
|
|
2385
|
+
whose success removes the camera from the network -- a reboot acks by
|
|
2386
|
+
going away, so waiting for one turns a working command into a False.
|
|
2387
|
+
|
|
2382
2388
|
All setDevAttrReq callers in the APK (b6.java, LiveCameraView, etc.)
|
|
2383
2389
|
are in live-view screens where the camera is already awake and MQTT-
|
|
2384
2390
|
connected. For battery cameras we send lowPowerActiveStateReq first so
|
|
@@ -2454,6 +2460,17 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
2454
2460
|
duration=timeout,
|
|
2455
2461
|
)
|
|
2456
2462
|
|
|
2463
|
+
# A fire-and-forget caller only needs to know the publish left the host.
|
|
2464
|
+
# `_st` separates that from "no ack came back", which the ack scan below
|
|
2465
|
+
# cannot: it returns False for a failed publish and for a camera that
|
|
2466
|
+
# rebooted before answering.
|
|
2467
|
+
if not expect_ack:
|
|
2468
|
+
_published = not (_st and _st.get("error"))
|
|
2469
|
+
_LOGGER.info(
|
|
2470
|
+
"_mqtt_device_cmd: published without waiting for an ack "
|
|
2471
|
+
"(topic=%s published=%s)", pub_topic, _published)
|
|
2472
|
+
return _published
|
|
2473
|
+
|
|
2457
2474
|
# Two passes, because being sure is worth more than being quick. A
|
|
2458
2475
|
# response carrying OUR seq is proof; anything else on these topics is
|
|
2459
2476
|
# only evidence that SOMETHING was acked - and several things are in
|
|
@@ -2546,6 +2563,7 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
2546
2563
|
"""
|
|
2547
2564
|
import json as _json
|
|
2548
2565
|
import random as _random
|
|
2566
|
+
import time as _time
|
|
2549
2567
|
|
|
2550
2568
|
lan = getattr(self, "_lan_client", None)
|
|
2551
2569
|
if lan is not None:
|
|
@@ -2567,11 +2585,17 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
2567
2585
|
inner["userId"] = str(user_id)
|
|
2568
2586
|
if cam_pwd:
|
|
2569
2587
|
inner["password"] = cam_pwd
|
|
2588
|
+
# `tst` matches the app, which stamps every setDevAttrReq and
|
|
2589
|
+
# devActionReq it builds. The camera accepts the message without it --
|
|
2590
|
+
# this path is proven and reads back within one poll -- so this is
|
|
2591
|
+
# parity rather than a fix; it costs nothing and removes one more way
|
|
2592
|
+
# our traffic differs from the vendor's.
|
|
2570
2593
|
payload = _json.dumps({
|
|
2571
2594
|
"id": device_id,
|
|
2572
2595
|
"method": "setDevAttrReq",
|
|
2573
2596
|
"service": "device",
|
|
2574
2597
|
"seq": seq,
|
|
2598
|
+
"tst": int(_time.time() * 1000),
|
|
2575
2599
|
"payload": inner,
|
|
2576
2600
|
})
|
|
2577
2601
|
pub_topic = f"iot/v1/c/{device_id}/device/setDevAttrReq"
|
|
@@ -2592,12 +2616,92 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
2592
2616
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_set_device_attribute', exc_info=True)
|
|
2593
2617
|
return ok
|
|
2594
2618
|
|
|
2619
|
+
async def async_query_device_action(
|
|
2620
|
+
self,
|
|
2621
|
+
action: str,
|
|
2622
|
+
params=None,
|
|
2623
|
+
*,
|
|
2624
|
+
timeout: float = 8.0,
|
|
2625
|
+
):
|
|
2626
|
+
"""Send a read-only devActionReq and return the camera's ``out`` payload.
|
|
2627
|
+
|
|
2628
|
+
:meth:`async_trigger_device_action` answers "did it go", which is all a
|
|
2629
|
+
control needs. But the camera *answers* its ``get`` actions with real
|
|
2630
|
+
data -- wifi details, SD capacity, which sound algorithms are armed --
|
|
2631
|
+
and none of that was reachable while the only helper returned a bool.
|
|
2632
|
+
|
|
2633
|
+
Returns the decoded ``out`` exactly as the camera sent it (list or
|
|
2634
|
+
dict), or ``None`` when nothing came back. ``None`` means UNKNOWN, not
|
|
2635
|
+
"off": several of these actions answer with ``out: null`` on models that
|
|
2636
|
+
do not implement them, and a caller that reads that as a disabled
|
|
2637
|
+
feature will report a camera as having something switched off when it
|
|
2638
|
+
simply cannot say.
|
|
2639
|
+
|
|
2640
|
+
Deliberately built on the module-level session helper rather than on
|
|
2641
|
+
:meth:`_mqtt_device_cmd`: that method reduces a reply to a bool and is
|
|
2642
|
+
the proven control path, so it is left alone.
|
|
2643
|
+
"""
|
|
2644
|
+
import json as _json
|
|
2645
|
+
import random as _random
|
|
2646
|
+
import time as _time
|
|
2647
|
+
|
|
2648
|
+
user_id = self.user_id
|
|
2649
|
+
device_id = self.device_id
|
|
2650
|
+
smarthome_auth = await self._async_get_smarthome_auth()
|
|
2651
|
+
mqtt_url = await self._async_get_mqtt_url()
|
|
2652
|
+
if not mqtt_url:
|
|
2653
|
+
_LOGGER.debug("async_query_device_action: no MQTT URL for %s", device_id)
|
|
2654
|
+
return None
|
|
2655
|
+
mqtt_user = (smarthome_auth or {}).get("mqttUser") or str(user_id)
|
|
2656
|
+
mqtt_pwd = (smarthome_auth or {}).get("mqttPassword") or ""
|
|
2657
|
+
client_id = (self._user_info.get("mqttClientId") or f"app-{mqtt_user}")
|
|
2658
|
+
|
|
2659
|
+
_dev = self._camera_properties() or {}
|
|
2660
|
+
parent_id = (getattr(getattr(self, "info", None), "direct_id", None)
|
|
2661
|
+
or _dev.get("directId") or _dev.get("parentId") or device_id)
|
|
2662
|
+
seq = f"ap{_random.randint(1000000, 9999999)}"
|
|
2663
|
+
payload = _json.dumps({
|
|
2664
|
+
"method": "devActionReq",
|
|
2665
|
+
"service": "device",
|
|
2666
|
+
"seq": seq,
|
|
2667
|
+
"tst": int(_time.time() * 1000),
|
|
2668
|
+
"payload": {
|
|
2669
|
+
"devId": device_id,
|
|
2670
|
+
"parentId": parent_id,
|
|
2671
|
+
"action": action,
|
|
2672
|
+
"in": params if params is not None else [],
|
|
2673
|
+
},
|
|
2674
|
+
})
|
|
2675
|
+
publish_items = [
|
|
2676
|
+
(f"iot/v1/s/{user_id}/device/devActionReq", payload),
|
|
2677
|
+
]
|
|
2678
|
+
messages, _st = await _mqtt_session_with_status(
|
|
2679
|
+
mqtt_url, mqtt_user, mqtt_pwd, client_id,
|
|
2680
|
+
subscribe_topics=[f"iot/v1/c/{user_id}/#", f"iot/v1/cb/{user_id}/#"],
|
|
2681
|
+
publish_items=publish_items,
|
|
2682
|
+
duration=timeout,
|
|
2683
|
+
)
|
|
2684
|
+
for topic, raw in messages:
|
|
2685
|
+
if "devAction" not in topic:
|
|
2686
|
+
continue
|
|
2687
|
+
try:
|
|
2688
|
+
msg = _json.loads(raw)
|
|
2689
|
+
except Exception:
|
|
2690
|
+
continue
|
|
2691
|
+
body = msg.get("payload") or {}
|
|
2692
|
+
if body.get("action") != action:
|
|
2693
|
+
continue
|
|
2694
|
+
return body.get("out")
|
|
2695
|
+
_LOGGER.debug("devActionReq %s: no reply from %s", action, device_id)
|
|
2696
|
+
return None
|
|
2697
|
+
|
|
2595
2698
|
async def async_trigger_device_action(
|
|
2596
2699
|
self,
|
|
2597
2700
|
action: str,
|
|
2598
2701
|
params,
|
|
2599
2702
|
*,
|
|
2600
2703
|
timeout: float = 4.0,
|
|
2704
|
+
expect_ack: bool = True,
|
|
2601
2705
|
) -> bool:
|
|
2602
2706
|
"""Send a devActionReq (e.g. siren/alarm trigger).
|
|
2603
2707
|
|
|
@@ -2607,23 +2711,42 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
2607
2711
|
"payload": {"devId": deviceId, "action": action, "in": params}}
|
|
2608
2712
|
"""
|
|
2609
2713
|
import json as _json
|
|
2714
|
+
import random as _random
|
|
2715
|
+
import time as _time
|
|
2610
2716
|
|
|
2611
2717
|
user_id = self.user_id
|
|
2612
2718
|
device_id = self.device_id
|
|
2613
2719
|
|
|
2720
|
+
# The app's own builder (main chunk, DeviceControl) sends five things we
|
|
2721
|
+
# did not: `seq`, `tst`, and a `parentId` alongside `devId`. The camera
|
|
2722
|
+
# answers nothing at all to a devActionReq without them -- a RebootFunc
|
|
2723
|
+
# sent in the short form drew zero replies and did not reboot, while the
|
|
2724
|
+
# same action with these fields is what the settings page uses.
|
|
2725
|
+
#
|
|
2726
|
+
# `password` is deliberately NOT sent: the H5 passes it to its wrapper,
|
|
2727
|
+
# but the wrapper does not put it on the wire, and a read-only
|
|
2728
|
+
# `WifiBaseInfo` acks without it.
|
|
2729
|
+
_dev = self._camera_properties() or {}
|
|
2730
|
+
parent_id = (getattr(getattr(self, "info", None), "direct_id", None)
|
|
2731
|
+
or _dev.get("directId") or _dev.get("parentId") or device_id)
|
|
2732
|
+
seq = f"ap{_random.randint(1000000, 9999999)}"
|
|
2614
2733
|
payload = _json.dumps({
|
|
2615
2734
|
"method": "devActionReq",
|
|
2616
2735
|
"service": "device",
|
|
2736
|
+
"seq": seq,
|
|
2737
|
+
"tst": int(_time.time() * 1000),
|
|
2617
2738
|
"payload": {
|
|
2618
|
-
"devId":
|
|
2619
|
-
"
|
|
2620
|
-
"
|
|
2739
|
+
"devId": device_id,
|
|
2740
|
+
"parentId": parent_id,
|
|
2741
|
+
"action": action,
|
|
2742
|
+
"in": params,
|
|
2621
2743
|
},
|
|
2622
2744
|
})
|
|
2623
2745
|
pub_topic = f"iot/v1/s/{user_id}/device/devActionReq"
|
|
2624
2746
|
_LOGGER.info("devActionReq: %s %s -> %s", action, params, device_id)
|
|
2625
2747
|
return await self._mqtt_device_cmd(
|
|
2626
|
-
pub_topic, payload, timeout=timeout, ack_keyword="devAction"
|
|
2748
|
+
pub_topic, payload, timeout=timeout, ack_keyword="devAction",
|
|
2749
|
+
seq=seq, expect_ack=expect_ack)
|
|
2627
2750
|
|
|
2628
2751
|
# Convenience wrappers - confirmed attribute names/types from APK source
|
|
2629
2752
|
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/controls.py
RENAMED
|
@@ -9,6 +9,8 @@ mixin must be combined into CameraMixin (it is not usable standalone).
|
|
|
9
9
|
import logging
|
|
10
10
|
import struct
|
|
11
11
|
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
12
14
|
from .constants import _PTZ_DIR_CODES, _STREAM_QUALITY, SETSTREAMCTRL_CMD
|
|
13
15
|
|
|
14
16
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -87,6 +89,131 @@ class _CameraControlsMixin:
|
|
|
87
89
|
self.status.siren = on
|
|
88
90
|
return result
|
|
89
91
|
|
|
92
|
+
#: Sound-detection algorithms, in the order the camera reports them. The
|
|
93
|
+
#: camera answers ``soundAlgorithmGet`` with a list of single-key dicts --
|
|
94
|
+
#: ``[{"sound_enable": 0}, {"glass_Break": 0}, {"smoke_T3": 0}, ...]`` --
|
|
95
|
+
#: so the key names come from the camera rather than from us. The vendor
|
|
96
|
+
#: app's own strings name baby_cry and dog_bark alongside these.
|
|
97
|
+
#: ``sound_enable`` is the master switch; the rest are individual detectors.
|
|
98
|
+
SOUND_MASTER_KEY = "sound_enable"
|
|
99
|
+
|
|
100
|
+
async def async_get_sound_detection(self) -> "Optional[dict]":
|
|
101
|
+
"""Which sound detectors are armed, as ``{key: bool}``.
|
|
102
|
+
|
|
103
|
+
``None`` means the camera did not answer -- unknown, NOT all-off. A
|
|
104
|
+
model that does not implement this replies with a null ``out``, and
|
|
105
|
+
reporting that as a set of disabled switches would invent a state the
|
|
106
|
+
camera never claimed.
|
|
107
|
+
"""
|
|
108
|
+
out = await self.async_query_device_action("soundAlgorithmGet")
|
|
109
|
+
if not isinstance(out, list):
|
|
110
|
+
return None
|
|
111
|
+
flags: dict = {}
|
|
112
|
+
for item in out:
|
|
113
|
+
if isinstance(item, dict):
|
|
114
|
+
for key, value in item.items():
|
|
115
|
+
flags[key] = bool(value)
|
|
116
|
+
return flags or None
|
|
117
|
+
|
|
118
|
+
async def async_set_sound_detection(self, key: str, enabled: bool) -> bool:
|
|
119
|
+
"""Arm or disarm one sound detector.
|
|
120
|
+
|
|
121
|
+
Read-modify-write: the camera is asked for the current list and gets
|
|
122
|
+
the same structure back with one flag changed. The alternative is
|
|
123
|
+
inventing a payload schema for ``soundAlgorithmSet``, and the camera
|
|
124
|
+
reports the shape it wants -- so echo it rather than guess it.
|
|
125
|
+
"""
|
|
126
|
+
out = await self.async_query_device_action("soundAlgorithmGet")
|
|
127
|
+
if not isinstance(out, list):
|
|
128
|
+
_LOGGER.warning(
|
|
129
|
+
"sound detection: %s did not report its algorithms; not writing",
|
|
130
|
+
self.device_id)
|
|
131
|
+
return False
|
|
132
|
+
found = False
|
|
133
|
+
payload = []
|
|
134
|
+
for item in out:
|
|
135
|
+
if isinstance(item, dict) and key in item:
|
|
136
|
+
payload.append({key: 1 if enabled else 0})
|
|
137
|
+
found = True
|
|
138
|
+
else:
|
|
139
|
+
payload.append(item)
|
|
140
|
+
if not found:
|
|
141
|
+
_LOGGER.warning("sound detection: %s does not report %r", self.device_id, key)
|
|
142
|
+
return False
|
|
143
|
+
return await self.async_trigger_device_action("soundAlgorithmSet", payload)
|
|
144
|
+
|
|
145
|
+
async def async_get_wifi_info(self) -> "Optional[dict]":
|
|
146
|
+
"""``{"ssid": str, "rssi": int}`` for the camera's own WiFi link.
|
|
147
|
+
|
|
148
|
+
The camera answers ``WifiBaseInfo`` with a positional list whose first
|
|
149
|
+
two entries are the SSID and signal strength. Only those two are named
|
|
150
|
+
here -- the third is an integer that looks like an encoded address, and
|
|
151
|
+
naming a field we have not confirmed is how a guess becomes a fact.
|
|
152
|
+
"""
|
|
153
|
+
out = await self.async_query_device_action("WifiBaseInfo")
|
|
154
|
+
if not isinstance(out, list) or not out:
|
|
155
|
+
return None
|
|
156
|
+
info: dict = {"ssid": out[0] if isinstance(out[0], str) else None}
|
|
157
|
+
if len(out) > 1 and isinstance(out[1], (int, float)):
|
|
158
|
+
info["rssi"] = int(out[1])
|
|
159
|
+
return info
|
|
160
|
+
|
|
161
|
+
async def async_get_sd_card_info(self) -> "Optional[dict]":
|
|
162
|
+
"""``{"present": bool, "total": int, "used": int, "raw": list}``.
|
|
163
|
+
|
|
164
|
+
``SDcardBaseInfo`` answers positionally: ``[False, 0, 0, 0, 0]`` with no
|
|
165
|
+
card, ``[True, 29838, 28848, 3, ...]`` on a nearly-full one.
|
|
166
|
+
|
|
167
|
+
Only the first three fields are named. The fourth was almost called
|
|
168
|
+
"free" -- but 29838 - 28848 is 990, not 3, so it is not a remainder;
|
|
169
|
+
28848/29838 is 96.7%, which makes 3 look like a percentage. "Looks
|
|
170
|
+
like" is not confirmation, so it stays unnamed in ``raw`` rather than
|
|
171
|
+
shipping a field that reports 3 MB free on a 30 GB card.
|
|
172
|
+
|
|
173
|
+
Units for ``total`` and ``used`` are unconfirmed too (MB is plausible
|
|
174
|
+
at these magnitudes), so the numbers are passed through exactly as the
|
|
175
|
+
camera reports them rather than converted.
|
|
176
|
+
"""
|
|
177
|
+
out = await self.async_query_device_action("SDcardBaseInfo")
|
|
178
|
+
if not isinstance(out, list) or not out:
|
|
179
|
+
return None
|
|
180
|
+
def _n(i):
|
|
181
|
+
return int(out[i]) if len(out) > i and isinstance(out[i], (int, float)) else None
|
|
182
|
+
return {"present": bool(out[0]), "total": _n(1), "used": _n(2),
|
|
183
|
+
"raw": list(out)}
|
|
184
|
+
|
|
185
|
+
async def async_reboot(self) -> bool:
|
|
186
|
+
"""Ask the camera to reboot. True means the request was SENT.
|
|
187
|
+
|
|
188
|
+
The vendor app's camera settings page does exactly this -- its H5
|
|
189
|
+
bundle calls ``devActionReq({action: "RebootFunc", in: []})`` -- and
|
|
190
|
+
``RebootFunc`` also appears in the native ``NewLivePresenter``. It is
|
|
191
|
+
the same ``devActionReq`` transport the siren already uses.
|
|
192
|
+
|
|
193
|
+
Two deliberate choices:
|
|
194
|
+
|
|
195
|
+
* **No ack is awaited.** A reboot succeeds by taking the camera off the
|
|
196
|
+
network, so a camera that reboots before answering would otherwise be
|
|
197
|
+
reported as a failure. The app passes no timeout for this action
|
|
198
|
+
either, unlike the reads beside it. So this reports "requested", and
|
|
199
|
+
the caller must not read it as "rebooted".
|
|
200
|
+
* **Refused when the cloud says the camera is offline.** The app only
|
|
201
|
+
renders its reboot button when the device is reachable
|
|
202
|
+
(``isCanUseStatus``), and a wake-then-reboot on a sleeping battery
|
|
203
|
+
camera is not something to fire blind.
|
|
204
|
+
|
|
205
|
+
The camera is gone for about a minute afterwards (the app's own wording
|
|
206
|
+
is "The device Needs a minute to reboot"), during which a viewer sees a
|
|
207
|
+
session that connects and delivers nothing.
|
|
208
|
+
"""
|
|
209
|
+
if (not self.status.online) and getattr(self, "_cloud_online_explicit", False):
|
|
210
|
+
_LOGGER.warning(
|
|
211
|
+
"reboot refused for %s: the cloud reports it offline", self.device_id)
|
|
212
|
+
return False
|
|
213
|
+
_LOGGER.info("reboot requested for %s", self.device_id)
|
|
214
|
+
return await self.async_trigger_device_action(
|
|
215
|
+
"RebootFunc", [], expect_ack=False)
|
|
216
|
+
|
|
90
217
|
async def async_set_night_vision(self, mode: str) -> bool:
|
|
91
218
|
"""mode: 'auto' (0), 'on' (1), 'off' (2)"""
|
|
92
219
|
_modes = {"auto": 0, "on": 1, "off": 2}
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -1058,6 +1058,11 @@ class _DirectTsServer:
|
|
|
1058
1058
|
self._pmt: "Optional[bytes]" = None
|
|
1059
1059
|
self._pmt_pid: "Optional[int]" = None
|
|
1060
1060
|
self._synced = False # has the current consumer been given a start
|
|
1061
|
+
# Set by the mux immediately before it writes a video keyframe, so a new
|
|
1062
|
+
# consumer can be spliced where the picture is actually decodable.
|
|
1063
|
+
self._kf_pending = False
|
|
1064
|
+
self._kf_ever = False
|
|
1065
|
+
self._waiting_since: "Optional[float]" = None
|
|
1061
1066
|
|
|
1062
1067
|
@property
|
|
1063
1068
|
def port(self) -> int:
|
|
@@ -1141,6 +1146,51 @@ class _DirectTsServer:
|
|
|
1141
1146
|
except (IndexError, ValueError):
|
|
1142
1147
|
return
|
|
1143
1148
|
|
|
1149
|
+
#: How long a new consumer may wait for a keyframe signal before the old
|
|
1150
|
+
#: PID-blind random-access test is allowed to start it anyway. The signal is
|
|
1151
|
+
#: the correct trigger, but a stream that never sends one must not hang
|
|
1152
|
+
#: forever - gating solely on a video-pid flag served ZERO bytes when tried
|
|
1153
|
+
#: (2026-08-29), because the muxer does not set it.
|
|
1154
|
+
_LEGACY_SYNC_AFTER_S = 5.0
|
|
1155
|
+
|
|
1156
|
+
def mark_keyframe(self) -> None:
|
|
1157
|
+
"""Tell the server the next bytes written begin a video keyframe.
|
|
1158
|
+
|
|
1159
|
+
The mux knows this and used to throw it away: it pulls ``(data, ts, kf)``
|
|
1160
|
+
off the queue and ``kf`` came from ``_h264_has_keyframe`` at the tap.
|
|
1161
|
+
Everything downstream then tried to rediscover it from the container,
|
|
1162
|
+
which does not carry it - PyAV's mpegts muxer does not set
|
|
1163
|
+
``random_access_indicator`` on the video PID in this stream, so a sync
|
|
1164
|
+
gated on that flag never fires at all.
|
|
1165
|
+
|
|
1166
|
+
Splicing a consumer anywhere else hands it video mid-GOP referencing an
|
|
1167
|
+
SPS/PPS it never received, which decodes as "non-existing PPS 0
|
|
1168
|
+
referenced": a video track that never produces a frame.
|
|
1169
|
+
"""
|
|
1170
|
+
if not self._kf_ever:
|
|
1171
|
+
_LOGGER.debug("direct-ts: first keyframe signal reached the server")
|
|
1172
|
+
self._kf_ever = True
|
|
1173
|
+
self._kf_pending = True
|
|
1174
|
+
|
|
1175
|
+
def _legacy_sync_ok(self, pkt: bytes) -> bool:
|
|
1176
|
+
"""The pre-signal behaviour, allowed only after a grace period.
|
|
1177
|
+
|
|
1178
|
+
Kept because a source that never calls ``mark_keyframe`` must still be
|
|
1179
|
+
servable. Deliberately NOT the first choice: this test accepts a
|
|
1180
|
+
random-access point on ANY pid, and AAC audio sets that flag ~50x/s, so
|
|
1181
|
+
before the signal existed a late joiner was spliced onto audio and never
|
|
1182
|
+
received a decodable picture.
|
|
1183
|
+
"""
|
|
1184
|
+
if self._kf_ever:
|
|
1185
|
+
return False # this source signals; wait for the real thing
|
|
1186
|
+
if not self._is_random_access(pkt):
|
|
1187
|
+
return False
|
|
1188
|
+
now = time.monotonic()
|
|
1189
|
+
if self._waiting_since is None:
|
|
1190
|
+
self._waiting_since = now
|
|
1191
|
+
return False
|
|
1192
|
+
return (now - self._waiting_since) >= self._LEGACY_SYNC_AFTER_S
|
|
1193
|
+
|
|
1144
1194
|
def write(self, b: bytes) -> int:
|
|
1145
1195
|
data = self._tail + b if self._tail else b
|
|
1146
1196
|
out = bytearray()
|
|
@@ -1164,7 +1214,11 @@ class _DirectTsServer:
|
|
|
1164
1214
|
self._pmt = pkt
|
|
1165
1215
|
if self._synced:
|
|
1166
1216
|
out += pkt
|
|
1167
|
-
elif self.
|
|
1217
|
+
elif self._kf_pending or self._legacy_sync_ok(pkt):
|
|
1218
|
+
_LOGGER.debug(
|
|
1219
|
+
"direct-ts: spliced a consumer via=%s signals=%s pid=%s",
|
|
1220
|
+
"keyframe-signal" if self._kf_pending else "legacy-rai",
|
|
1221
|
+
self._kf_ever, pid)
|
|
1168
1222
|
# Start this consumer here: tables first, then the keyframe.
|
|
1169
1223
|
if self._pat is not None:
|
|
1170
1224
|
out += self._pat
|
|
@@ -1172,6 +1226,8 @@ class _DirectTsServer:
|
|
|
1172
1226
|
out += self._pmt
|
|
1173
1227
|
out += pkt
|
|
1174
1228
|
self._synced = True
|
|
1229
|
+
self._kf_pending = False
|
|
1230
|
+
self._waiting_since = None
|
|
1175
1231
|
self._tail = data[i:] if i < n else b""
|
|
1176
1232
|
|
|
1177
1233
|
with self._lock:
|
|
@@ -1568,6 +1624,16 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1568
1624
|
def close(self):
|
|
1569
1625
|
self._f.close()
|
|
1570
1626
|
|
|
1627
|
+
def mark_keyframe(self):
|
|
1628
|
+
"""Forward the keyframe signal when the sink understands it.
|
|
1629
|
+
|
|
1630
|
+
The sink is a _DirectTsServer on the direct path and an os pipe to
|
|
1631
|
+
ffmpeg otherwise; only the former can use this.
|
|
1632
|
+
"""
|
|
1633
|
+
fn = getattr(self._f, "mark_keyframe", None)
|
|
1634
|
+
if fn is not None:
|
|
1635
|
+
fn()
|
|
1636
|
+
|
|
1571
1637
|
wsink = _FailFlagWriter(out_fileobj)
|
|
1572
1638
|
try:
|
|
1573
1639
|
# max_interleave_delta (default 10s of stream time): with an AAC stream
|
|
@@ -1659,6 +1725,10 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1659
1725
|
pkt.stream = vs
|
|
1660
1726
|
pkt.pts, pkt.dts = video_pts_dts(_ts_state, _rel, _slack)
|
|
1661
1727
|
pkt.time_base = Fraction(1, 90000)
|
|
1728
|
+
if kf:
|
|
1729
|
+
# Announce it BEFORE muxing: the bytes this produces are where a
|
|
1730
|
+
# new consumer can safely be spliced, and only we know that.
|
|
1731
|
+
wsink.mark_keyframe()
|
|
1662
1732
|
try:
|
|
1663
1733
|
out.mux(pkt)
|
|
1664
1734
|
progress[0] = _t.monotonic()
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc4}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -108,6 +108,70 @@ def _sctp_reassemble(flags: int, stream_id: int, payload: bytes,
|
|
|
108
108
|
return None
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
async def _await_rtsp_publish_target(url: "Optional[str]", *,
|
|
112
|
+
timeout: float = 20.0) -> bool:
|
|
113
|
+
"""Wait until an ``rtsp://host:port`` target accepts a connection.
|
|
114
|
+
|
|
115
|
+
Publishing into go2rtc is a race after a Home Assistant restart: the serve
|
|
116
|
+
ffmpeg is launched as soon as media arrives, and go2rtc's RTSP listener may
|
|
117
|
+
not be back yet. ffmpeg then fails its very first connect with
|
|
118
|
+
``Connection refused``, reports ``Could not write header (incorrect codec
|
|
119
|
+
parameters ?)``, and exits 145 -- which reads as "stream ended" and takes
|
|
120
|
+
that camera's view down until something retries.
|
|
121
|
+
|
|
122
|
+
Measured on a live box: every occurrence of that failure sat inside a
|
|
123
|
+
one-minute window around an HA restart (11:32, 11:43, 11:49, 11:52, 11:54,
|
|
124
|
+
11:56, 11:57 on 2026-09-01, and the same shape the night before), and none
|
|
125
|
+
in steady state.
|
|
126
|
+
|
|
127
|
+
Returns True when the port accepts, False on timeout. False is not fatal --
|
|
128
|
+
the caller launches ffmpeg anyway, because a target that never opens is a
|
|
129
|
+
different problem and should surface as ffmpeg's own error rather than as a
|
|
130
|
+
silent refusal to start.
|
|
131
|
+
"""
|
|
132
|
+
if not url or not url.startswith("rtsp://"):
|
|
133
|
+
return True
|
|
134
|
+
try:
|
|
135
|
+
rest = url[len("rtsp://"):]
|
|
136
|
+
hostport = rest.split("/", 1)[0]
|
|
137
|
+
if "@" in hostport:
|
|
138
|
+
hostport = hostport.rsplit("@", 1)[1]
|
|
139
|
+
if hostport.startswith("["): # IPv6 literal
|
|
140
|
+
if "]" not in hostport:
|
|
141
|
+
return True # malformed: do not block
|
|
142
|
+
host, _, tail = hostport[1:].partition("]")
|
|
143
|
+
port = int(tail.lstrip(":") or 554)
|
|
144
|
+
else:
|
|
145
|
+
host, _, port_s = hostport.partition(":")
|
|
146
|
+
port = int(port_s or 554)
|
|
147
|
+
except (ValueError, IndexError):
|
|
148
|
+
return True # unparseable: do not block
|
|
149
|
+
|
|
150
|
+
_deadline = time.monotonic() + timeout
|
|
151
|
+
_attempt = 0
|
|
152
|
+
while time.monotonic() < _deadline:
|
|
153
|
+
_attempt += 1
|
|
154
|
+
try:
|
|
155
|
+
_r, _w = await asyncio.wait_for(
|
|
156
|
+
asyncio.open_connection(host, port), timeout=3.0)
|
|
157
|
+
_w.close()
|
|
158
|
+
try:
|
|
159
|
+
await _w.wait_closed()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
if _attempt > 1:
|
|
163
|
+
_LOGGER.info(
|
|
164
|
+
"publish target %s:%s accepted after %d attempts",
|
|
165
|
+
host, port, _attempt)
|
|
166
|
+
return True
|
|
167
|
+
except (OSError, TimeoutError):
|
|
168
|
+
await asyncio.sleep(0.5)
|
|
169
|
+
_LOGGER.warning(
|
|
170
|
+
"publish target %s:%s did not accept a connection within %.0fs - "
|
|
171
|
+
"launching ffmpeg anyway so its own error surfaces", host, port, timeout)
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
111
175
|
def _sctp_sack_chunk(cum_tsn: int, a_rwnd: int = 131072) -> bytes:
|
|
112
176
|
"""An SCTP SACK (RFC 4960 s3.3.4) with no gap blocks and no duplicates.
|
|
113
177
|
|
|
@@ -6820,6 +6884,8 @@ class _SdesOpenMixin:
|
|
|
6820
6884
|
if _keep_a is not None else ""))
|
|
6821
6885
|
except Exception:
|
|
6822
6886
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_open_sdes_stream', exc_info=True)
|
|
6887
|
+
# Do not launch the publisher into a listener that is not up yet.
|
|
6888
|
+
await _await_rtsp_publish_target(rtsp_push_url)
|
|
6823
6889
|
_LOGGER.info("SDES ffmpeg cmd: %s", " ".join(cmd))
|
|
6824
6890
|
if _ffmpeg_path() is None:
|
|
6825
6891
|
# ffmpeg is not installed - clean up and surface a clear error
|
|
@@ -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.0rc4"
|
|
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.0rc4
|
|
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
|
|
@@ -418,6 +418,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
418
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`) |
|
|
419
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) |
|
|
420
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) |
|
|
421
|
+
| `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
|
|
421
422
|
| `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) |
|
|
422
423
|
| `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) |
|
|
423
424
|
| `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`) |
|
|
@@ -93,6 +93,7 @@ tests/test_bitrate_campaign_arms.py
|
|
|
93
93
|
tests/test_ble_gateway.py
|
|
94
94
|
tests/test_bridge_state_sentinels.py
|
|
95
95
|
tests/test_busy_backoff.py
|
|
96
|
+
tests/test_camera_properties.py
|
|
96
97
|
tests/test_canary_log_names_the_camera.py
|
|
97
98
|
tests/test_cloud_count.py
|
|
98
99
|
tests/test_cloud_plan.py
|
|
@@ -102,6 +103,7 @@ tests/test_deferred_hardening.py
|
|
|
102
103
|
tests/test_device_cmd_seq_match.py
|
|
103
104
|
tests/test_device_login_guard.py
|
|
104
105
|
tests/test_device_user_info_cache.py
|
|
106
|
+
tests/test_direct_ts_keyframe_signal.py
|
|
105
107
|
tests/test_direct_ts_serve.py
|
|
106
108
|
tests/test_dormant_camera_cleanup.py
|
|
107
109
|
tests/test_drain_is_cancellable.py
|
|
@@ -177,7 +179,9 @@ tests/test_playback_tls.py
|
|
|
177
179
|
tests/test_post_merge_hardening.py
|
|
178
180
|
tests/test_ptz_probe_reads_what_it_is_told.py
|
|
179
181
|
tests/test_public_signature_matches_impl.py
|
|
182
|
+
tests/test_publish_target_readiness.py
|
|
180
183
|
tests/test_quality_campaign_in_session.py
|
|
184
|
+
tests/test_reboot_action.py
|
|
181
185
|
tests/test_recent_recordings.py
|
|
182
186
|
tests/test_remb.py
|
|
183
187
|
tests/test_remb_goes_out_the_relay_aware_sender.py
|