python-aidot-cameras 0.17.2__tar.gz → 1.0.0b2__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-0.17.2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b2}/PKG-INFO +37 -1
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/README.md +36 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/client.py +37 -7
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/lan_control.py +26 -2
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/protocol.py +12 -4
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes.py +28 -8
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes_open.py +795 -138
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/webrtc_open.py +51 -16
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info}/PKG-INFO +37 -1
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/SOURCES.txt +17 -1
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_control_verdicts.py +12 -2
- python_aidot_cameras-1.0.0b2/tests/test_dtls_resolution_ack.py +238 -0
- python_aidot_cameras-1.0.0b2/tests/test_feature_probe_needs_a_live_session.py +104 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_futile_keepalive.py +35 -4
- python_aidot_cameras-1.0.0b2/tests/test_gate_requires_a_decoded_frame.py +101 -0
- python_aidot_cameras-1.0.0b2/tests/test_key_restart_sdp.py +195 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_lan_control.py +61 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_live_validate_policy.py +182 -0
- python_aidot_cameras-1.0.0b2/tests/test_media_counted_only_when_plaintext.py +237 -0
- python_aidot_cameras-1.0.0b2/tests/test_no_key_material_in_logs.py +269 -0
- python_aidot_cameras-1.0.0b2/tests/test_ptz_probe_reads_what_it_is_told.py +88 -0
- python_aidot_cameras-1.0.0b2/tests/test_sctp_init_ack_tsn.py +94 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_bridge_break_gate.py +74 -10
- python_aidot_cameras-1.0.0b2/tests/test_sdes_first_media_stall_report.py +344 -0
- python_aidot_cameras-1.0.0b2/tests/test_sdes_offer_video_order.py +263 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_talk.py +16 -0
- python_aidot_cameras-1.0.0b2/tests/test_snapshot_cancellation.py +105 -0
- python_aidot_cameras-1.0.0b2/tests/test_sprop_write_is_reported_truthfully.py +102 -0
- python_aidot_cameras-1.0.0b2/tests/test_srtp_rx_rekey.py +183 -0
- python_aidot_cameras-1.0.0b2/tests/test_srtp_tx_key_note.py +101 -0
- python_aidot_cameras-1.0.0b2/tests/test_talk_reports_a_speaker_that_never_opened.py +170 -0
- python_aidot_cameras-1.0.0b2/tests/test_tutk_path_is_out_of_reach.py +78 -0
- python_aidot_cameras-1.0.0b2/tests/test_webrtc_answer_race.py +101 -0
- python_aidot_cameras-0.17.2/tests/test_key_restart_sdp.py +0 -61
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/LICENSE +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/setup.cfg +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_remb.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.17.2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0b2
|
|
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
|
|
@@ -66,6 +66,40 @@ Other battery models (A001108, A001360) are recognized in code with the same
|
|
|
66
66
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
67
67
|
the authoritative table and per-model notes.
|
|
68
68
|
|
|
69
|
+
## Local (LAN) control and account ownership
|
|
70
|
+
|
|
71
|
+
Local control over the devices' TCP:10000 channel - both the light/plug protocol
|
|
72
|
+
and this package's `CameraLanClient` - is accepted **only for the account that
|
|
73
|
+
owns the devices**. A member of a shared home is rejected at the device, even
|
|
74
|
+
though the cloud happily hands that member a full device list, complete with the
|
|
75
|
+
per-device `password` and `aesKey`.
|
|
76
|
+
|
|
77
|
+
The rejection is easy to misread, because the device answers with a code that
|
|
78
|
+
varies by firmware family and a message that blames the wrong thing:
|
|
79
|
+
|
|
80
|
+
| Model | Ack |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `LK.light.A001493` | 400 `not equal abort user id or password` |
|
|
83
|
+
| `LK.light.A001497`, `LK.plug.A001535` | 4354 `fail` |
|
|
84
|
+
| `LK.IPC.A000088` | 4352 `fail` |
|
|
85
|
+
|
|
86
|
+
The A001493's message is the honest one: it is the **user id** that does not
|
|
87
|
+
match, not the password. Every credential in the request can be correct and the
|
|
88
|
+
login will still be refused if the `userId` is not the owner's.
|
|
89
|
+
|
|
90
|
+
Verified 2026-08-08 against ten devices across four model families: every one
|
|
91
|
+
returns 200 for the owning account and one of the codes above for a shared-home
|
|
92
|
+
member.
|
|
93
|
+
|
|
94
|
+
**If local control never engages, check which account the integration is signed
|
|
95
|
+
in as before looking anywhere else.** A dedicated or secondary login - the sort
|
|
96
|
+
of thing you might use to avoid contending with a phone app over a rotating
|
|
97
|
+
refresh token - will control everything fine through the cloud and never once
|
|
98
|
+
log in locally.
|
|
99
|
+
|
|
100
|
+
Cameras' live video is unaffected either way: it is WebRTC signaled over cloud
|
|
101
|
+
MQTT and does not use this path.
|
|
102
|
+
|
|
69
103
|
## Library install
|
|
70
104
|
|
|
71
105
|
Install from PyPI (the simplest, recommended method):
|
|
@@ -175,6 +209,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
175
209
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
176
210
|
| `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) |
|
|
177
211
|
| `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) |
|
|
212
|
+
| `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) |
|
|
213
|
+
| `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. | unset (`96 97`) |
|
|
178
214
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
179
215
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
180
216
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
@@ -35,6 +35,40 @@ Other battery models (A001108, A001360) are recognized in code with the same
|
|
|
35
35
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
36
36
|
the authoritative table and per-model notes.
|
|
37
37
|
|
|
38
|
+
## Local (LAN) control and account ownership
|
|
39
|
+
|
|
40
|
+
Local control over the devices' TCP:10000 channel - both the light/plug protocol
|
|
41
|
+
and this package's `CameraLanClient` - is accepted **only for the account that
|
|
42
|
+
owns the devices**. A member of a shared home is rejected at the device, even
|
|
43
|
+
though the cloud happily hands that member a full device list, complete with the
|
|
44
|
+
per-device `password` and `aesKey`.
|
|
45
|
+
|
|
46
|
+
The rejection is easy to misread, because the device answers with a code that
|
|
47
|
+
varies by firmware family and a message that blames the wrong thing:
|
|
48
|
+
|
|
49
|
+
| Model | Ack |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `LK.light.A001493` | 400 `not equal abort user id or password` |
|
|
52
|
+
| `LK.light.A001497`, `LK.plug.A001535` | 4354 `fail` |
|
|
53
|
+
| `LK.IPC.A000088` | 4352 `fail` |
|
|
54
|
+
|
|
55
|
+
The A001493's message is the honest one: it is the **user id** that does not
|
|
56
|
+
match, not the password. Every credential in the request can be correct and the
|
|
57
|
+
login will still be refused if the `userId` is not the owner's.
|
|
58
|
+
|
|
59
|
+
Verified 2026-08-08 against ten devices across four model families: every one
|
|
60
|
+
returns 200 for the owning account and one of the codes above for a shared-home
|
|
61
|
+
member.
|
|
62
|
+
|
|
63
|
+
**If local control never engages, check which account the integration is signed
|
|
64
|
+
in as before looking anywhere else.** A dedicated or secondary login - the sort
|
|
65
|
+
of thing you might use to avoid contending with a phone app over a rotating
|
|
66
|
+
refresh token - will control everything fine through the cloud and never once
|
|
67
|
+
log in locally.
|
|
68
|
+
|
|
69
|
+
Cameras' live video is unaffected either way: it is WebRTC signaled over cloud
|
|
70
|
+
MQTT and does not use this path.
|
|
71
|
+
|
|
38
72
|
## Library install
|
|
39
73
|
|
|
40
74
|
Install from PyPI (the simplest, recommended method):
|
|
@@ -144,6 +178,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
144
178
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
145
179
|
| `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) |
|
|
146
180
|
| `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) |
|
|
181
|
+
| `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) |
|
|
182
|
+
| `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. | unset (`96 97`) |
|
|
147
183
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
148
184
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
149
185
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
@@ -398,6 +398,21 @@ _BUSY_BACKOFF_S = float(os.environ.get("AIDOT_BUSY_BACKOFF_S", "20"))
|
|
|
398
398
|
_FUTILE_KEEPALIVE_LIMIT = int(os.environ.get("AIDOT_FUTILE_KEEPALIVE_LIMIT", "5"))
|
|
399
399
|
|
|
400
400
|
|
|
401
|
+
def _next_no_media_streak(no_media_streak: int, healthy: bool) -> int:
|
|
402
|
+
"""The streak after a keepalive session that was (or was not) healthy.
|
|
403
|
+
|
|
404
|
+
The ceiling counts CONSECUTIVE failures, so a session that delivered media
|
|
405
|
+
resets it to zero. Without that reset the count is cumulative and a camera
|
|
406
|
+
that merely fails occasionally - five scattered no-media sessions spread
|
|
407
|
+
across a day, each one separated by sessions that worked - eventually trips
|
|
408
|
+
a limit meant for cameras that never deliver anything at all, and has its
|
|
409
|
+
background keepalive stopped for good.
|
|
410
|
+
"""
|
|
411
|
+
if healthy:
|
|
412
|
+
return 0
|
|
413
|
+
return no_media_streak + 1
|
|
414
|
+
|
|
415
|
+
|
|
401
416
|
def _should_abandon_keepalive(no_media_streak: int, *, is_battery: bool,
|
|
402
417
|
limit: int = _FUTILE_KEEPALIVE_LIMIT) -> bool:
|
|
403
418
|
"""Should the background keepalive stop reopening this camera?
|
|
@@ -1808,13 +1823,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1808
1823
|
self.device_id, len(data))
|
|
1809
1824
|
elif isinstance(body, dict):
|
|
1810
1825
|
data = body.get("data") or {}
|
|
1826
|
+
# Keys, never values: this response can carry the per-device
|
|
1827
|
+
# streaming credentials (tutkAccount / tutkPassword - see
|
|
1828
|
+
# docs/DEFERRED_FEATURES.md), and the warning below is logged
|
|
1829
|
+
# at default level, so a whole-body dump lands in every user's
|
|
1830
|
+
# home-assistant.log. The matched-device log further down
|
|
1831
|
+
# already prints keys only; these two did not.
|
|
1811
1832
|
if data:
|
|
1812
|
-
_LOGGER.debug(
|
|
1813
|
-
|
|
1833
|
+
_LOGGER.debug(
|
|
1834
|
+
"batchGetDeviceUserInfo response for %s (status=%d):"
|
|
1835
|
+
" keys=%s, %d item(s)",
|
|
1836
|
+
self.device_id, status, sorted(body.keys()), len(data),
|
|
1837
|
+
)
|
|
1814
1838
|
else:
|
|
1815
1839
|
_LOGGER.warning(
|
|
1816
|
-
"batchGetDeviceUserInfo no data for %s (status=%d):
|
|
1817
|
-
self.device_id, status, body,
|
|
1840
|
+
"batchGetDeviceUserInfo no data for %s (status=%d): keys=%s",
|
|
1841
|
+
self.device_id, status, sorted(body.keys()),
|
|
1818
1842
|
)
|
|
1819
1843
|
else:
|
|
1820
1844
|
data = {}
|
|
@@ -2622,7 +2646,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2622
2646
|
_session.wait_done(),
|
|
2623
2647
|
timeout=_sdes_snap_seconds + 80,
|
|
2624
2648
|
)
|
|
2625
|
-
except
|
|
2649
|
+
except _asyncio.CancelledError:
|
|
2650
|
+
# Not the same event as a timeout. A timeout means the
|
|
2651
|
+
# camera was slow and whatever landed in the temp file is
|
|
2652
|
+
# worth salvaging; a cancellation means the caller is gone,
|
|
2653
|
+
# so salvaging it (another ffmpeg, up to 15s) is work nobody
|
|
2654
|
+
# is waiting for and it stops shutdown from completing.
|
|
2655
|
+
raise
|
|
2656
|
+
except TimeoutError:
|
|
2626
2657
|
pass
|
|
2627
2658
|
finally:
|
|
2628
2659
|
await _session.stop()
|
|
@@ -3599,6 +3630,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3599
3630
|
# Adaptive bookkeeping: a fast attempt that never delivered media
|
|
3600
3631
|
# latches the loop onto the full relay path for its remaining opens.
|
|
3601
3632
|
_healthy = session.last_media_monotonic > 0.0
|
|
3633
|
+
_no_media_streak = _next_no_media_streak(_no_media_streak, _healthy)
|
|
3602
3634
|
if _healthy:
|
|
3603
3635
|
# A session that delivered media is a real, cleanly-used session;
|
|
3604
3636
|
# the next open should be a fresh one rather than re-offering on a
|
|
@@ -3606,9 +3638,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3606
3638
|
_loop_peer_id = self.generate_webrtc_peer_id(
|
|
3607
3639
|
live_type=2, stream_id=0, sdes=True)
|
|
3608
3640
|
_peer_reuses = 0
|
|
3609
|
-
_no_media_streak = 0
|
|
3610
3641
|
else:
|
|
3611
|
-
_no_media_streak += 1
|
|
3612
3642
|
if _should_abandon_keepalive(
|
|
3613
3643
|
_no_media_streak, is_battery=self.is_battery_camera):
|
|
3614
3644
|
# Every open so far has cost the camera a wake and returned
|
{python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/lan_control.py
RENAMED
|
@@ -164,6 +164,26 @@ class CameraLanError(Exception):
|
|
|
164
164
|
"""A local control operation failed (connect, login, or protocol error)."""
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
class CameraLanLoginRejected(CameraLanError):
|
|
168
|
+
"""The device advertised local control and then refused our login.
|
|
169
|
+
|
|
170
|
+
Separate from its parent because the two mean opposite things to a caller.
|
|
171
|
+
A device that does not offer local control is ordinary and should be quiet;
|
|
172
|
+
a device that offers it, accepts the connection, decrypts our request with
|
|
173
|
+
its own key, and STILL answers with a non-200 ack is a defect - either in
|
|
174
|
+
what we send or in the device - and the caller should be able to say so out
|
|
175
|
+
loud rather than logging it at the same level as "not supported here".
|
|
176
|
+
|
|
177
|
+
``ack`` is the device's own code, because the refusals differ by family and
|
|
178
|
+
the number is the only handle on which one happened: A000088 cameras answer
|
|
179
|
+
4352, A001497 lights and A001535 plugs 4354, A001493 lights 400.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, message: str, ack: Any = None) -> None:
|
|
183
|
+
super().__init__(message)
|
|
184
|
+
self.ack = ack
|
|
185
|
+
|
|
186
|
+
|
|
167
187
|
class CameraLanClient:
|
|
168
188
|
"""Short-lived local device-control client for one camera.
|
|
169
189
|
|
|
@@ -291,9 +311,13 @@ class CameraLanClient:
|
|
|
291
311
|
) from exc
|
|
292
312
|
if ack != 200:
|
|
293
313
|
# The real device rejected our key-authenticated login; stop using
|
|
294
|
-
# the LAN path and revert to cloud.
|
|
314
|
+
# the LAN path and revert to cloud. Raised as the specific type so
|
|
315
|
+
# a caller can tell this apart from a device that never offered
|
|
316
|
+
# local control - see CameraLanLoginRejected.
|
|
295
317
|
self._eligible = False
|
|
296
|
-
raise
|
|
318
|
+
raise CameraLanLoginRejected(
|
|
319
|
+
f"{self.device_id}: login rejected ack={ack}", ack=ack
|
|
320
|
+
)
|
|
297
321
|
return (resp.get("payload") or {}).get("ascNumber", 1)
|
|
298
322
|
|
|
299
323
|
# -- public control ---------------------------------------------------- #
|
{python_aidot_cameras-0.17.2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -662,8 +662,14 @@ def _load_sprop(devid: str) -> "Optional[str]":
|
|
|
662
662
|
return None
|
|
663
663
|
|
|
664
664
|
|
|
665
|
-
def _save_sprop(devid: str, sprop: str) ->
|
|
666
|
-
"""Persist the ``sprop-parameter-sets``
|
|
665
|
+
def _save_sprop(devid: str, sprop: str) -> bool:
|
|
666
|
+
"""Persist the ``sprop-parameter-sets`` for ``devid``. True if it wrote.
|
|
667
|
+
|
|
668
|
+
Returns whether anything was actually cached, because two of the three
|
|
669
|
+
outcomes here write nothing and the caller announces the write in the only
|
|
670
|
+
log line this feature has. An unconditional announcement claims a cache
|
|
671
|
+
that an unstable camera provably does not have, which is the observable an
|
|
672
|
+
investigation reads first.
|
|
667
673
|
|
|
668
674
|
If a DIFFERENT value was already cached, this camera does not keep its
|
|
669
675
|
parameter sets stable: mark it and drop the cache, so nothing is injected
|
|
@@ -672,7 +678,7 @@ def _save_sprop(devid: str, sprop: str) -> None:
|
|
|
672
678
|
loses nothing by the injection being skipped.
|
|
673
679
|
"""
|
|
674
680
|
if _sprop_is_unstable(devid):
|
|
675
|
-
return
|
|
681
|
+
return False # already known unstable; nothing to cache for this camera
|
|
676
682
|
try:
|
|
677
683
|
prev = _load_sprop(devid)
|
|
678
684
|
if prev is not None and prev != sprop:
|
|
@@ -689,17 +695,19 @@ def _save_sprop(devid: str, sprop: str) -> None:
|
|
|
689
695
|
os.remove(_sprop_cache_path(devid))
|
|
690
696
|
except OSError:
|
|
691
697
|
pass
|
|
692
|
-
return
|
|
698
|
+
return False
|
|
693
699
|
os.makedirs(_SPROP_DIR, exist_ok=True)
|
|
694
700
|
tmp = _sprop_cache_path(devid) + ".tmp"
|
|
695
701
|
with open(tmp, "w") as fh:
|
|
696
702
|
fh.write(sprop)
|
|
697
703
|
os.replace(tmp, _sprop_cache_path(devid))
|
|
704
|
+
return True
|
|
698
705
|
except OSError as exc:
|
|
699
706
|
# Surface (don't swallow): if the cache dir isn't writable the whole
|
|
700
707
|
# sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
|
|
701
708
|
_LOGGER.warning("sprop cache write failed (%s): %s - set AIDOT_SPROP_DIR "
|
|
702
709
|
"to a writable path", _SPROP_DIR, exc)
|
|
710
|
+
return False
|
|
703
711
|
|
|
704
712
|
|
|
705
713
|
def _inject_sprop(sdp: str, devid: str) -> str:
|
|
@@ -133,12 +133,19 @@ class SdesSession(AvioRequestMixin):
|
|
|
133
133
|
def talk_supported(self) -> bool:
|
|
134
134
|
"""True if two-way audio can be used on this SDES session.
|
|
135
135
|
|
|
136
|
-
Requires a talk-capable open (offer advertised sendrecv + a=ssrc)
|
|
137
|
-
camera audio address is filled in by
|
|
138
|
-
|
|
139
|
-
the address before emitting.
|
|
136
|
+
Requires a talk-capable open (offer advertised sendrecv + a=ssrc) AND a
|
|
137
|
+
session that is still running. The camera audio address is filled in by
|
|
138
|
+
the bridge on first inbound audio; async_start_talk tolerates being
|
|
139
|
+
called before that - the pump waits for the address before emitting.
|
|
140
|
+
|
|
141
|
+
Liveness is part of the answer because the talk state is a plain dict
|
|
142
|
+
that outlives ffmpeg, while everything that acts on it - the bridge
|
|
143
|
+
thread that sends SPEAKERSTART, the socket the pump writes to - dies
|
|
144
|
+
with ffmpeg. ``async_speak`` reuses the warm session whenever this is
|
|
145
|
+
true, so answering from the dict alone routed talk into a session that
|
|
146
|
+
could no longer carry it instead of opening a fresh one.
|
|
140
147
|
"""
|
|
141
|
-
return bool(self._talk_state)
|
|
148
|
+
return bool(self._talk_state) and self.is_alive
|
|
142
149
|
|
|
143
150
|
async def async_start_talk(
|
|
144
151
|
self, pcm_provider: "Callable[[], Optional[bytes]]"
|
|
@@ -170,10 +177,23 @@ class SdesSession(AvioRequestMixin):
|
|
|
170
177
|
target=_run_sdes_talk_pump, args=(self._talk_state,), daemon=True
|
|
171
178
|
)
|
|
172
179
|
self._talk_thread.start()
|
|
173
|
-
|
|
180
|
+
accepted = await _speaker_ack_accepted(waiter, SDES_SPEAKER_ACK_TIMEOUT_S)
|
|
181
|
+
# _speaker_ack_accepted is generous by design: it treats an unfamiliar or
|
|
182
|
+
# absent ack as acceptance, because no genuine refusal has ever been
|
|
183
|
+
# identified and guessing at one would break working cameras. That
|
|
184
|
+
# generosity is about INTERPRETING the camera's answer, and it has
|
|
185
|
+
# nothing to say about the case where the question was never asked.
|
|
186
|
+
# `speaker_on` is set on exactly one line - immediately after the bridge
|
|
187
|
+
# thread dispatches SPEAKERSTART(848) - so a falsy flag here means our
|
|
188
|
+
# own bridge never sent the command, which happens whenever ffmpeg (and
|
|
189
|
+
# with it that thread) has already exited. The ack budget above already
|
|
190
|
+
# covers SDES_SPEAKERSTART_DELAY, so a live bridge has always set it by
|
|
191
|
+
# now and this cannot race a working session.
|
|
192
|
+
if accepted and self._talk_state.get("speaker_on"):
|
|
174
193
|
return True
|
|
175
|
-
#
|
|
176
|
-
#
|
|
194
|
+
# Either the camera refused, or the speaker command never left this
|
|
195
|
+
# process. Stop the microphone rather than stream viewer audio at a
|
|
196
|
+
# speaker that never opened.
|
|
177
197
|
self._talk_state["want_speaker"] = False
|
|
178
198
|
self._talk_state["provider"] = None
|
|
179
199
|
return False
|