python-aidot-cameras 1.0.0b7__tar.gz → 1.0.0b9__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.0b9/MANIFEST.in +3 -0
- {python_aidot_cameras-1.0.0b7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b9}/PKG-INFO +148 -10
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/README.md +137 -8
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/__init__.py +10 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/__main__.py +48 -7
- python_aidot_cameras-1.0.0b9/aidot_cameras/_vendor/aiortc/LICENSE +25 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/client.py +80 -7
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/controls.py +18 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/lan_control.py +4 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/models.py +19 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/protocol.py +118 -0
- python_aidot_cameras-1.0.0b9/aidot_cameras/camera/sd_playback.py +112 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/webrtc_open.py +121 -28
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/client.py +4 -3
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/pyproject.toml +17 -5
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9/python_aidot_cameras.egg-info}/PKG-INFO +148 -10
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/SOURCES.txt +7 -0
- python_aidot_cameras-1.0.0b9/tests/test_answer_section_selection.py +189 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_stdout_producer.py +48 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_go2rtc_cli.py +29 -1
- python_aidot_cameras-1.0.0b9/tests/test_local_only_controls.py +76 -0
- python_aidot_cameras-1.0.0b9/tests/test_on_frame_is_an_av_frame.py +64 -0
- python_aidot_cameras-1.0.0b9/tests/test_public_signature_matches_impl.py +70 -0
- python_aidot_cameras-1.0.0b9/tests/test_sd_playback.py +107 -0
- python_aidot_cameras-1.0.0b7/MANIFEST.in +0 -1
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_webrtc_answer_race.py +0 -0
{python_aidot_cameras-1.0.0b7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b9}/PKG-INFO
RENAMED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b9
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://github.com/cbrightly/python-aidot-cameras
|
|
8
8
|
Project-URL: Issue Tracker, https://github.com/cbrightly/python-aidot-cameras/issues
|
|
9
|
-
Classifier:
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
10
12
|
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Classifier: Typing :: Typed
|
|
11
19
|
Requires-Python: >=3.11
|
|
12
20
|
Description-Content-Type: text/markdown
|
|
13
21
|
License-File: LICENSE
|
|
22
|
+
License-File: aidot_cameras/_vendor/aiortc/LICENSE
|
|
14
23
|
Requires-Dist: python-aidot<0.4,>=0.3.55
|
|
15
24
|
Requires-Dist: aiohttp>=3.9
|
|
16
25
|
Requires-Dist: paho-mqtt>=1.6.1
|
|
@@ -33,7 +42,7 @@ Dynamic: license-file
|
|
|
33
42
|
|
|
34
43
|
[](https://pypi.org/project/python-aidot-cameras/)
|
|
35
44
|
[](https://pypi.org/project/python-aidot-cameras/)
|
|
36
|
-
[](LICENSE)
|
|
45
|
+
[](https://github.com/cbrightly/python-aidot-cameras/blob/main/LICENSE)
|
|
37
46
|
|
|
38
47
|
Control AIDOT WiFi lights **and cameras** from Python.
|
|
39
48
|
|
|
@@ -46,7 +55,7 @@ recordings/thumbnails, and two-way (push-to-talk) audio.
|
|
|
46
55
|
Upstream owns the `aidot` import name and handles non-camera devices (lights,
|
|
47
56
|
plugs, switches) with none of this package's code in the path; everything here
|
|
48
57
|
lives under `aidot_cameras`. Taking a new upstream release is a dependency bump
|
|
49
|
-
plus a test run - see [`docs/UPSTREAM.md`](docs/UPSTREAM.md).
|
|
58
|
+
plus a test run - see [`docs/UPSTREAM.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/UPSTREAM.md).
|
|
50
59
|
|
|
51
60
|
This repository is the **library** (distribution name `python-aidot-cameras`).
|
|
52
61
|
The Home Assistant custom component (`custom_components/aidot/`) lives in the
|
|
@@ -63,7 +72,7 @@ The streaming transport is auto-selected per camera from its model id:
|
|
|
63
72
|
- **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
|
|
64
73
|
|
|
65
74
|
Other battery models (A001108, A001360) are recognized in code with the same
|
|
66
|
-
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
75
|
+
battery handling. See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#supported-cameras) for
|
|
67
76
|
the authoritative table and per-model notes.
|
|
68
77
|
|
|
69
78
|
## Known characteristics
|
|
@@ -164,7 +173,25 @@ uv tool install "python-aidot-cameras[webrtc]"
|
|
|
164
173
|
|
|
165
174
|
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
166
175
|
aidot-go2rtc --list # discover cameras + their transport
|
|
167
|
-
|
|
176
|
+
|
|
177
|
+
# Stream one camera. Pick the form that matches how you are running it:
|
|
178
|
+
aidot-go2rtc <device_id> - # MPEG-TS to stdout
|
|
179
|
+
aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull it
|
|
180
|
+
aidot-go2rtc <device_id> '{output}' # go2rtc exec: source only
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`'{output}'` is a placeholder go2rtc substitutes; run by hand it is passed
|
|
184
|
+
through as an RTSP push URL, which DTLS cameras (A000088) have no path for and
|
|
185
|
+
the tool refuses. Use `-` or an `http://host:port/name.ts` serve for a
|
|
186
|
+
standalone run.
|
|
187
|
+
|
|
188
|
+
**ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
|
|
189
|
+
stream entirely through an ffmpeg subprocess, and pip cannot install a system
|
|
190
|
+
binary:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
sudo apt install ffmpeg # Debian/Ubuntu
|
|
194
|
+
brew install ffmpeg # macOS
|
|
168
195
|
```
|
|
169
196
|
|
|
170
197
|
Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
|
|
@@ -176,13 +203,49 @@ the full list of authentication and stream env vars.
|
|
|
176
203
|
|
|
177
204
|
## Usage
|
|
178
205
|
|
|
179
|
-
|
|
206
|
+
Import from `aidot_cameras`, never from `aidot`. The upstream `aidot` package is
|
|
207
|
+
a dependency of this one and its `AidotClient` is lights-only: importing from it
|
|
208
|
+
gives you a client with no camera surface and **no ImportError** to tell you
|
|
209
|
+
why.
|
|
210
|
+
|
|
211
|
+
Log in, find a camera, and open a stream:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
import aiohttp
|
|
215
|
+
from aidot_cameras.client import AidotClient
|
|
216
|
+
from aidot_cameras.const import CONF_DEVICE_LIST, CONF_NAME
|
|
217
|
+
|
|
218
|
+
async with aiohttp.ClientSession() as http:
|
|
219
|
+
client = AidotClient(http, country_code="US", username=..., password=...)
|
|
220
|
+
await client.async_post_login()
|
|
221
|
+
devices = (await client.async_get_all_device())[CONF_DEVICE_LIST]
|
|
222
|
+
cam = next(d for d in devices if d.get(CONF_NAME) == "Front Door")
|
|
223
|
+
device_client = client.get_device_client(cam)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`get_device_client` returns a `CameraDeviceClient` for a camera. Plain
|
|
227
|
+
`DeviceClient` and `LightDeviceClient` are the lights clients and carry none of
|
|
228
|
+
the camera surface, so a `hasattr` probe against the wrong one fails silently.
|
|
229
|
+
|
|
230
|
+
Open a live WebRTC stream:
|
|
180
231
|
|
|
181
232
|
```python
|
|
182
233
|
session = await device_client.async_open_webrtc_stream(on_frame=cb, timeout=30.0)
|
|
183
234
|
# ... session.stop() when done
|
|
184
235
|
```
|
|
185
236
|
|
|
237
|
+
`on_frame` receives a PyAV `av.VideoFrame` - use `pts` / `time_base` /
|
|
238
|
+
`to_ndarray()`. It is **DTLS-only** (A000088): SDES cameras (A001513, A001064)
|
|
239
|
+
decode out of process, so `on_frame` never fires for them and no error is
|
|
240
|
+
raised. For those, take the media as a file or a stream instead:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
session = await device_client.async_open_webrtc_stream(
|
|
244
|
+
output_path="/tmp/live.ts", # record, or
|
|
245
|
+
rtsp_push_url="rtsp://127.0.0.1:8554/cam", # push to go2rtc
|
|
246
|
+
)
|
|
247
|
+
```
|
|
248
|
+
|
|
186
249
|
Two-way (push-to-talk) audio:
|
|
187
250
|
|
|
188
251
|
```python
|
|
@@ -192,9 +255,84 @@ await session.async_start_talk(pcm_provider) # provider() -> 320B s16le PCM (2
|
|
|
192
255
|
await session.async_stop_talk()
|
|
193
256
|
```
|
|
194
257
|
|
|
195
|
-
See [`docs/CAMERAS.md`](docs/CAMERAS.md) for the full camera API (streaming,
|
|
258
|
+
See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
|
|
196
259
|
snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
|
|
197
260
|
|
|
261
|
+
## Getting an RTSP URL
|
|
262
|
+
|
|
263
|
+
Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
|
|
264
|
+
not from this library alone: **it does not run an RTSP server**, and neither do
|
|
265
|
+
the cameras - they have no RTSP and no ONVIF endpoint. The only way video
|
|
266
|
+
leaves them is the cloud-signalled WebRTC handshake and SRTP decrypt this
|
|
267
|
+
library does. Pair it with [go2rtc](https://github.com/AlexxIT/go2rtc) and
|
|
268
|
+
go2rtc publishes the RTSP URL for you.
|
|
269
|
+
|
|
270
|
+
Three steps, about five minutes.
|
|
271
|
+
|
|
272
|
+
**1. List your cameras.** The transport column decides one word in step 3, so
|
|
273
|
+
keep the output around:
|
|
274
|
+
|
|
275
|
+
```console
|
|
276
|
+
$ pip install "python-aidot-cameras[webrtc]"
|
|
277
|
+
$ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
|
|
278
|
+
$ aidot-go2rtc --list
|
|
279
|
+
1a2b3c4d... LK.IPC.A000088 DTLS (http-pull)
|
|
280
|
+
5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**2. Add one line per camera to `go2rtc.yaml`.** Both forms make go2rtc the
|
|
284
|
+
parent process, so it starts the stream on the first viewer and kills it when
|
|
285
|
+
the last one leaves - nothing talks to your cameras while nobody is watching:
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
streams:
|
|
289
|
+
driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
|
|
290
|
+
frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Use `-` for a DTLS camera and `{output}` for an SDES one - that is the whole
|
|
294
|
+
difference, and `--list` told you which is which. (`{output}` is a placeholder
|
|
295
|
+
go2rtc fills in with its own publish URL. It is not something you type at a
|
|
296
|
+
shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
|
|
297
|
+
variables the CLI does, so set them wherever go2rtc starts.
|
|
298
|
+
|
|
299
|
+
**3. Use the URL.** go2rtc serves every stream you named at:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
rtsp://<go2rtc-host>:8554/driveway
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Check it before wiring anything up to it:
|
|
306
|
+
|
|
307
|
+
```console
|
|
308
|
+
$ ffprobe rtsp://127.0.0.1:8554/driveway
|
|
309
|
+
Stream #0:0: Video: h264 (High), yuv420p, 1280x720, 15 fps
|
|
310
|
+
Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Details worth knowing
|
|
314
|
+
|
|
315
|
+
- **Which camera is which.** `--list` reports the transport per device. DTLS is
|
|
316
|
+
the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
|
|
317
|
+
form is not silent - the CLI tells you which one to use and exits.
|
|
318
|
+
- **Audio.** The SDES `{output}` push carries G.711 audio alongside video; the
|
|
319
|
+
DTLS `-` producer carries AAC (48 kHz mono, resampled from the camera's 8 kHz
|
|
320
|
+
A-law, because 8 kHz AAC plays silent in a lot of browsers).
|
|
321
|
+
- **A stream that starts with no video** and picks it up a few seconds later is
|
|
322
|
+
normal: the mux waits for a keyframe so the first GOP is decodable. A stream
|
|
323
|
+
that stays audio-only is a camera that never sent one - retry the view.
|
|
324
|
+
- **Skipping go2rtc.** `aidot-go2rtc <id> http://127.0.0.1:8555/cam.ts` serves
|
|
325
|
+
MPEG-TS on a listening socket instead, for a consumer that pulls rather than
|
|
326
|
+
spawns. That process has to stay running to keep the port bound, so it holds
|
|
327
|
+
a camera session whether or not anyone is watching. Prefer the `exec:` forms.
|
|
328
|
+
- **From Python**, `rtsp_push_url=` publishes to your own RTSP server directly
|
|
329
|
+
(SDES cameras only - there is no RTSP-push path for DTLS), and
|
|
330
|
+
`output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
|
|
331
|
+
address above, and `ensure_stream(name, source)` registers a stream with a
|
|
332
|
+
running go2rtc if you would rather not edit YAML.
|
|
333
|
+
- **Home Assistant users need none of this.** The integration manages go2rtc
|
|
334
|
+
itself; this section is for standalone use.
|
|
335
|
+
|
|
198
336
|
## Home Assistant component and CLI
|
|
199
337
|
|
|
200
338
|
The Home Assistant custom component (`custom_components/aidot/`) is **not** part
|
|
@@ -210,7 +348,7 @@ The library reads the following environment variables.
|
|
|
210
348
|
### Credentials
|
|
211
349
|
|
|
212
350
|
Used by the credential helper (`aidot.credentials`); they take priority over any
|
|
213
|
-
stored credentials file. See [`aidot_cameras/credentials.py`](aidot_cameras/credentials.py).
|
|
351
|
+
stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
|
|
214
352
|
|
|
215
353
|
| Variable | Purpose | Default |
|
|
216
354
|
| --- | --- | --- |
|
|
@@ -224,7 +362,7 @@ The most useful knobs read by the camera client (`aidot_cameras.camera.client`).
|
|
|
224
362
|
are chosen to work out of the box; override only when tuning. Finer-grained
|
|
225
363
|
internal knobs (audio normalization, keyframe/PLI cadence, retry timing, SDES
|
|
226
364
|
audio, idle release, the sprop cache path) are documented in
|
|
227
|
-
[`docs/CAMERAS.md`](docs/CAMERAS.md#advanced-tuning-environment-variables).
|
|
365
|
+
[`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#advanced-tuning-environment-variables).
|
|
228
366
|
|
|
229
367
|
| Variable | Purpose | Default |
|
|
230
368
|
| --- | --- | --- |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/python-aidot-cameras/)
|
|
4
4
|
[](https://pypi.org/project/python-aidot-cameras/)
|
|
5
|
-
[](LICENSE)
|
|
5
|
+
[](https://github.com/cbrightly/python-aidot-cameras/blob/main/LICENSE)
|
|
6
6
|
|
|
7
7
|
Control AIDOT WiFi lights **and cameras** from Python.
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ recordings/thumbnails, and two-way (push-to-talk) audio.
|
|
|
15
15
|
Upstream owns the `aidot` import name and handles non-camera devices (lights,
|
|
16
16
|
plugs, switches) with none of this package's code in the path; everything here
|
|
17
17
|
lives under `aidot_cameras`. Taking a new upstream release is a dependency bump
|
|
18
|
-
plus a test run - see [`docs/UPSTREAM.md`](docs/UPSTREAM.md).
|
|
18
|
+
plus a test run - see [`docs/UPSTREAM.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/UPSTREAM.md).
|
|
19
19
|
|
|
20
20
|
This repository is the **library** (distribution name `python-aidot-cameras`).
|
|
21
21
|
The Home Assistant custom component (`custom_components/aidot/`) lives in the
|
|
@@ -32,7 +32,7 @@ The streaming transport is auto-selected per camera from its model id:
|
|
|
32
32
|
- **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
|
|
33
33
|
|
|
34
34
|
Other battery models (A001108, A001360) are recognized in code with the same
|
|
35
|
-
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
35
|
+
battery handling. See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#supported-cameras) for
|
|
36
36
|
the authoritative table and per-model notes.
|
|
37
37
|
|
|
38
38
|
## Known characteristics
|
|
@@ -133,7 +133,25 @@ uv tool install "python-aidot-cameras[webrtc]"
|
|
|
133
133
|
|
|
134
134
|
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
135
135
|
aidot-go2rtc --list # discover cameras + their transport
|
|
136
|
-
|
|
136
|
+
|
|
137
|
+
# Stream one camera. Pick the form that matches how you are running it:
|
|
138
|
+
aidot-go2rtc <device_id> - # MPEG-TS to stdout
|
|
139
|
+
aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull it
|
|
140
|
+
aidot-go2rtc <device_id> '{output}' # go2rtc exec: source only
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`'{output}'` is a placeholder go2rtc substitutes; run by hand it is passed
|
|
144
|
+
through as an RTSP push URL, which DTLS cameras (A000088) have no path for and
|
|
145
|
+
the tool refuses. Use `-` or an `http://host:port/name.ts` serve for a
|
|
146
|
+
standalone run.
|
|
147
|
+
|
|
148
|
+
**ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
|
|
149
|
+
stream entirely through an ffmpeg subprocess, and pip cannot install a system
|
|
150
|
+
binary:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
sudo apt install ffmpeg # Debian/Ubuntu
|
|
154
|
+
brew install ffmpeg # macOS
|
|
137
155
|
```
|
|
138
156
|
|
|
139
157
|
Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
|
|
@@ -145,13 +163,49 @@ the full list of authentication and stream env vars.
|
|
|
145
163
|
|
|
146
164
|
## Usage
|
|
147
165
|
|
|
148
|
-
|
|
166
|
+
Import from `aidot_cameras`, never from `aidot`. The upstream `aidot` package is
|
|
167
|
+
a dependency of this one and its `AidotClient` is lights-only: importing from it
|
|
168
|
+
gives you a client with no camera surface and **no ImportError** to tell you
|
|
169
|
+
why.
|
|
170
|
+
|
|
171
|
+
Log in, find a camera, and open a stream:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import aiohttp
|
|
175
|
+
from aidot_cameras.client import AidotClient
|
|
176
|
+
from aidot_cameras.const import CONF_DEVICE_LIST, CONF_NAME
|
|
177
|
+
|
|
178
|
+
async with aiohttp.ClientSession() as http:
|
|
179
|
+
client = AidotClient(http, country_code="US", username=..., password=...)
|
|
180
|
+
await client.async_post_login()
|
|
181
|
+
devices = (await client.async_get_all_device())[CONF_DEVICE_LIST]
|
|
182
|
+
cam = next(d for d in devices if d.get(CONF_NAME) == "Front Door")
|
|
183
|
+
device_client = client.get_device_client(cam)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`get_device_client` returns a `CameraDeviceClient` for a camera. Plain
|
|
187
|
+
`DeviceClient` and `LightDeviceClient` are the lights clients and carry none of
|
|
188
|
+
the camera surface, so a `hasattr` probe against the wrong one fails silently.
|
|
189
|
+
|
|
190
|
+
Open a live WebRTC stream:
|
|
149
191
|
|
|
150
192
|
```python
|
|
151
193
|
session = await device_client.async_open_webrtc_stream(on_frame=cb, timeout=30.0)
|
|
152
194
|
# ... session.stop() when done
|
|
153
195
|
```
|
|
154
196
|
|
|
197
|
+
`on_frame` receives a PyAV `av.VideoFrame` - use `pts` / `time_base` /
|
|
198
|
+
`to_ndarray()`. It is **DTLS-only** (A000088): SDES cameras (A001513, A001064)
|
|
199
|
+
decode out of process, so `on_frame` never fires for them and no error is
|
|
200
|
+
raised. For those, take the media as a file or a stream instead:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
session = await device_client.async_open_webrtc_stream(
|
|
204
|
+
output_path="/tmp/live.ts", # record, or
|
|
205
|
+
rtsp_push_url="rtsp://127.0.0.1:8554/cam", # push to go2rtc
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
155
209
|
Two-way (push-to-talk) audio:
|
|
156
210
|
|
|
157
211
|
```python
|
|
@@ -161,9 +215,84 @@ await session.async_start_talk(pcm_provider) # provider() -> 320B s16le PCM (2
|
|
|
161
215
|
await session.async_stop_talk()
|
|
162
216
|
```
|
|
163
217
|
|
|
164
|
-
See [`docs/CAMERAS.md`](docs/CAMERAS.md) for the full camera API (streaming,
|
|
218
|
+
See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
|
|
165
219
|
snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
|
|
166
220
|
|
|
221
|
+
## Getting an RTSP URL
|
|
222
|
+
|
|
223
|
+
Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
|
|
224
|
+
not from this library alone: **it does not run an RTSP server**, and neither do
|
|
225
|
+
the cameras - they have no RTSP and no ONVIF endpoint. The only way video
|
|
226
|
+
leaves them is the cloud-signalled WebRTC handshake and SRTP decrypt this
|
|
227
|
+
library does. Pair it with [go2rtc](https://github.com/AlexxIT/go2rtc) and
|
|
228
|
+
go2rtc publishes the RTSP URL for you.
|
|
229
|
+
|
|
230
|
+
Three steps, about five minutes.
|
|
231
|
+
|
|
232
|
+
**1. List your cameras.** The transport column decides one word in step 3, so
|
|
233
|
+
keep the output around:
|
|
234
|
+
|
|
235
|
+
```console
|
|
236
|
+
$ pip install "python-aidot-cameras[webrtc]"
|
|
237
|
+
$ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
|
|
238
|
+
$ aidot-go2rtc --list
|
|
239
|
+
1a2b3c4d... LK.IPC.A000088 DTLS (http-pull)
|
|
240
|
+
5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**2. Add one line per camera to `go2rtc.yaml`.** Both forms make go2rtc the
|
|
244
|
+
parent process, so it starts the stream on the first viewer and kills it when
|
|
245
|
+
the last one leaves - nothing talks to your cameras while nobody is watching:
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
streams:
|
|
249
|
+
driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
|
|
250
|
+
frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Use `-` for a DTLS camera and `{output}` for an SDES one - that is the whole
|
|
254
|
+
difference, and `--list` told you which is which. (`{output}` is a placeholder
|
|
255
|
+
go2rtc fills in with its own publish URL. It is not something you type at a
|
|
256
|
+
shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
|
|
257
|
+
variables the CLI does, so set them wherever go2rtc starts.
|
|
258
|
+
|
|
259
|
+
**3. Use the URL.** go2rtc serves every stream you named at:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
rtsp://<go2rtc-host>:8554/driveway
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Check it before wiring anything up to it:
|
|
266
|
+
|
|
267
|
+
```console
|
|
268
|
+
$ ffprobe rtsp://127.0.0.1:8554/driveway
|
|
269
|
+
Stream #0:0: Video: h264 (High), yuv420p, 1280x720, 15 fps
|
|
270
|
+
Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Details worth knowing
|
|
274
|
+
|
|
275
|
+
- **Which camera is which.** `--list` reports the transport per device. DTLS is
|
|
276
|
+
the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
|
|
277
|
+
form is not silent - the CLI tells you which one to use and exits.
|
|
278
|
+
- **Audio.** The SDES `{output}` push carries G.711 audio alongside video; the
|
|
279
|
+
DTLS `-` producer carries AAC (48 kHz mono, resampled from the camera's 8 kHz
|
|
280
|
+
A-law, because 8 kHz AAC plays silent in a lot of browsers).
|
|
281
|
+
- **A stream that starts with no video** and picks it up a few seconds later is
|
|
282
|
+
normal: the mux waits for a keyframe so the first GOP is decodable. A stream
|
|
283
|
+
that stays audio-only is a camera that never sent one - retry the view.
|
|
284
|
+
- **Skipping go2rtc.** `aidot-go2rtc <id> http://127.0.0.1:8555/cam.ts` serves
|
|
285
|
+
MPEG-TS on a listening socket instead, for a consumer that pulls rather than
|
|
286
|
+
spawns. That process has to stay running to keep the port bound, so it holds
|
|
287
|
+
a camera session whether or not anyone is watching. Prefer the `exec:` forms.
|
|
288
|
+
- **From Python**, `rtsp_push_url=` publishes to your own RTSP server directly
|
|
289
|
+
(SDES cameras only - there is no RTSP-push path for DTLS), and
|
|
290
|
+
`output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
|
|
291
|
+
address above, and `ensure_stream(name, source)` registers a stream with a
|
|
292
|
+
running go2rtc if you would rather not edit YAML.
|
|
293
|
+
- **Home Assistant users need none of this.** The integration manages go2rtc
|
|
294
|
+
itself; this section is for standalone use.
|
|
295
|
+
|
|
167
296
|
## Home Assistant component and CLI
|
|
168
297
|
|
|
169
298
|
The Home Assistant custom component (`custom_components/aidot/`) is **not** part
|
|
@@ -179,7 +308,7 @@ The library reads the following environment variables.
|
|
|
179
308
|
### Credentials
|
|
180
309
|
|
|
181
310
|
Used by the credential helper (`aidot.credentials`); they take priority over any
|
|
182
|
-
stored credentials file. See [`aidot_cameras/credentials.py`](aidot_cameras/credentials.py).
|
|
311
|
+
stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
|
|
183
312
|
|
|
184
313
|
| Variable | Purpose | Default |
|
|
185
314
|
| --- | --- | --- |
|
|
@@ -193,7 +322,7 @@ The most useful knobs read by the camera client (`aidot_cameras.camera.client`).
|
|
|
193
322
|
are chosen to work out of the box; override only when tuning. Finer-grained
|
|
194
323
|
internal knobs (audio normalization, keyframe/PLI cadence, retry timing, SDES
|
|
195
324
|
audio, idle release, the sprop cache path) are documented in
|
|
196
|
-
[`docs/CAMERAS.md`](docs/CAMERAS.md#advanced-tuning-environment-variables).
|
|
325
|
+
[`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#advanced-tuning-environment-variables).
|
|
197
326
|
|
|
198
327
|
| Variable | Purpose | Default |
|
|
199
328
|
| --- | --- | --- |
|
|
@@ -13,6 +13,12 @@ import time as _time
|
|
|
13
13
|
from .client import AidotClient, CameraClient
|
|
14
14
|
from .device_client import CameraDeviceClient, DeviceInformation, DeviceStatusData
|
|
15
15
|
from .camera.client import configure_stream_limits
|
|
16
|
+
# Promised by docs/API-STABILITY.md and previously not reachable from the
|
|
17
|
+
# package root. Verified to pull no optional dependency: importing these does
|
|
18
|
+
# not drag in av or aiortc, so a plain (non-[webrtc]) install still imports.
|
|
19
|
+
from .camera.models import CameraDeviceInformation, CameraStatusData
|
|
20
|
+
from .camera.sdes import SdesSession
|
|
21
|
+
from .camera.webrtc import WebRTCSession
|
|
16
22
|
from .discover import CameraDiscover, Discover
|
|
17
23
|
from .exceptions import (
|
|
18
24
|
AidotAuthFailed,
|
|
@@ -141,12 +147,16 @@ __all__ = [
|
|
|
141
147
|
"AidotUserOrPassIncorrect",
|
|
142
148
|
"CameraClient",
|
|
143
149
|
"CameraDeviceClient",
|
|
150
|
+
"CameraDeviceInformation",
|
|
144
151
|
"CameraDiscover",
|
|
152
|
+
"CameraStatusData",
|
|
145
153
|
"DeviceInformation",
|
|
146
154
|
"DeviceStatusData",
|
|
147
155
|
"Discover",
|
|
148
156
|
"HTTPError",
|
|
149
157
|
"InvalidHost",
|
|
150
158
|
"InvalidURL",
|
|
159
|
+
"SdesSession",
|
|
160
|
+
"WebRTCSession",
|
|
151
161
|
"configure_stream_limits",
|
|
152
162
|
]
|
|
@@ -7,13 +7,16 @@ use them: it authenticates to the AiDot cloud, opens the keepalive session for
|
|
|
7
7
|
one camera, and pushes/serves the decrypted H.264 (+ G711 audio on the RTSP-push
|
|
8
8
|
path) to a URL the consumer reads.
|
|
9
9
|
|
|
10
|
-
Run it as ``aidot-go2rtc`` (console script) or ``python -m
|
|
10
|
+
Run it as ``aidot-go2rtc`` (console script) or ``python -m aidot_cameras``.
|
|
11
11
|
|
|
12
12
|
Two modes:
|
|
13
13
|
|
|
14
14
|
* --list Print every camera on the account with its device id
|
|
15
|
-
and transport
|
|
16
|
-
|
|
15
|
+
and transport, followed by a paste-ready go2rtc
|
|
16
|
+
``streams:`` block - each camera already paired with
|
|
17
|
+
the right output argument, which is the one thing
|
|
18
|
+
that differs between the transports and the easiest
|
|
19
|
+
thing to get wrong.
|
|
17
20
|
|
|
18
21
|
* <dev_id> <output_url> Stream one camera. Meant to be invoked by go2rtc as
|
|
19
22
|
an ``exec:`` source - go2rtc substitutes ``{output}``
|
|
@@ -66,14 +69,16 @@ import asyncio
|
|
|
66
69
|
import json
|
|
67
70
|
import logging
|
|
68
71
|
import os
|
|
72
|
+
import re
|
|
69
73
|
import signal
|
|
70
74
|
import sys
|
|
71
75
|
import tempfile
|
|
76
|
+
from typing import Optional
|
|
72
77
|
|
|
73
78
|
import aiohttp
|
|
74
79
|
|
|
75
80
|
from .client import AidotClient
|
|
76
|
-
from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID
|
|
81
|
+
from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID, CONF_NAME
|
|
77
82
|
|
|
78
83
|
_LOGGER = logging.getLogger("aidot.go2rtc")
|
|
79
84
|
|
|
@@ -206,8 +211,30 @@ async def _cameras(client: AidotClient) -> list[dict]:
|
|
|
206
211
|
]
|
|
207
212
|
|
|
208
213
|
|
|
214
|
+
def _stream_slug(name: Optional[str], dev_id: str) -> str:
|
|
215
|
+
"""A go2rtc stream key from the camera's name (``Front Door`` -> ``front_door``).
|
|
216
|
+
|
|
217
|
+
go2rtc stream names end up in URLs, so keep it to lowercase word characters
|
|
218
|
+
and fall back to the device id when a name is missing or is punctuation
|
|
219
|
+
only. Pure (unit-testable)."""
|
|
220
|
+
slug = re.sub(r"[^a-z0-9]+", "_", (name or "").lower()).strip("_")
|
|
221
|
+
return slug or f"camera_{dev_id[:8]}"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _go2rtc_source(dev_id: str, is_sdes: bool) -> str:
|
|
225
|
+
"""The ``exec:`` source line for one camera.
|
|
226
|
+
|
|
227
|
+
The only difference between the two transports is the output argument:
|
|
228
|
+
SDES cameras RTSP-push into go2rtc's own ``{output}`` URL, DTLS cameras
|
|
229
|
+
write MPEG-TS to stdout (``-``). Getting that backwards is the single most
|
|
230
|
+
common way to end up with a stream that never starts, which is why --list
|
|
231
|
+
writes the line out rather than describing it."""
|
|
232
|
+
out = "{output}" if is_sdes else "-"
|
|
233
|
+
return f"exec:aidot-go2rtc {dev_id} {out}"
|
|
234
|
+
|
|
235
|
+
|
|
209
236
|
async def cmd_list() -> int:
|
|
210
|
-
"""Print device id + transport for every camera,
|
|
237
|
+
"""Print device id + transport for every camera, plus paste-ready go2rtc YAML."""
|
|
211
238
|
async with aiohttp.ClientSession() as session:
|
|
212
239
|
client = await _make_client(session)
|
|
213
240
|
cams = await _cameras(client)
|
|
@@ -216,10 +243,20 @@ async def cmd_list() -> int:
|
|
|
216
243
|
return 1
|
|
217
244
|
print(f"{'device_id':<40} {'model':<14} transport")
|
|
218
245
|
print("-" * 72)
|
|
246
|
+
rows = []
|
|
219
247
|
for d in cams:
|
|
220
248
|
dc = client.get_device_client(d)
|
|
221
|
-
transport = "SDES (rtsp-push)" if dc.is_sdes_camera else "DTLS (
|
|
249
|
+
transport = "SDES (rtsp-push)" if dc.is_sdes_camera else "DTLS (stdout)"
|
|
222
250
|
print(f"{d[CONF_ID]:<40} {(d.get(CONF_MODEL_ID) or ''):<14} {transport}")
|
|
251
|
+
rows.append((_stream_slug(d.get(CONF_NAME), d[CONF_ID]),
|
|
252
|
+
_go2rtc_source(d[CONF_ID], dc.is_sdes_camera)))
|
|
253
|
+
width = max(len(slug) for slug, _ in rows) + 1
|
|
254
|
+
print("\n# Paste into go2rtc.yaml (rename the streams to taste). go2rtc needs")
|
|
255
|
+
print("# the same AIDOT_* environment variables this command just used.")
|
|
256
|
+
print("streams:")
|
|
257
|
+
for slug, source in rows:
|
|
258
|
+
print(f" {(slug + ':'):<{width}} {source}")
|
|
259
|
+
print("\n# Then each camera is at rtsp://<go2rtc-host>:8554/<stream-name>")
|
|
223
260
|
return 0
|
|
224
261
|
|
|
225
262
|
|
|
@@ -293,9 +330,13 @@ async def cmd_stream(dev_id: str, output_url: str) -> int:
|
|
|
293
330
|
|
|
294
331
|
|
|
295
332
|
def main(argv: list[str] | None = None) -> int:
|
|
333
|
+
# The module docstring above IS the reference for every env var this tool
|
|
334
|
+
# reads, and the README tells users --help will show it. Print it rather
|
|
335
|
+
# than a one-line summary that leaves them hunting through source.
|
|
296
336
|
parser = argparse.ArgumentParser(
|
|
297
337
|
prog="aidot-go2rtc",
|
|
298
|
-
description=
|
|
338
|
+
description=__doc__,
|
|
339
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
299
340
|
epilog="Run with --list first to discover device ids and transports.",
|
|
300
341
|
)
|
|
301
342
|
parser.add_argument("dev_id", nargs="?", help="camera device id (see --list)")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Copyright (c) Jeremy Lainé.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice,
|
|
8
|
+
this list of conditions and the following disclaimer.
|
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
* Neither the name of aiortc nor the names of its contributors may
|
|
13
|
+
be used to endorse or promote products derived from this software without
|
|
14
|
+
specific prior written permission.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|