python-aidot-cameras 1.0.0rc5__tar.gz → 1.0.0rc7__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.0rc5/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc7}/PKG-INFO +169 -17
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/README.md +168 -16
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/__main__.py +48 -15
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/client.py +2 -2
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sdes_open.py +229 -6
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/webrtc_open.py +121 -21
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7/python_aidot_cameras.egg-info}/PKG-INFO +169 -17
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/SOURCES.txt +4 -0
- python_aidot_cameras-1.0.0rc7/tests/test_battery_stale_offer_is_abandoned_early.py +150 -0
- python_aidot_cameras-1.0.0rc7/tests/test_battery_wake_gate_app_parity.py +89 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_go2rtc_cli.py +57 -7
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_liveplay_echo_timeout.py +11 -5
- python_aidot_cameras-1.0.0rc7/tests/test_off_subnet_candidate.py +68 -0
- python_aidot_cameras-1.0.0rc7/tests/test_signalling_is_attributable_to_a_camera.py +150 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/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.0rc7
|
|
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
|
|
@@ -200,9 +200,40 @@ pip install "python-aidot-cameras[webrtc] @ git+https://github.com/cbrightly/pyt
|
|
|
200
200
|
|
|
201
201
|
## Standalone CLI: `aidot-go2rtc`
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
### What it does
|
|
204
|
+
|
|
205
|
+
These cameras do not speak RTSP. They stream over WebRTC, negotiated through
|
|
206
|
+
AiDot's cloud with a per-session key exchange, which is why you cannot simply
|
|
207
|
+
point VLC or Frigate at one and get a picture.
|
|
208
|
+
|
|
209
|
+
`aidot-go2rtc` does that negotiation for you and hands the result out as
|
|
210
|
+
something ordinary: an MPEG-TS stream on stdout, an HTTP endpoint, or an RTSP
|
|
211
|
+
push into [go2rtc](https://github.com/AlexxIT/go2rtc). One camera per process.
|
|
212
|
+
From there anything that reads RTSP or TS can consume it.
|
|
213
|
+
|
|
214
|
+
### Why you would use it
|
|
215
|
+
|
|
216
|
+
- **You want these cameras in something that is not Home Assistant** -- Frigate,
|
|
217
|
+
a standalone go2rtc, an NVR, a browser, `ffmpeg` writing to disk.
|
|
218
|
+
- **You want the streaming stack outside Home Assistant's Python environment,**
|
|
219
|
+
so its dependencies cannot collide with Home Assistant's pins.
|
|
220
|
+
- **You are debugging.** `--list` shows every camera and which transport it uses,
|
|
221
|
+
and a single camera can be streamed in isolation with the env knobs below,
|
|
222
|
+
which is far easier to reason about than the same thing inside Home Assistant.
|
|
223
|
+
|
|
224
|
+
### When you do not need it
|
|
225
|
+
|
|
226
|
+
**If you use Home Assistant, install the integration instead** -- it does all of
|
|
227
|
+
this for you, and running both at once means two clients competing for the same
|
|
228
|
+
camera. A camera serves a limited number of concurrent viewers and holds a slot
|
|
229
|
+
for about 120 s after one leaves, so the loser gets a clean handshake and then no
|
|
230
|
+
media at all. The limit belongs to the device, so a second AiDot account with the
|
|
231
|
+
house shared to it does not avoid it.
|
|
232
|
+
|
|
233
|
+
### Installing
|
|
234
|
+
|
|
235
|
+
Installing the package provides the `aidot-go2rtc` console script; for an
|
|
236
|
+
isolated tool install use pipx or uv:
|
|
206
237
|
|
|
207
238
|
```bash
|
|
208
239
|
pipx install "python-aidot-cameras[webrtc]"
|
|
@@ -212,16 +243,48 @@ uv tool install "python-aidot-cameras[webrtc]"
|
|
|
212
243
|
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
213
244
|
aidot-go2rtc --list # discover cameras + their transport
|
|
214
245
|
|
|
215
|
-
# Stream one camera.
|
|
216
|
-
aidot-go2rtc <device_id> - #
|
|
217
|
-
aidot-go2rtc <device_id> '{output}' #
|
|
218
|
-
aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts #
|
|
246
|
+
# Stream one camera. The second argument is WHERE the media should go:
|
|
247
|
+
aidot-go2rtc <device_id> - # to stdout
|
|
248
|
+
aidot-go2rtc <device_id> '{output}' # go2rtc fills this in
|
|
249
|
+
aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull
|
|
219
250
|
```
|
|
220
251
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
252
|
+
**Which form to use.** `--list` tells you each camera's transport, and that
|
|
253
|
+
decides it:
|
|
254
|
+
|
|
255
|
+
| your camera | use | why |
|
|
256
|
+
| --- | --- | --- |
|
|
257
|
+
| **DTLS** (mains, e.g. A000088) | `-` | keeps the mux's 48 kHz AAC. The push path has to transcode audio down to 8 kHz G.711 |
|
|
258
|
+
| **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
|
|
259
|
+
| either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
|
|
260
|
+
|
|
261
|
+
`'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
|
|
262
|
+
URL when it launches the process from an `exec:` source -- quote it so your
|
|
263
|
+
shell does not eat the braces. It is not something to type at a shell: run by
|
|
264
|
+
hand the literal text is taken as a publish target and the push fails. To try
|
|
265
|
+
the push path by hand, put a real `rtsp://` URL there instead. Both transports
|
|
266
|
+
can push, so `{output}` works on any camera; the table is about which gives the
|
|
267
|
+
better result, not what is possible.
|
|
268
|
+
|
|
269
|
+
### Wiring it into go2rtc
|
|
270
|
+
|
|
271
|
+
`--list` prints a paste-ready `streams:` block for every camera on the account,
|
|
272
|
+
so you rarely have to write this by hand. It looks like this:
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
streams:
|
|
276
|
+
front_door: exec:aidot-go2rtc 1234abcd... {output}
|
|
277
|
+
driveway: exec:aidot-go2rtc 5678efgh... -
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
go2rtc launches one process per stream, on demand, and substitutes `{output}`
|
|
281
|
+
with that stream's push URL.
|
|
282
|
+
|
|
283
|
+
The process needs the same `AIDOT_*` environment variables you used above. It is
|
|
284
|
+
launched as a child of go2rtc, so the practical answer is to put them where
|
|
285
|
+
go2rtc itself gets its environment: export them in the shell before starting it,
|
|
286
|
+
or set them in its service unit or container definition. If a stream fails
|
|
287
|
+
immediately with an authentication error, that is the thing to check first.
|
|
225
288
|
|
|
226
289
|
**ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
|
|
227
290
|
stream entirely through an ffmpeg subprocess, and pip cannot install a system
|
|
@@ -299,6 +362,91 @@ await session.async_stop_talk()
|
|
|
299
362
|
See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
|
|
300
363
|
snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
|
|
301
364
|
|
|
365
|
+
## Camera controls and state
|
|
366
|
+
|
|
367
|
+
Controls are on the camera device client. Writes are confirmed by reading the
|
|
368
|
+
value back on each model that offers them, because this firmware acknowledges
|
|
369
|
+
writes it then ignores.
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
await device_client.async_set_motion_detection(True)
|
|
373
|
+
await device_client.async_set_night_vision("auto") # auto | on | off
|
|
374
|
+
await device_client.async_set_speaker_volume(50)
|
|
375
|
+
await device_client.async_reboot() # see below
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Reboot
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
sent = await device_client.async_reboot()
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`async_reboot` sends the same `devActionReq({action: "RebootFunc", in: []})` the
|
|
385
|
+
vendor app's camera settings page sends. Two things to know:
|
|
386
|
+
|
|
387
|
+
* **It reports that the request was SENT, not that the camera rebooted.** A
|
|
388
|
+
reboot succeeds by taking the camera off the network, so waiting for an
|
|
389
|
+
acknowledgement would report a working reboot as a failure. Do not read
|
|
390
|
+
`True` as "it came back".
|
|
391
|
+
* **It refuses when the cloud explicitly reports the camera offline**, mirroring
|
|
392
|
+
the app, which only offers the button on a reachable device.
|
|
393
|
+
|
|
394
|
+
The camera is away for a few seconds afterwards, during which a viewer sees a
|
|
395
|
+
session that connects and delivers nothing.
|
|
396
|
+
|
|
397
|
+
### Sound detection
|
|
398
|
+
|
|
399
|
+
The cameras can listen for glass breaking, a smoke alarm, a baby crying and a
|
|
400
|
+
dog barking.
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
flags = await device_client.async_get_sound_detection()
|
|
404
|
+
# {'sound_enable': False, 'all_sound': True, 'glass_Break': False,
|
|
405
|
+
# 'smoke_T3': False, 'smoke_T4': False, 'baby_cry': False}
|
|
406
|
+
|
|
407
|
+
await device_client.async_set_sound_detection("glass_Break", True)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The keys come from the camera, not from a fixed table, so a model reporting a
|
|
411
|
+
detector not listed above still works. A write is read-modify-write: the current
|
|
412
|
+
list is fetched and sent back with one flag changed, rather than against a
|
|
413
|
+
payload schema invented here.
|
|
414
|
+
|
|
415
|
+
**`None` means unknown, never "all off".** A camera that does not answer -- a
|
|
416
|
+
battery camera asleep, or a model without the feature -- returns `None`, and
|
|
417
|
+
treating that as a set of disabled detectors would report a state the camera
|
|
418
|
+
never claimed. A write refuses outright when there is no current list to echo.
|
|
419
|
+
|
|
420
|
+
### WiFi and SD card
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
await device_client.async_get_wifi_info() # {'ssid': ..., 'rssi': 63}
|
|
424
|
+
await device_client.async_get_sd_card_info() # {'present': True, 'total': 29838,
|
|
425
|
+
# 'used': 28848, 'raw': [...]}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Only confirmed fields are named. `SDcardBaseInfo` answers positionally and its
|
|
429
|
+
fourth value was nearly called "free" -- but on a card reading 29838 total and
|
|
430
|
+
28848 used it returns 3, which is not a remainder (990) and looks like a
|
|
431
|
+
percentage. It stays unnamed in `raw` rather than shipping a field that reports
|
|
432
|
+
3 MB free on a 30 GB card. The units of `total` and `used` are unconfirmed too,
|
|
433
|
+
so the numbers pass through exactly as the camera reports them.
|
|
434
|
+
|
|
435
|
+
### Reading anything else the camera answers
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
out = await device_client.async_query_device_action("getWorkMode")
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`async_query_device_action` sends a read-only `devActionReq` and returns the
|
|
442
|
+
camera's `out` payload. The cameras answer more actions than this library wraps
|
|
443
|
+
-- `getWorkMode`, `getAutoAlarm`, `getRoiHuman` (human/vehicle/package
|
|
444
|
+
detection), `getRoiPrivacy` and others. `None` means no reply, which is not the
|
|
445
|
+
same as "unsupported".
|
|
446
|
+
|
|
447
|
+
**Do not use it to send actions that change something** unless you know the
|
|
448
|
+
payload shape. `SDcardFormatFunc` is one of the actions the cameras accept.
|
|
449
|
+
|
|
302
450
|
## Getting an RTSP URL
|
|
303
451
|
|
|
304
452
|
Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
|
|
@@ -317,7 +465,7 @@ keep the output around:
|
|
|
317
465
|
$ pip install "python-aidot-cameras[webrtc]"
|
|
318
466
|
$ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
|
|
319
467
|
$ aidot-go2rtc --list
|
|
320
|
-
1a2b3c4d... LK.IPC.A000088 DTLS (
|
|
468
|
+
1a2b3c4d... LK.IPC.A000088 DTLS (stdout)
|
|
321
469
|
5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
|
|
322
470
|
```
|
|
323
471
|
|
|
@@ -356,9 +504,10 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
|
356
504
|
- **Which camera is which.** `--list` reports the transport per device. DTLS is
|
|
357
505
|
the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
|
|
358
506
|
form is not silent - the CLI tells you which one to use and exits.
|
|
359
|
-
- **Audio.**
|
|
360
|
-
|
|
361
|
-
|
|
507
|
+
- **Audio.** Both forms carry AAC at 48 kHz mono, resampled from the camera's
|
|
508
|
+
8 kHz A-law, because 8 kHz AAC plays silent in a lot of browsers and browsers
|
|
509
|
+
on the MSE path have no mapping for G.711 at all. Audio is on by default; see
|
|
510
|
+
`AIDOT_SDES_SERVE_AUDIO` to turn it off.
|
|
362
511
|
- **A stream that starts with no video** and picks it up a few seconds later is
|
|
363
512
|
normal: the mux waits for a keyframe so the first GOP is decodable. A stream
|
|
364
513
|
that stays audio-only is a camera that never sent one - retry the view.
|
|
@@ -418,7 +567,10 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
418
567
|
| `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
568
|
| `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
569
|
| `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).
|
|
570
|
+
| `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). It is pure latency: across 22 h of one deployment the wait 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. The fast-liveplay path kept 1.5 s until it was measured on its own - 22 runs, 22 timeouts at 1500-1501 ms, no echo ever - and now shares the same value; that was 1.25 s of a 5.4 s cold connect. Still honoured, so a broker that does echo short-circuits it. | 0.25 s |
|
|
571
|
+
| `AIDOT_BATTERY_WAKE_GATE_S` | Hold a **battery** camera's SDP offer until the camera itself answers, in seconds. **Measured and shipped off (`0`).** This is the official client's own shape - its live view fires the keep-alive, sends the wake, and shows a sleeping placeholder rather than opening a session until the device reports itself awake - and on this camera family it is self-defeating, because the live-play signalling is what wakes the camera: withholding the offer withholds the wake. At a 20 s budget, on a camera settled for ten minutes, the gate ran its whole budget with the camera silent, the offer went out at +20.9 s, the camera's own `wakeupStatus` arrived at +23.6 s (after the offer), and first media at +27.0 s - against 5.4-10.0 s on the same camera with no gate. Kept as a lever for anyone re-testing on other firmware. | `0` (off) |
|
|
572
|
+
| `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
|
|
573
|
+
| `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
|
|
422
574
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
423
575
|
| `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) |
|
|
424
576
|
| `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`) |
|
|
@@ -160,9 +160,40 @@ pip install "python-aidot-cameras[webrtc] @ git+https://github.com/cbrightly/pyt
|
|
|
160
160
|
|
|
161
161
|
## Standalone CLI: `aidot-go2rtc`
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
### What it does
|
|
164
|
+
|
|
165
|
+
These cameras do not speak RTSP. They stream over WebRTC, negotiated through
|
|
166
|
+
AiDot's cloud with a per-session key exchange, which is why you cannot simply
|
|
167
|
+
point VLC or Frigate at one and get a picture.
|
|
168
|
+
|
|
169
|
+
`aidot-go2rtc` does that negotiation for you and hands the result out as
|
|
170
|
+
something ordinary: an MPEG-TS stream on stdout, an HTTP endpoint, or an RTSP
|
|
171
|
+
push into [go2rtc](https://github.com/AlexxIT/go2rtc). One camera per process.
|
|
172
|
+
From there anything that reads RTSP or TS can consume it.
|
|
173
|
+
|
|
174
|
+
### Why you would use it
|
|
175
|
+
|
|
176
|
+
- **You want these cameras in something that is not Home Assistant** -- Frigate,
|
|
177
|
+
a standalone go2rtc, an NVR, a browser, `ffmpeg` writing to disk.
|
|
178
|
+
- **You want the streaming stack outside Home Assistant's Python environment,**
|
|
179
|
+
so its dependencies cannot collide with Home Assistant's pins.
|
|
180
|
+
- **You are debugging.** `--list` shows every camera and which transport it uses,
|
|
181
|
+
and a single camera can be streamed in isolation with the env knobs below,
|
|
182
|
+
which is far easier to reason about than the same thing inside Home Assistant.
|
|
183
|
+
|
|
184
|
+
### When you do not need it
|
|
185
|
+
|
|
186
|
+
**If you use Home Assistant, install the integration instead** -- it does all of
|
|
187
|
+
this for you, and running both at once means two clients competing for the same
|
|
188
|
+
camera. A camera serves a limited number of concurrent viewers and holds a slot
|
|
189
|
+
for about 120 s after one leaves, so the loser gets a clean handshake and then no
|
|
190
|
+
media at all. The limit belongs to the device, so a second AiDot account with the
|
|
191
|
+
house shared to it does not avoid it.
|
|
192
|
+
|
|
193
|
+
### Installing
|
|
194
|
+
|
|
195
|
+
Installing the package provides the `aidot-go2rtc` console script; for an
|
|
196
|
+
isolated tool install use pipx or uv:
|
|
166
197
|
|
|
167
198
|
```bash
|
|
168
199
|
pipx install "python-aidot-cameras[webrtc]"
|
|
@@ -172,16 +203,48 @@ uv tool install "python-aidot-cameras[webrtc]"
|
|
|
172
203
|
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
173
204
|
aidot-go2rtc --list # discover cameras + their transport
|
|
174
205
|
|
|
175
|
-
# Stream one camera.
|
|
176
|
-
aidot-go2rtc <device_id> - #
|
|
177
|
-
aidot-go2rtc <device_id> '{output}' #
|
|
178
|
-
aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts #
|
|
206
|
+
# Stream one camera. The second argument is WHERE the media should go:
|
|
207
|
+
aidot-go2rtc <device_id> - # to stdout
|
|
208
|
+
aidot-go2rtc <device_id> '{output}' # go2rtc fills this in
|
|
209
|
+
aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull
|
|
179
210
|
```
|
|
180
211
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
**Which form to use.** `--list` tells you each camera's transport, and that
|
|
213
|
+
decides it:
|
|
214
|
+
|
|
215
|
+
| your camera | use | why |
|
|
216
|
+
| --- | --- | --- |
|
|
217
|
+
| **DTLS** (mains, e.g. A000088) | `-` | keeps the mux's 48 kHz AAC. The push path has to transcode audio down to 8 kHz G.711 |
|
|
218
|
+
| **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
|
|
219
|
+
| either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
|
|
220
|
+
|
|
221
|
+
`'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
|
|
222
|
+
URL when it launches the process from an `exec:` source -- quote it so your
|
|
223
|
+
shell does not eat the braces. It is not something to type at a shell: run by
|
|
224
|
+
hand the literal text is taken as a publish target and the push fails. To try
|
|
225
|
+
the push path by hand, put a real `rtsp://` URL there instead. Both transports
|
|
226
|
+
can push, so `{output}` works on any camera; the table is about which gives the
|
|
227
|
+
better result, not what is possible.
|
|
228
|
+
|
|
229
|
+
### Wiring it into go2rtc
|
|
230
|
+
|
|
231
|
+
`--list` prints a paste-ready `streams:` block for every camera on the account,
|
|
232
|
+
so you rarely have to write this by hand. It looks like this:
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
streams:
|
|
236
|
+
front_door: exec:aidot-go2rtc 1234abcd... {output}
|
|
237
|
+
driveway: exec:aidot-go2rtc 5678efgh... -
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
go2rtc launches one process per stream, on demand, and substitutes `{output}`
|
|
241
|
+
with that stream's push URL.
|
|
242
|
+
|
|
243
|
+
The process needs the same `AIDOT_*` environment variables you used above. It is
|
|
244
|
+
launched as a child of go2rtc, so the practical answer is to put them where
|
|
245
|
+
go2rtc itself gets its environment: export them in the shell before starting it,
|
|
246
|
+
or set them in its service unit or container definition. If a stream fails
|
|
247
|
+
immediately with an authentication error, that is the thing to check first.
|
|
185
248
|
|
|
186
249
|
**ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
|
|
187
250
|
stream entirely through an ffmpeg subprocess, and pip cannot install a system
|
|
@@ -259,6 +322,91 @@ await session.async_stop_talk()
|
|
|
259
322
|
See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
|
|
260
323
|
snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
|
|
261
324
|
|
|
325
|
+
## Camera controls and state
|
|
326
|
+
|
|
327
|
+
Controls are on the camera device client. Writes are confirmed by reading the
|
|
328
|
+
value back on each model that offers them, because this firmware acknowledges
|
|
329
|
+
writes it then ignores.
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
await device_client.async_set_motion_detection(True)
|
|
333
|
+
await device_client.async_set_night_vision("auto") # auto | on | off
|
|
334
|
+
await device_client.async_set_speaker_volume(50)
|
|
335
|
+
await device_client.async_reboot() # see below
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Reboot
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
sent = await device_client.async_reboot()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`async_reboot` sends the same `devActionReq({action: "RebootFunc", in: []})` the
|
|
345
|
+
vendor app's camera settings page sends. Two things to know:
|
|
346
|
+
|
|
347
|
+
* **It reports that the request was SENT, not that the camera rebooted.** A
|
|
348
|
+
reboot succeeds by taking the camera off the network, so waiting for an
|
|
349
|
+
acknowledgement would report a working reboot as a failure. Do not read
|
|
350
|
+
`True` as "it came back".
|
|
351
|
+
* **It refuses when the cloud explicitly reports the camera offline**, mirroring
|
|
352
|
+
the app, which only offers the button on a reachable device.
|
|
353
|
+
|
|
354
|
+
The camera is away for a few seconds afterwards, during which a viewer sees a
|
|
355
|
+
session that connects and delivers nothing.
|
|
356
|
+
|
|
357
|
+
### Sound detection
|
|
358
|
+
|
|
359
|
+
The cameras can listen for glass breaking, a smoke alarm, a baby crying and a
|
|
360
|
+
dog barking.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
flags = await device_client.async_get_sound_detection()
|
|
364
|
+
# {'sound_enable': False, 'all_sound': True, 'glass_Break': False,
|
|
365
|
+
# 'smoke_T3': False, 'smoke_T4': False, 'baby_cry': False}
|
|
366
|
+
|
|
367
|
+
await device_client.async_set_sound_detection("glass_Break", True)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The keys come from the camera, not from a fixed table, so a model reporting a
|
|
371
|
+
detector not listed above still works. A write is read-modify-write: the current
|
|
372
|
+
list is fetched and sent back with one flag changed, rather than against a
|
|
373
|
+
payload schema invented here.
|
|
374
|
+
|
|
375
|
+
**`None` means unknown, never "all off".** A camera that does not answer -- a
|
|
376
|
+
battery camera asleep, or a model without the feature -- returns `None`, and
|
|
377
|
+
treating that as a set of disabled detectors would report a state the camera
|
|
378
|
+
never claimed. A write refuses outright when there is no current list to echo.
|
|
379
|
+
|
|
380
|
+
### WiFi and SD card
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
await device_client.async_get_wifi_info() # {'ssid': ..., 'rssi': 63}
|
|
384
|
+
await device_client.async_get_sd_card_info() # {'present': True, 'total': 29838,
|
|
385
|
+
# 'used': 28848, 'raw': [...]}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Only confirmed fields are named. `SDcardBaseInfo` answers positionally and its
|
|
389
|
+
fourth value was nearly called "free" -- but on a card reading 29838 total and
|
|
390
|
+
28848 used it returns 3, which is not a remainder (990) and looks like a
|
|
391
|
+
percentage. It stays unnamed in `raw` rather than shipping a field that reports
|
|
392
|
+
3 MB free on a 30 GB card. The units of `total` and `used` are unconfirmed too,
|
|
393
|
+
so the numbers pass through exactly as the camera reports them.
|
|
394
|
+
|
|
395
|
+
### Reading anything else the camera answers
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
out = await device_client.async_query_device_action("getWorkMode")
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`async_query_device_action` sends a read-only `devActionReq` and returns the
|
|
402
|
+
camera's `out` payload. The cameras answer more actions than this library wraps
|
|
403
|
+
-- `getWorkMode`, `getAutoAlarm`, `getRoiHuman` (human/vehicle/package
|
|
404
|
+
detection), `getRoiPrivacy` and others. `None` means no reply, which is not the
|
|
405
|
+
same as "unsupported".
|
|
406
|
+
|
|
407
|
+
**Do not use it to send actions that change something** unless you know the
|
|
408
|
+
payload shape. `SDcardFormatFunc` is one of the actions the cameras accept.
|
|
409
|
+
|
|
262
410
|
## Getting an RTSP URL
|
|
263
411
|
|
|
264
412
|
Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
|
|
@@ -277,7 +425,7 @@ keep the output around:
|
|
|
277
425
|
$ pip install "python-aidot-cameras[webrtc]"
|
|
278
426
|
$ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
|
|
279
427
|
$ aidot-go2rtc --list
|
|
280
|
-
1a2b3c4d... LK.IPC.A000088 DTLS (
|
|
428
|
+
1a2b3c4d... LK.IPC.A000088 DTLS (stdout)
|
|
281
429
|
5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
|
|
282
430
|
```
|
|
283
431
|
|
|
@@ -316,9 +464,10 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
|
316
464
|
- **Which camera is which.** `--list` reports the transport per device. DTLS is
|
|
317
465
|
the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
|
|
318
466
|
form is not silent - the CLI tells you which one to use and exits.
|
|
319
|
-
- **Audio.**
|
|
320
|
-
|
|
321
|
-
|
|
467
|
+
- **Audio.** Both forms carry AAC at 48 kHz mono, resampled from the camera's
|
|
468
|
+
8 kHz A-law, because 8 kHz AAC plays silent in a lot of browsers and browsers
|
|
469
|
+
on the MSE path have no mapping for G.711 at all. Audio is on by default; see
|
|
470
|
+
`AIDOT_SDES_SERVE_AUDIO` to turn it off.
|
|
322
471
|
- **A stream that starts with no video** and picks it up a few seconds later is
|
|
323
472
|
normal: the mux waits for a keyframe so the first GOP is decodable. A stream
|
|
324
473
|
that stays audio-only is a camera that never sent one - retry the view.
|
|
@@ -378,7 +527,10 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
378
527
|
| `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
528
|
| `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
529
|
| `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).
|
|
530
|
+
| `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). It is pure latency: across 22 h of one deployment the wait 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. The fast-liveplay path kept 1.5 s until it was measured on its own - 22 runs, 22 timeouts at 1500-1501 ms, no echo ever - and now shares the same value; that was 1.25 s of a 5.4 s cold connect. Still honoured, so a broker that does echo short-circuits it. | 0.25 s |
|
|
531
|
+
| `AIDOT_BATTERY_WAKE_GATE_S` | Hold a **battery** camera's SDP offer until the camera itself answers, in seconds. **Measured and shipped off (`0`).** This is the official client's own shape - its live view fires the keep-alive, sends the wake, and shows a sleeping placeholder rather than opening a session until the device reports itself awake - and on this camera family it is self-defeating, because the live-play signalling is what wakes the camera: withholding the offer withholds the wake. At a 20 s budget, on a camera settled for ten minutes, the gate ran its whole budget with the camera silent, the offer went out at +20.9 s, the camera's own `wakeupStatus` arrived at +23.6 s (after the offer), and first media at +27.0 s - against 5.4-10.0 s on the same camera with no gate. Kept as a lever for anyone re-testing on other firmware. | `0` (off) |
|
|
532
|
+
| `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
|
|
533
|
+
| `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
|
|
382
534
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
383
535
|
| `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) |
|
|
384
536
|
| `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`) |
|
|
@@ -39,18 +39,20 @@ Authentication (in precedence order):
|
|
|
39
39
|
Optional stream knobs:
|
|
40
40
|
|
|
41
41
|
AIDOT_FAST_CONNECT=1 LAN-direct mode (skip TURN relay; same-subnet only)
|
|
42
|
-
AIDOT_SDES_SERVE_AUDIO=
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
AIDOT_SDES_SERVE_AUDIO=0 audio is ON by default on both SDES output paths
|
|
43
|
+
(app parity). Set 0/false/no/off for video only,
|
|
44
|
+
which is what a consumer that cannot cope with the
|
|
45
|
+
audio wants. Both paths encode AAC 48 kHz.
|
|
45
46
|
|
|
46
47
|
go2rtc.yaml example (SDES camera - native RTSP push, video + audio):
|
|
47
48
|
|
|
48
49
|
streams:
|
|
49
50
|
frontdoor: exec:aidot-go2rtc 0a1b2c3d... {output}
|
|
50
51
|
|
|
51
|
-
A DTLS camera (A000088)
|
|
52
|
-
same lazy way. Pass ``-`` as the output and this
|
|
53
|
-
plus AAC audio) to stdout, which go2rtc's exec:
|
|
52
|
+
A DTLS camera (A000088) can push RTSP too, but a stdout producer is the better
|
|
53
|
+
default for it and works the same lazy way. Pass ``-`` as the output and this
|
|
54
|
+
process writes MPEG-TS (video plus AAC audio) to stdout, which go2rtc's exec:
|
|
55
|
+
source reads directly:
|
|
54
56
|
|
|
55
57
|
streams:
|
|
56
58
|
garage: exec:aidot-go2rtc 0a1b2c3d... -
|
|
@@ -89,11 +91,39 @@ _LOGGER = logging.getLogger("aidot.go2rtc")
|
|
|
89
91
|
|
|
90
92
|
|
|
91
93
|
def _env_bool(name: str) -> bool | None:
|
|
92
|
-
"""Tri-state env flag:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
"""Tri-state env flag: truthy -> True, falsy -> False, anything else -> None.
|
|
95
|
+
|
|
96
|
+
``None`` is the whole point, and it is not timidity. A non-``None`` here is
|
|
97
|
+
passed to ``start_keepalive``, which stores it as the per-camera option - and
|
|
98
|
+
every library resolver checks that option *before* it looks at the
|
|
99
|
+
environment. So a value returned from here does not merely beat the env var,
|
|
100
|
+
it outranks the library's entire resolution chain. This reader knows the
|
|
101
|
+
spelling of a flag but not what any knob defaults to, so it must decline
|
|
102
|
+
rather than guess: unset, blank, or a word it cannot read all mean "no
|
|
103
|
+
opinion", leaving the decision where the default actually lives.
|
|
104
|
+
|
|
105
|
+
The library states each knob as "a recognised token flips it, anything else
|
|
106
|
+
keeps the default", spelled as a truthy allowlist where the default is off
|
|
107
|
+
(``AIDOT_FAST_CONNECT``) and a falsy denylist where the default is on
|
|
108
|
+
(``AIDOT_SDES_SERVE_AUDIO``). Reading only the literal ``"1"`` broke that:
|
|
109
|
+
``AIDOT_FAST_CONNECT=true`` became an explicit False and *disabled* the
|
|
110
|
+
feature. Folding an unreadable value to False would break it the other way,
|
|
111
|
+
turning ``AIDOT_SDES_SERVE_AUDIO=`` - an ordinary empty-value shape in a
|
|
112
|
+
systemd unit or a compose file - into a silent loss of audio.
|
|
113
|
+
|
|
114
|
+
Not every knob is safe to route through this. A handful still compare against
|
|
115
|
+
a bare ``"0"`` (``AIDOT_SERVE_RELAY``, ``AIDOT_INCLUDE_SHARED_HOUSES``), where
|
|
116
|
+
a word spelling reads as true whatever it says; passing one of those through
|
|
117
|
+
here would hand the library an override it disagrees with. Check the
|
|
118
|
+
resolver before adding a knob to ``cmd_stream``."""
|
|
119
|
+
# Unset and unreadable are the same answer, so let the default reach the
|
|
120
|
+
# tuples rather than branching on it separately.
|
|
121
|
+
val = os.environ.get(name, "").strip().lower()
|
|
122
|
+
if val in ("1", "true", "yes", "on"):
|
|
123
|
+
return True
|
|
124
|
+
if val in ("0", "false", "no", "off"):
|
|
125
|
+
return False
|
|
126
|
+
return None
|
|
97
127
|
|
|
98
128
|
|
|
99
129
|
async def _cameras(client: AidotClient) -> list[dict]:
|
|
@@ -187,14 +217,17 @@ async def cmd_stream(dev_id: str, output_url: str) -> int:
|
|
|
187
217
|
"stdout for a go2rtc exec: consumer."
|
|
188
218
|
)
|
|
189
219
|
elif output_url.startswith("rtsp") and not dc.is_sdes_camera:
|
|
220
|
+
# Why G.711 here: see the codec_args branch in camera/client.py.
|
|
190
221
|
_LOGGER.info(
|
|
191
|
-
"DTLS RTSP push: publishing video +
|
|
222
|
+
"DTLS RTSP push: publishing video + G.711 audio to %s. Pass '-' "
|
|
223
|
+
"instead to keep the mux's 48 kHz AAC.", output_url
|
|
192
224
|
)
|
|
193
225
|
elif not output_url.startswith("rtsp") and dc.is_sdes_camera:
|
|
194
226
|
_LOGGER.warning(
|
|
195
|
-
"SDES camera but output %r is not rtsp:// -
|
|
196
|
-
"
|
|
197
|
-
"For go2rtc exec,
|
|
227
|
+
"SDES camera but output %r is not rtsp:// - only an http(s) "
|
|
228
|
+
"output is served for a consumer to pull; anything else is "
|
|
229
|
+
"treated as a publish target and will fail. For go2rtc exec, "
|
|
230
|
+
"pass {output} and let go2rtc substitute its own rtsp:// URL.",
|
|
198
231
|
output_url,
|
|
199
232
|
)
|
|
200
233
|
await dc.async_login()
|
{python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/client.py
RENAMED
|
@@ -5970,8 +5970,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
|
|
|
5970
5970
|
# `-bsf:a aac_adtstoasc` does not save it: the header is written
|
|
5971
5971
|
# before the filter has produced extradata. So transcode to G.711
|
|
5972
5972
|
# A-law, which is RTP-native, costs almost nothing at 8 kHz mono,
|
|
5973
|
-
# and
|
|
5974
|
-
#
|
|
5973
|
+
# and was what the SDES push carried until that path moved to AAC
|
|
5974
|
+
# (fMP4/MSE has no G.711 mapping, so browsers got silence there).
|
|
5975
5975
|
codec_args = ["-c:v", "copy", "-c:a", "pcm_alaw", "-ar", "8000", "-ac", "1"]
|
|
5976
5976
|
out_args = ["-f", "rtsp", "-rtsp_transport", "tcp", serve_url]
|
|
5977
5977
|
out_target = asyncio.subprocess.DEVNULL
|