python-aidot-cameras 0.15.1__tar.gz → 0.15.3__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.15.1/python_aidot_cameras.egg-info → python_aidot_cameras-0.15.3}/PKG-INFO +1 -1
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/client.py +5 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/controls.py +32 -2
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/sdes.py +19 -1
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/sdes_open.py +43 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/pyproject.toml +1 -1
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- python_aidot_cameras-0.15.3/tests/test_resolution_persists.py +95 -0
- python_aidot_cameras-0.15.3/tests/test_sdes_serve_stderr_drain.py +161 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/LICENSE +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/README.md +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/setup.cfg +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.15.1/python_aidot_cameras.egg-info → python_aidot_cameras-0.15.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.3
|
|
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
|
|
@@ -867,6 +867,8 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
867
867
|
self.latest_jpeg: Optional[bytes] = None
|
|
868
868
|
self._streaming_active: bool = False
|
|
869
869
|
self._stream_session: Optional[Any] = None
|
|
870
|
+
# Last resolution asked for, re-sent when a session starts.
|
|
871
|
+
self._desired_quality: Optional[str] = None
|
|
870
872
|
self._stream_task: Optional["asyncio.Task[None]"] = None
|
|
871
873
|
self._stream_mqtt_drain: Optional["asyncio.Future"] = None
|
|
872
874
|
self._last_frame_time: float = 0.0
|
|
@@ -3354,6 +3356,8 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3354
3356
|
|
|
3355
3357
|
self._fast_attempt_override = None
|
|
3356
3358
|
self._stream_session = session
|
|
3359
|
+
# A remembered resolution is only deliverable once a session exists.
|
|
3360
|
+
self._apply_pending_resolution()
|
|
3357
3361
|
# Don't block solely on wait_done(): the SDES ffmpeg reads RTP over a
|
|
3358
3362
|
# UDP socket with no input timeout, so when a battery camera tears the
|
|
3359
3363
|
# session down (~49-72s) ffmpeg keeps waiting on dead sockets forever
|
|
@@ -3564,6 +3568,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3564
3568
|
continue
|
|
3565
3569
|
|
|
3566
3570
|
self._stream_session = session
|
|
3571
|
+
self._apply_pending_resolution()
|
|
3567
3572
|
try:
|
|
3568
3573
|
while self._streaming_active:
|
|
3569
3574
|
await asyncio.sleep(5.0)
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/controls.py
RENAMED
|
@@ -144,6 +144,13 @@ class _CameraControlsMixin:
|
|
|
144
144
|
mirroring the official app's HD/SD toggle. Rides the active stream
|
|
145
145
|
session (SETSTREAMCTRL=800), so the camera must be streaming.
|
|
146
146
|
|
|
147
|
+
The choice is REMEMBERED and re-sent whenever a session next starts.
|
|
148
|
+
Without that it was simply dropped when nothing was streaming - and
|
|
149
|
+
since a camera is idle far more often than not, the usual outcome of
|
|
150
|
+
changing this was nothing at all, while the caller was told the setting
|
|
151
|
+
had been applied. Nothing else re-sent it, so "it will apply on the next
|
|
152
|
+
session" was not true of any code path.
|
|
153
|
+
|
|
147
154
|
Source: f0.java g3() -> X2(800, SetStreamCtrlReq.parseContent(0, quality)).
|
|
148
155
|
Payload <IB3x> = channel(0) + quality byte + 3 reserved.
|
|
149
156
|
"""
|
|
@@ -151,14 +158,37 @@ class _CameraControlsMixin:
|
|
|
151
158
|
if q is None:
|
|
152
159
|
_LOGGER.error("async_set_resolution: unknown quality %r", quality)
|
|
153
160
|
return False
|
|
161
|
+
self._desired_quality = quality.lower()
|
|
154
162
|
payload = struct.pack("<IB3x", 0, q)
|
|
155
163
|
session = self._stream_session
|
|
156
164
|
if session is None:
|
|
157
|
-
_LOGGER.
|
|
158
|
-
|
|
165
|
+
_LOGGER.info(
|
|
166
|
+
"resolution %s remembered; the camera is not streaming, so it "
|
|
167
|
+
"will be applied when a session next starts", quality)
|
|
168
|
+
return True
|
|
159
169
|
ok = session._avio_cmd(SETSTREAMCTRL_CMD, payload)
|
|
160
170
|
_LOGGER.debug(
|
|
161
171
|
"set resolution %s (quality=%d) -> %s",
|
|
162
172
|
quality, q, "sent" if ok else "no channel yet",
|
|
163
173
|
)
|
|
164
174
|
return ok
|
|
175
|
+
|
|
176
|
+
def _apply_pending_resolution(self) -> None:
|
|
177
|
+
"""Re-send the remembered resolution now that a session exists.
|
|
178
|
+
|
|
179
|
+
Called when a stream session becomes active. Best-effort and silent on
|
|
180
|
+
failure: a stream that is up matters more than a quality preference, so
|
|
181
|
+
this must never be able to disturb it.
|
|
182
|
+
"""
|
|
183
|
+
quality = getattr(self, "_desired_quality", None)
|
|
184
|
+
if not quality:
|
|
185
|
+
return
|
|
186
|
+
q = _STREAM_QUALITY.get(quality)
|
|
187
|
+
session = self._stream_session
|
|
188
|
+
if q is None or session is None:
|
|
189
|
+
return
|
|
190
|
+
try:
|
|
191
|
+
if session._avio_cmd(SETSTREAMCTRL_CMD, struct.pack("<IB3x", 0, q)):
|
|
192
|
+
_LOGGER.debug("re-applied remembered resolution %s", quality)
|
|
193
|
+
except Exception:
|
|
194
|
+
_LOGGER.debug("could not re-apply resolution %s", quality, exc_info=True)
|
|
@@ -177,6 +177,24 @@ class SdesSession:
|
|
|
177
177
|
"audio_pt": self._first_audio_pt[0],
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
def _drained_stderr_tail(self) -> bytes:
|
|
181
|
+
"""The stderr the drain thread already consumed, for the teardown log.
|
|
182
|
+
|
|
183
|
+
The serve's stderr is drained continuously on a daemon thread (see
|
|
184
|
+
``_start_serve_stderr_drain`` in sdes_open) so an un-drained pipe cannot
|
|
185
|
+
fill and stall ffmpeg. A consequence is that by teardown the pipe is at
|
|
186
|
+
EOF and stop()'s ``stderr.read()`` returns nothing, which would leave
|
|
187
|
+
``_log_ffmpeg_stderr`` with an empty buffer and silently drop the
|
|
188
|
+
teardown diagnostic - the mid-stream ffmpeg error it exists to surface
|
|
189
|
+
included. Fall back to the bounded tail the drainer kept. Returns
|
|
190
|
+
``b""`` when there is no drainer or it captured nothing, which
|
|
191
|
+
``_log_ffmpeg_stderr`` already treats as "nothing to say".
|
|
192
|
+
"""
|
|
193
|
+
tail = getattr(self._proc, "_aidot_stderr_tail", None)
|
|
194
|
+
if not tail:
|
|
195
|
+
return b""
|
|
196
|
+
return "\n".join(tail).encode("utf-8", "replace")
|
|
197
|
+
|
|
180
198
|
def _log_ffmpeg_stderr(self, stderr_bytes: bytes) -> None:
|
|
181
199
|
"""Log ffmpeg's captured stderr at a level that matches what happened.
|
|
182
200
|
|
|
@@ -285,7 +303,7 @@ class SdesSession:
|
|
|
285
303
|
self._proc.stderr.close()
|
|
286
304
|
except Exception:
|
|
287
305
|
_LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
|
|
288
|
-
self._log_ffmpeg_stderr(stderr_bytes)
|
|
306
|
+
self._log_ffmpeg_stderr(stderr_bytes or self._drained_stderr_tail())
|
|
289
307
|
import os
|
|
290
308
|
try:
|
|
291
309
|
os.unlink(self._sdp_path)
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -38,6 +38,38 @@ from .protocol import (
|
|
|
38
38
|
|
|
39
39
|
_LOGGER = logging.getLogger(__name__)
|
|
40
40
|
|
|
41
|
+
|
|
42
|
+
def _start_serve_stderr_drain(proc, *, maxlines: int = 40) -> None:
|
|
43
|
+
"""Drain a serve ffmpeg's stderr continuously into a bounded tail on the proc.
|
|
44
|
+
|
|
45
|
+
The serve is spawned with ``stderr=PIPE`` but the bridge loop only polls the
|
|
46
|
+
process and reads its RTP sockets - it never reads stderr. An un-drained
|
|
47
|
+
``PIPE`` fills its ~64KB kernel buffer and then blocks ffmpeg on its next
|
|
48
|
+
stderr write, stalling the serve; and when ffmpeg exits non-zero the reason is
|
|
49
|
+
otherwise lost (the RTSP-push ANNOUNCE error in particular). Read it on a
|
|
50
|
+
daemon thread that ends at EOF when the process exits, keeping the last
|
|
51
|
+
``maxlines`` lines on ``proc._aidot_stderr_tail`` for the exit logger.
|
|
52
|
+
"""
|
|
53
|
+
import collections
|
|
54
|
+
import threading
|
|
55
|
+
|
|
56
|
+
tail = collections.deque(maxlen=maxlines)
|
|
57
|
+
proc._aidot_stderr_tail = tail
|
|
58
|
+
if proc.stderr is None:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
def _drain() -> None:
|
|
62
|
+
try:
|
|
63
|
+
for _line in iter(proc.stderr.readline, b""):
|
|
64
|
+
tail.append(_line.decode("utf-8", "replace").rstrip())
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
threading.Thread(
|
|
69
|
+
target=_drain, daemon=True, name="aidot-sdes-serve-stderr"
|
|
70
|
+
).start()
|
|
71
|
+
|
|
72
|
+
|
|
41
73
|
# Strong references to detached per-session helper tasks, so the event loop
|
|
42
74
|
# cannot garbage-collect one mid-flight. Entries remove themselves on done.
|
|
43
75
|
_SDES_BACKGROUND_TASKS: set = set()
|
|
@@ -2803,6 +2835,15 @@ class _SdesOpenMixin:
|
|
|
2803
2835
|
_log_br.getLogger(__name__).log(
|
|
2804
2836
|
_br_level, _br_msg, _br_rc
|
|
2805
2837
|
)
|
|
2838
|
+
if _br_level >= _log_br.WARNING:
|
|
2839
|
+
_serr_tail = getattr(
|
|
2840
|
+
_br_proc, "_aidot_stderr_tail", None)
|
|
2841
|
+
if _serr_tail:
|
|
2842
|
+
_log_br.getLogger(__name__).warning(
|
|
2843
|
+
"SDES serve ffmpeg stderr (last %d"
|
|
2844
|
+
" lines):\n%s", len(_serr_tail),
|
|
2845
|
+
"\n".join(_serr_tail),
|
|
2846
|
+
)
|
|
2806
2847
|
if _bridge_should_break(
|
|
2807
2848
|
_br_rc, _br_teardown_requested
|
|
2808
2849
|
):
|
|
@@ -4462,6 +4503,7 @@ class _SdesOpenMixin:
|
|
|
4462
4503
|
stdout=subprocess.DEVNULL,
|
|
4463
4504
|
stderr=subprocess.PIPE,
|
|
4464
4505
|
)
|
|
4506
|
+
_start_serve_stderr_drain(proc)
|
|
4465
4507
|
_proc_holder[0] = proc
|
|
4466
4508
|
_cl(_reap, proc) # kill ffmpeg if the open is cancelled before hand-off
|
|
4467
4509
|
except FileNotFoundError:
|
|
@@ -4745,6 +4787,7 @@ class _SdesOpenMixin:
|
|
|
4745
4787
|
stdout=subprocess.DEVNULL,
|
|
4746
4788
|
stderr=subprocess.PIPE,
|
|
4747
4789
|
)
|
|
4790
|
+
_start_serve_stderr_drain(proc)
|
|
4748
4791
|
# Point the shared holder at the live proc immediately.
|
|
4749
4792
|
# The bridge thread polls _proc_holder[0]; if it still
|
|
4750
4793
|
# sees the terminated old proc it logs "stream ended",
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.3"
|
|
8
8
|
description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.3
|
|
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
|
|
@@ -125,6 +125,7 @@ tests/test_persistent_mqtt.py
|
|
|
125
125
|
tests/test_persistent_mqtt_robustness.py
|
|
126
126
|
tests/test_playback_tls.py
|
|
127
127
|
tests/test_post_merge_hardening.py
|
|
128
|
+
tests/test_resolution_persists.py
|
|
128
129
|
tests/test_restart_cycle.py
|
|
129
130
|
tests/test_retry_policy.py
|
|
130
131
|
tests/test_scripts_importable.py
|
|
@@ -137,6 +138,7 @@ tests/test_sdes_idle_release.py
|
|
|
137
138
|
tests/test_sdes_open_cleanup.py
|
|
138
139
|
tests/test_sdes_serve_audio.py
|
|
139
140
|
tests/test_sdes_serve_cmd.py
|
|
141
|
+
tests/test_sdes_serve_stderr_drain.py
|
|
140
142
|
tests/test_sdes_sprop.py
|
|
141
143
|
tests/test_sdes_talk.py
|
|
142
144
|
tests/test_sdes_teardown_exit_log.py
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""A resolution choice must survive being made while nothing is streaming.
|
|
2
|
+
|
|
3
|
+
The setter rides the live session (SETSTREAMCTRL): with no session there is
|
|
4
|
+
nothing to send it over. It used to return False and drop the choice entirely,
|
|
5
|
+
and nothing re-sent it - so the usual outcome of changing resolution was
|
|
6
|
+
nothing at all, because a camera is idle far more often than it is streaming.
|
|
7
|
+
The caller was told the setting had been applied, and the comment in the
|
|
8
|
+
integration's select claimed it would "take effect on the next session", which
|
|
9
|
+
no code path did.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from unittest.mock import MagicMock
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from aidot_cameras.camera.constants import _STREAM_QUALITY, SETSTREAMCTRL_CMD
|
|
21
|
+
from aidot_cameras.camera.controls import _CameraControlsMixin
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _Cam(_CameraControlsMixin):
|
|
25
|
+
def __init__(self, session=None):
|
|
26
|
+
self._stream_session = session
|
|
27
|
+
self._desired_quality = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _session():
|
|
31
|
+
s = MagicMock()
|
|
32
|
+
s._avio_cmd = MagicMock(return_value=True)
|
|
33
|
+
return s
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_choice_made_while_idle_is_remembered_not_dropped():
|
|
38
|
+
cam = _Cam(session=None)
|
|
39
|
+
|
|
40
|
+
assert await cam.async_set_resolution("sd") is True
|
|
41
|
+
assert cam._desired_quality == "sd"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.asyncio
|
|
45
|
+
async def test_remembered_choice_is_sent_when_a_session_starts():
|
|
46
|
+
cam = _Cam(session=None)
|
|
47
|
+
await cam.async_set_resolution("sd")
|
|
48
|
+
|
|
49
|
+
cam._stream_session = _session()
|
|
50
|
+
cam._apply_pending_resolution()
|
|
51
|
+
|
|
52
|
+
cam._stream_session._avio_cmd.assert_called_once()
|
|
53
|
+
cmd, payload = cam._stream_session._avio_cmd.call_args[0]
|
|
54
|
+
assert cmd == SETSTREAMCTRL_CMD
|
|
55
|
+
assert payload[4] == _STREAM_QUALITY["sd"] # AVIOCTRL_QUALITY_MIDDLE
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_hd_round_trips_too():
|
|
60
|
+
cam = _Cam(session=None)
|
|
61
|
+
await cam.async_set_resolution("hd")
|
|
62
|
+
cam._stream_session = _session()
|
|
63
|
+
cam._apply_pending_resolution()
|
|
64
|
+
assert cam._stream_session._avio_cmd.call_args[0][1][4] == _STREAM_QUALITY["hd"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_a_live_session_still_gets_it_immediately():
|
|
69
|
+
"""Unchanged behaviour when the camera is already streaming."""
|
|
70
|
+
cam = _Cam(session=_session())
|
|
71
|
+
|
|
72
|
+
assert await cam.async_set_resolution("sd") is True
|
|
73
|
+
cam._stream_session._avio_cmd.assert_called_once()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_nothing_is_sent_when_no_choice_was_ever_made():
|
|
77
|
+
cam = _Cam(session=_session())
|
|
78
|
+
cam._apply_pending_resolution()
|
|
79
|
+
cam._stream_session._avio_cmd.assert_not_called()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_an_unknown_quality_is_still_rejected():
|
|
84
|
+
cam = _Cam(session=_session())
|
|
85
|
+
assert await cam.async_set_resolution("ultra") is False
|
|
86
|
+
assert cam._desired_quality is None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_reapply_never_disturbs_a_working_stream():
|
|
90
|
+
"""A quality preference must never be able to break a stream that is up."""
|
|
91
|
+
cam = _Cam(session=_session())
|
|
92
|
+
cam._desired_quality = "sd"
|
|
93
|
+
cam._stream_session._avio_cmd.side_effect = RuntimeError("channel gone")
|
|
94
|
+
|
|
95
|
+
cam._apply_pending_resolution() # must not raise
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Tests for the SDES serve ffmpeg stderr drain.
|
|
2
|
+
|
|
3
|
+
The serve is spawned with ``stderr=PIPE`` but the bridge loop never reads it, so an
|
|
4
|
+
un-drained pipe can fill and stall ffmpeg and a non-zero exit loses its reason.
|
|
5
|
+
``_start_serve_stderr_drain`` reads the pipe on a daemon thread and keeps a bounded
|
|
6
|
+
tail on ``proc._aidot_stderr_tail`` for the exit logger.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import io
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from aidot_cameras.camera.sdes import SdesSession
|
|
15
|
+
from aidot_cameras.camera.sdes_open import _start_serve_stderr_drain
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _FakeProc:
|
|
19
|
+
"""Minimal stand-in exposing a readable ``stderr``, like ``subprocess.Popen``."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, data: bytes) -> None:
|
|
22
|
+
self.stderr = io.BytesIO(data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _tail(proc) -> list:
|
|
26
|
+
"""The captured stderr tail (attribute is set dynamically by the drainer)."""
|
|
27
|
+
return list(getattr(proc, "_aidot_stderr_tail"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _wait_until(pred, timeout: float = 2.0) -> None:
|
|
31
|
+
deadline = time.monotonic() + timeout
|
|
32
|
+
while not pred():
|
|
33
|
+
if time.monotonic() > deadline:
|
|
34
|
+
break
|
|
35
|
+
time.sleep(0.01)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_stderr_drain_captures_lines() -> None:
|
|
39
|
+
proc = _FakeProc(b"line one\nline two\nline three\n")
|
|
40
|
+
_start_serve_stderr_drain(proc)
|
|
41
|
+
_wait_until(lambda: _tail(proc)[-1:] == ["line three"])
|
|
42
|
+
assert _tail(proc) == ["line one", "line two", "line three"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_stderr_drain_is_bounded_to_the_tail() -> None:
|
|
46
|
+
data = b"".join(f"l{i}\n".encode() for i in range(100))
|
|
47
|
+
proc = _FakeProc(data)
|
|
48
|
+
_start_serve_stderr_drain(proc, maxlines=10)
|
|
49
|
+
_wait_until(lambda: _tail(proc)[-1:] == ["l99"])
|
|
50
|
+
assert _tail(proc) == [f"l{i}" for i in range(90, 100)]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_stderr_none_is_safe() -> None:
|
|
54
|
+
class _NoStderr:
|
|
55
|
+
stderr = None
|
|
56
|
+
|
|
57
|
+
proc = _NoStderr()
|
|
58
|
+
_start_serve_stderr_drain(proc) # must not raise
|
|
59
|
+
assert _tail(proc) == []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _TornDownProc:
|
|
63
|
+
"""A serve proc whose stderr pipe the drainer already emptied.
|
|
64
|
+
|
|
65
|
+
``stop()`` reads ``stderr`` after the process is gone; with the drain thread
|
|
66
|
+
running that read returns nothing, so the teardown diagnostic has to come
|
|
67
|
+
from the tail the drainer kept.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, tail: list) -> None:
|
|
71
|
+
self._alive = True
|
|
72
|
+
self.returncode = 0
|
|
73
|
+
self.stderr = self
|
|
74
|
+
self._aidot_stderr_tail = tail
|
|
75
|
+
|
|
76
|
+
def poll(self):
|
|
77
|
+
return None if self._alive else self.returncode
|
|
78
|
+
|
|
79
|
+
def terminate(self):
|
|
80
|
+
self._alive = False
|
|
81
|
+
|
|
82
|
+
def wait(self, timeout=None):
|
|
83
|
+
self._alive = False
|
|
84
|
+
return self.returncode
|
|
85
|
+
|
|
86
|
+
def kill(self):
|
|
87
|
+
self._alive = False
|
|
88
|
+
|
|
89
|
+
def read(self): # stderr.read() - the drainer got there first
|
|
90
|
+
return b""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _FakeSock:
|
|
94
|
+
def close(self) -> None:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _FakeQ:
|
|
99
|
+
def put_nowait(self, _x) -> None:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _stopped_session(tail: list) -> None:
|
|
104
|
+
"""Run the real ``SdesSession.stop()`` against a drained serve proc."""
|
|
105
|
+
session = SdesSession(
|
|
106
|
+
proc=_TornDownProc(tail),
|
|
107
|
+
sdp_path="/tmp/aidot_test_does_not_exist.sdp",
|
|
108
|
+
outgoing_q=_FakeQ(),
|
|
109
|
+
mqtt_fut=None,
|
|
110
|
+
audio_sock=_FakeSock(),
|
|
111
|
+
video_sock=_FakeSock(),
|
|
112
|
+
cmd_chan=[None],
|
|
113
|
+
talk_state=None,
|
|
114
|
+
)
|
|
115
|
+
asyncio.run(session.stop())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_teardown_logs_the_drained_tail_when_the_pipe_is_empty(caplog) -> None:
|
|
119
|
+
# Without the fallback this diagnostic is lost entirely: the drain thread
|
|
120
|
+
# consumed the pipe, so stop()'s stderr.read() has nothing left to log.
|
|
121
|
+
caplog.set_level(logging.DEBUG, logger="aidot_cameras.camera.sdes")
|
|
122
|
+
_stopped_session(["frame= 120 fps=20", "Error writing trailer: Broken pipe"])
|
|
123
|
+
warnings = [
|
|
124
|
+
r for r in caplog.records
|
|
125
|
+
if r.levelno == logging.WARNING and "ffmpeg SDES stderr" in r.getMessage()
|
|
126
|
+
]
|
|
127
|
+
assert warnings, "a mid-stream ffmpeg error must still reach the log"
|
|
128
|
+
assert "Error writing trailer: Broken pipe" in warnings[0].getMessage()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_teardown_tail_still_demotes_the_expected_no_media_shape(caplog) -> None:
|
|
132
|
+
# The tail feeds the SAME classifier, so a camera that never sent media does
|
|
133
|
+
# not start warning once per retry just because the stderr route changed.
|
|
134
|
+
caplog.set_level(logging.DEBUG, logger="aidot_cameras.camera.sdes")
|
|
135
|
+
_stopped_session(["Output file is empty, nothing was encoded"])
|
|
136
|
+
assert not [
|
|
137
|
+
r for r in caplog.records
|
|
138
|
+
if r.levelno == logging.WARNING and "ffmpeg SDES stderr" in r.getMessage()
|
|
139
|
+
]
|
|
140
|
+
assert [
|
|
141
|
+
r for r in caplog.records
|
|
142
|
+
if r.levelno == logging.DEBUG and "ffmpeg SDES stderr" in r.getMessage()
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_teardown_without_a_tail_logs_nothing(caplog) -> None:
|
|
147
|
+
caplog.set_level(logging.DEBUG, logger="aidot_cameras.camera.sdes")
|
|
148
|
+
_stopped_session([])
|
|
149
|
+
assert not [
|
|
150
|
+
r for r in caplog.records if "ffmpeg SDES stderr" in r.getMessage()
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_undecodable_bytes_do_not_crash_the_drain() -> None:
|
|
155
|
+
proc = _FakeProc(b"good\n\xff\xfe bad bytes\ndone\n")
|
|
156
|
+
_start_serve_stderr_drain(proc)
|
|
157
|
+
_wait_until(lambda: _tail(proc)[-1:] == ["done"])
|
|
158
|
+
tail = _tail(proc)
|
|
159
|
+
assert tail[0] == "good"
|
|
160
|
+
assert tail[-1] == "done"
|
|
161
|
+
assert len(tail) == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/clock.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_answer_sdp_video_pt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_constants_imports_resolve.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dormant_camera_cleanup.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_mux_terminal_write.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_serve_open_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_ice_peer_reflexive.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_idle_release_both_loops.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_light_active_color_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_live_validate_policy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_credential_refetch.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_credential_self_heal.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_no_self_referential_go2rtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_scripts_importable.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_bridge_break_gate.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_ffmpeg_stderr_log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdes_terminal_ack_latency.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdp_narrowing_properties.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_sdp_payload_type_narrowing.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_stream_drain_ownership.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_turn_create_permission.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_viewer_check_throttle.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_viewer_present_push_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.15.1 → python_aidot_cameras-0.15.3}/tests/test_wake_readiness_retry.py
RENAMED
|
File without changes
|