python-aidot-cameras 1.0.0rc4__tar.gz → 1.0.0rc6__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.0rc4/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc6}/PKG-INFO +166 -16
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/README.md +165 -15
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/__main__.py +35 -12
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/client.py +87 -10
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sdes_open.py +67 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6/python_aidot_cameras.egg-info}/PKG-INFO +166 -16
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_camera_properties.py +64 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_go2rtc_cli.py +58 -2
- python_aidot_cameras-1.0.0rc6/tests/test_off_subnet_candidate.py +68 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/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.0rc6
|
|
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.
|
|
@@ -419,6 +568,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
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
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). 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) |
|
|
571
|
+
| `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
572
|
| `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
573
|
| `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
574
|
| `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.
|
|
@@ -379,6 +528,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
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
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). 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) |
|
|
531
|
+
| `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
532
|
| `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
533
|
| `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
534
|
| `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,28 @@ _LOGGER = logging.getLogger("aidot.go2rtc")
|
|
|
89
91
|
|
|
90
92
|
|
|
91
93
|
def _env_bool(name: str) -> bool | None:
|
|
92
|
-
"""Tri-state env flag:
|
|
94
|
+
"""Tri-state env flag: truthy -> True, falsy -> False, anything else -> None.
|
|
95
|
+
|
|
96
|
+
``None`` means "no opinion", so ``start_keepalive`` leaves the knob unset and
|
|
97
|
+
the library's own resolver reads the environment itself. That matters because
|
|
98
|
+
the library's resolvers do not share one convention: ``AIDOT_FAST_CONNECT`` is
|
|
99
|
+
a truthy allowlist over a default of off, while ``AIDOT_SDES_SERVE_AUDIO`` is a
|
|
100
|
+
falsy denylist over a default of *on*. No single reader here can mirror both,
|
|
101
|
+
so anything not plainly true or false must not become an override - reading
|
|
102
|
+
only the literal ``"1"`` made ``AIDOT_FAST_CONNECT=true`` an explicit False that
|
|
103
|
+
disabled the feature the user asked for, and returning False for an
|
|
104
|
+
unrecognised value would do the same to ``AIDOT_SDES_SERVE_AUDIO=`` (an
|
|
105
|
+
ordinary empty-value shape in a systemd unit or a compose file), silently
|
|
106
|
+
dropping audio the library would have served."""
|
|
93
107
|
raw = os.environ.get(name)
|
|
94
108
|
if raw is None:
|
|
95
109
|
return None
|
|
96
|
-
|
|
110
|
+
val = raw.strip().lower()
|
|
111
|
+
if val in ("1", "true", "yes", "on"):
|
|
112
|
+
return True
|
|
113
|
+
if val in ("0", "false", "no", "off"):
|
|
114
|
+
return False
|
|
115
|
+
return None
|
|
97
116
|
|
|
98
117
|
|
|
99
118
|
async def _cameras(client: AidotClient) -> list[dict]:
|
|
@@ -187,14 +206,18 @@ async def cmd_stream(dev_id: str, output_url: str) -> int:
|
|
|
187
206
|
"stdout for a go2rtc exec: consumer."
|
|
188
207
|
)
|
|
189
208
|
elif output_url.startswith("rtsp") and not dc.is_sdes_camera:
|
|
209
|
+
# The RTSP muxer refuses the mux's ADTS AAC, so this path transcodes
|
|
210
|
+
# audio to G.711 A-law; '-' is the form that keeps 48 kHz AAC.
|
|
190
211
|
_LOGGER.info(
|
|
191
|
-
"DTLS RTSP push: publishing video +
|
|
212
|
+
"DTLS RTSP push: publishing video + G.711 audio to %s. Pass '-' "
|
|
213
|
+
"instead to keep the mux's 48 kHz AAC.", output_url
|
|
192
214
|
)
|
|
193
215
|
elif not output_url.startswith("rtsp") and dc.is_sdes_camera:
|
|
194
216
|
_LOGGER.warning(
|
|
195
|
-
"SDES camera but output %r is not rtsp:// -
|
|
196
|
-
"
|
|
197
|
-
"For go2rtc exec,
|
|
217
|
+
"SDES camera but output %r is not rtsp:// - only an http(s) "
|
|
218
|
+
"output is served for a consumer to pull; anything else is "
|
|
219
|
+
"treated as a publish target and will fail. For go2rtc exec, "
|
|
220
|
+
"pass {output} and let go2rtc substitute its own rtsp:// URL.",
|
|
198
221
|
output_url,
|
|
199
222
|
)
|
|
200
223
|
await dc.async_login()
|