python-aidot-cameras 0.15.0b2__tar.gz → 0.15.1__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.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-0.15.1}/PKG-INFO +3 -1
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/README.md +2 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/client.py +16 -0
- python_aidot_cameras-0.15.1/aidot_cameras/camera/hwaccel.py +457 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/sdes_open.py +11 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/pyproject.toml +1 -1
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1/python_aidot_cameras.egg-info}/PKG-INFO +3 -1
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- python_aidot_cameras-0.15.1/tests/test_hwaccel_probe.py +322 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/LICENSE +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/setup.cfg +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.15.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-0.15.1}/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.1
|
|
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
|
|
@@ -167,6 +167,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
167
167
|
|
|
168
168
|
| Variable | Purpose | Default |
|
|
169
169
|
| --- | --- | --- |
|
|
170
|
+
| `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
|
|
171
|
+
| `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
|
|
170
172
|
| `AIDOT_MAX_CONCURRENT_OPENS` | Caps how many stream opens run concurrently across all cameras. | `2` |
|
|
171
173
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
172
174
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
@@ -136,6 +136,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
136
136
|
|
|
137
137
|
| Variable | Purpose | Default |
|
|
138
138
|
| --- | --- | --- |
|
|
139
|
+
| `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
|
|
140
|
+
| `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
|
|
139
141
|
| `AIDOT_MAX_CONCURRENT_OPENS` | Caps how many stream opens run concurrently across all cameras. | `2` |
|
|
140
142
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
141
143
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
{python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/client.py
RENAMED
|
@@ -374,6 +374,7 @@ def _build_sdes_serve_cmd(
|
|
|
374
374
|
sdes_audio: bool = False,
|
|
375
375
|
audio_gain_db: float = -8.0,
|
|
376
376
|
push_video_only: bool = False,
|
|
377
|
+
video_decoder: Optional[list] = None,
|
|
377
378
|
) -> list:
|
|
378
379
|
"""Build the ffmpeg argv for the SDES bridge serve.
|
|
379
380
|
|
|
@@ -447,10 +448,25 @@ def _build_sdes_serve_cmd(
|
|
|
447
448
|
dest_args = ["-c", "copy", *time_args, output_path]
|
|
448
449
|
else:
|
|
449
450
|
dest_args = [*time_args, "-f", "null", "/dev/null"]
|
|
451
|
+
|
|
452
|
+
# Decoder selection applies to the ``-f null`` drain alone - it is the only
|
|
453
|
+
# destination that decodes; every other one is ``-c copy``. The caller
|
|
454
|
+
# passes ready-made input arguments, either ``["-hwaccel", m]`` or
|
|
455
|
+
# ``["-c:v", name]``, and they must sit BEFORE ``-i``: after ``-i`` ffmpeg
|
|
456
|
+
# reads ``-c:v`` as an ENCODER, and ``-hwaccel`` is an input option that has
|
|
457
|
+
# no meaning there at all. Empty or None means ffmpeg chooses, as always. Keep the decode: it is what proves frames are not merely
|
|
458
|
+
# arriving but decodable, which is what catches a parameter-set mismatch
|
|
459
|
+
# that otherwise presents as a permanently black picture.
|
|
460
|
+
decode_args = (
|
|
461
|
+
list(video_decoder)
|
|
462
|
+
if video_decoder and not rtsp_push_url and not output_path
|
|
463
|
+
else []
|
|
464
|
+
)
|
|
450
465
|
return [
|
|
451
466
|
"ffmpeg", "-y",
|
|
452
467
|
"-loglevel", "warning",
|
|
453
468
|
"-protocol_whitelist", "file,rtp,udp,srtp",
|
|
469
|
+
*decode_args,
|
|
454
470
|
# 2 s analyzeduration: the camera sends SPS+PPS+IDR in the first burst;
|
|
455
471
|
# 15 s consumed nearly all packets during analysis. PLI re-arms an IDR
|
|
456
472
|
# every 5 s so parameters always re-arrive.
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Pick the fastest video decoder this host can actually use.
|
|
2
|
+
|
|
3
|
+
Enumeration is not enough. ``ffmpeg -decoders`` lists what the binary was
|
|
4
|
+
COMPILED with, not what works on the machine: a Raspberry Pi 4 happily advertises
|
|
5
|
+
``h264_cuvid`` and ``hevc_cuvid`` with no Nvidia hardware anywhere, and the same
|
|
6
|
+
Pi advertises ``h264_v4l2m2m`` while failing to open it ("No such file or
|
|
7
|
+
directory") even with the kernel device present and the user in the ``video``
|
|
8
|
+
group. Selecting a decoder from that list would not accelerate decoding, it would
|
|
9
|
+
stop decoding working at all.
|
|
10
|
+
|
|
11
|
+
So every candidate is PROVEN before it is offered: a clip at the resolution the
|
|
12
|
+
cameras actually send is generated and decoded with it, and only a clean run
|
|
13
|
+
that produces frames qualifies. The winner is the fastest qualifying candidate,
|
|
14
|
+
which is a hardware path when one genuinely works and software otherwise.
|
|
15
|
+
|
|
16
|
+
Nor is hardware automatically faster. Measured on an Apple M1, VideoToolbox
|
|
17
|
+
decodes H.264 about three times SLOWER than software, in the same pipeline
|
|
18
|
+
production runs. Ranking by measured time rather than by preference is what
|
|
19
|
+
keeps that from becoming a regression sold as an optimisation.
|
|
20
|
+
|
|
21
|
+
Probing costs a second or two per codec, so the verdict is cached on disk. The
|
|
22
|
+
cache key includes the ffmpeg build identity, so upgrading ffmpeg re-probes
|
|
23
|
+
rather than trusting a verdict made about a different binary.
|
|
24
|
+
|
|
25
|
+
Where this matters: the streaming paths use ``-c copy`` and never decode, so they
|
|
26
|
+
are unaffected by design. The decode cost is in the diagnostic/validation path
|
|
27
|
+
(``-f null``), which decodes deliberately - that is what proves a stream is not
|
|
28
|
+
merely arriving but actually decodable, and it is what catches a parameter-set
|
|
29
|
+
mismatch that would otherwise present as a permanently black picture.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import json
|
|
34
|
+
import logging
|
|
35
|
+
import os
|
|
36
|
+
import glob
|
|
37
|
+
import subprocess
|
|
38
|
+
import sys
|
|
39
|
+
import threading
|
|
40
|
+
import time
|
|
41
|
+
from typing import List, Optional
|
|
42
|
+
|
|
43
|
+
from .protocol import _SPROP_DIR # reuse the same per-user cache root
|
|
44
|
+
|
|
45
|
+
_LOGGER = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# Ways to decode, best first; the empty list is plain software and always works.
|
|
48
|
+
#
|
|
49
|
+
# There are TWO forms and they are not interchangeable:
|
|
50
|
+
#
|
|
51
|
+
# ["-c:v", "<name>"] a named decoder, e.g. h264_v4l2m2m on a Pi
|
|
52
|
+
# ["-hwaccel", "<method>"] an acceleration method, e.g. videotoolbox on macOS
|
|
53
|
+
#
|
|
54
|
+
# Getting this wrong silently costs the whole feature. VideoToolbox and VAAPI
|
|
55
|
+
# expose no decoder at all - ffmpeg lists h264_videotoolbox and h264_vaapi as
|
|
56
|
+
# ENCODERS - so naming them with -c:v can never work, and every macOS and VAAPI
|
|
57
|
+
# machine would fall back to software while genuinely having hardware decoding
|
|
58
|
+
# available through -hwaccel. Verified on an Apple M1 (videotoolbox appears in
|
|
59
|
+
# -hwaccels, nowhere in -decoders) and on a Pi 4 (same for vaapi).
|
|
60
|
+
#
|
|
61
|
+
# Both forms go before -i. Ordering is only a starting preference; whichever
|
|
62
|
+
# proves fastest wins, and anything that cannot decode is dropped.
|
|
63
|
+
_CANDIDATES = {
|
|
64
|
+
"h264": [
|
|
65
|
+
["-hwaccel", "videotoolbox"], # macOS / Apple silicon
|
|
66
|
+
["-hwaccel", "vaapi"], # generic Linux (Intel/AMD)
|
|
67
|
+
["-c:v", "h264_v4l2m2m"], # Raspberry Pi 4 / many ARM SoCs
|
|
68
|
+
["-c:v", "h264_rkmpp"], # Rockchip
|
|
69
|
+
["-c:v", "h264_qsv"], # Intel QuickSync
|
|
70
|
+
["-c:v", "h264_cuvid"], # Nvidia
|
|
71
|
+
[], # software
|
|
72
|
+
],
|
|
73
|
+
"hevc": [
|
|
74
|
+
["-hwaccel", "videotoolbox"],
|
|
75
|
+
["-hwaccel", "vaapi"],
|
|
76
|
+
["-c:v", "hevc_v4l2m2m"],
|
|
77
|
+
["-c:v", "hevc_rkmpp"],
|
|
78
|
+
["-c:v", "hevc_qsv"],
|
|
79
|
+
["-c:v", "hevc_cuvid"],
|
|
80
|
+
[],
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Bumped when the shape of a cached verdict changes, so an older entry is
|
|
85
|
+
# re-probed instead of being read as something it is not.
|
|
86
|
+
_CACHE_SCHEMA = 2
|
|
87
|
+
|
|
88
|
+
# Force a choice and skip probing. Takes either form: a bare name is a decoder
|
|
89
|
+
# ("h264_v4l2m2m"), a "hwaccel:" prefix an acceleration method
|
|
90
|
+
# ("hwaccel:videotoolbox"). The prefix is needed because the two are not
|
|
91
|
+
# interchangeable - VideoToolbox and VAAPI have no decoder to name.
|
|
92
|
+
_ENV_OVERRIDE = "AIDOT_VIDEO_DECODER"
|
|
93
|
+
_ENV_DISABLE = "AIDOT_DISABLE_HWACCEL" # stay on software decoding
|
|
94
|
+
|
|
95
|
+
_PROBE_TIMEOUT_S = 40.0
|
|
96
|
+
_cache_mem: dict = {}
|
|
97
|
+
_warm_thread: Optional["threading.Thread"] = None
|
|
98
|
+
_warm_lock = threading.Lock()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _ffmpeg() -> str:
|
|
102
|
+
return os.environ.get("AIDOT_FFMPEG", "ffmpeg")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_identity_memo: Optional[str] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _ffmpeg_identity() -> str:
|
|
109
|
+
"""Identify the ffmpeg build, so a verdict never outlives the binary.
|
|
110
|
+
|
|
111
|
+
Derived by STATTING the binary (path, size, mtime) rather than running
|
|
112
|
+
``ffmpeg -version``. Reading a cached verdict has to be nearly free: this is
|
|
113
|
+
consulted on a startup path, and shelling out cost ~3.4s per process on a
|
|
114
|
+
loaded Pi - which is most of a cache hit's budget spent proving the cache is
|
|
115
|
+
still valid. An upgrade changes size or mtime and re-probes, which is the
|
|
116
|
+
property that actually matters.
|
|
117
|
+
"""
|
|
118
|
+
global _identity_memo
|
|
119
|
+
if _identity_memo is not None:
|
|
120
|
+
return _identity_memo
|
|
121
|
+
import shutil
|
|
122
|
+
ident = "unknown"
|
|
123
|
+
try:
|
|
124
|
+
path = shutil.which(_ffmpeg()) or _ffmpeg()
|
|
125
|
+
st = os.stat(path)
|
|
126
|
+
ident = f"{path}:{st.st_size}:{int(st.st_mtime)}"
|
|
127
|
+
except Exception:
|
|
128
|
+
# No resolvable binary: fall back to the version string so a verdict is
|
|
129
|
+
# still keyed to something, at the cost of one subprocess.
|
|
130
|
+
try:
|
|
131
|
+
ident = subprocess.run(
|
|
132
|
+
[_ffmpeg(), "-hide_banner", "-version"],
|
|
133
|
+
capture_output=True, text=True, timeout=10,
|
|
134
|
+
).stdout.splitlines()[0]
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
_identity_memo = hashlib.sha256(ident.encode("utf-8", "replace")).hexdigest()[:16]
|
|
138
|
+
return _identity_memo
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _cache_path() -> str:
|
|
142
|
+
return os.path.join(_SPROP_DIR, "decoders.json")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _load_cache() -> dict:
|
|
146
|
+
try:
|
|
147
|
+
with open(_cache_path()) as fh:
|
|
148
|
+
return json.load(fh)
|
|
149
|
+
except (OSError, ValueError):
|
|
150
|
+
return {}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _save_cache(data: dict) -> None:
|
|
154
|
+
try:
|
|
155
|
+
os.makedirs(_SPROP_DIR, exist_ok=True)
|
|
156
|
+
tmp = _cache_path() + ".tmp"
|
|
157
|
+
with open(tmp, "w") as fh:
|
|
158
|
+
json.dump(data, fh)
|
|
159
|
+
os.replace(tmp, _cache_path())
|
|
160
|
+
except OSError as exc:
|
|
161
|
+
_LOGGER.debug("decoder cache write failed: %s", exc)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_decoders_memo: Optional[set] = None
|
|
165
|
+
_hwaccels_memo: Optional[set] = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _list_names(flag: str) -> set:
|
|
169
|
+
try:
|
|
170
|
+
out = subprocess.run(
|
|
171
|
+
[_ffmpeg(), "-hide_banner", flag],
|
|
172
|
+
capture_output=True, text=True, timeout=10,
|
|
173
|
+
).stdout
|
|
174
|
+
except Exception:
|
|
175
|
+
return set()
|
|
176
|
+
names = set()
|
|
177
|
+
for ln in out.splitlines():
|
|
178
|
+
parts = ln.split()
|
|
179
|
+
if not parts or ln.startswith(" -") or ln.rstrip().endswith(":"):
|
|
180
|
+
continue
|
|
181
|
+
# -decoders lines are " V....D name description"; -hwaccels is one
|
|
182
|
+
# bare name per line under a heading.
|
|
183
|
+
names.add(parts[1] if flag == "-decoders" and len(parts) > 1 else parts[0])
|
|
184
|
+
return names
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _plausible(cand: List[str]) -> bool:
|
|
188
|
+
"""Could this candidate possibly work, judged without spawning anything?
|
|
189
|
+
|
|
190
|
+
A build advertises support compiled in, not hardware present. Trying a
|
|
191
|
+
candidate costs an ffmpeg process and a couple of seconds on a small board,
|
|
192
|
+
and on a machine with no video hardware at all EVERY trial is wasted - as is
|
|
193
|
+
encoding the sample they would have decoded. Checking for the device first
|
|
194
|
+
costs a stat.
|
|
195
|
+
|
|
196
|
+
Deliberately permissive: it rules out only what is impossible, never what is
|
|
197
|
+
merely unlikely. Anything that survives still has to prove itself.
|
|
198
|
+
"""
|
|
199
|
+
if not cand:
|
|
200
|
+
return True # software
|
|
201
|
+
name = cand[1]
|
|
202
|
+
if name == "videotoolbox":
|
|
203
|
+
return sys.platform == "darwin"
|
|
204
|
+
if name == "vaapi" or name.endswith("_vaapi") or name.endswith("_qsv"):
|
|
205
|
+
return bool(glob.glob("/dev/dri/render*"))
|
|
206
|
+
if name.endswith("_cuvid") or name.endswith("_nvdec"):
|
|
207
|
+
return bool(glob.glob("/dev/nvidia*"))
|
|
208
|
+
if name.endswith("_v4l2m2m"):
|
|
209
|
+
return bool(glob.glob("/dev/video*"))
|
|
210
|
+
if name.endswith("_rkmpp"):
|
|
211
|
+
return bool(glob.glob("/dev/mpp_service") or glob.glob("/dev/rga"))
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _available(cand: List[str]) -> bool:
|
|
216
|
+
"""Cheap pre-filter. Never sufficient on its own - a Pi advertises
|
|
217
|
+
h264_cuvid with no Nvidia hardware present, which is why everything that
|
|
218
|
+
passes here still has to prove it can decode."""
|
|
219
|
+
global _decoders_memo, _hwaccels_memo
|
|
220
|
+
if not cand:
|
|
221
|
+
return True # software
|
|
222
|
+
if cand[0] == "-c:v":
|
|
223
|
+
if _decoders_memo is None:
|
|
224
|
+
_decoders_memo = _list_names("-decoders")
|
|
225
|
+
return cand[1] in _decoders_memo
|
|
226
|
+
if _hwaccels_memo is None:
|
|
227
|
+
_hwaccels_memo = _list_names("-hwaccels")
|
|
228
|
+
return cand[1] in _hwaccels_memo
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _compiled_in(name: str) -> bool:
|
|
232
|
+
"""Back-compat shim: is this named decoder in the build?"""
|
|
233
|
+
return _available(["-c:v", name])
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# The cameras send 1280x720, and the probe clip matches that deliberately.
|
|
237
|
+
# A smaller clip is cheaper but ranks candidates badly: hardware decoding pays a
|
|
238
|
+
# fixed session-setup cost that dominates a tiny job, so a 320x240 clip can make
|
|
239
|
+
# a decoder that wins comfortably on real frames look like a loser. Measured on
|
|
240
|
+
# a Pi 4: 2.75s per candidate at 720p against 1.73s at 320x240 - a fair price
|
|
241
|
+
# for a measurement that reflects the actual workload, and it is paid once per
|
|
242
|
+
# host in the background.
|
|
243
|
+
_SAMPLE = "testsrc=size=1280x720:rate=15:duration=1"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _make_sample(codec: str, path: str) -> bool:
|
|
247
|
+
"""Generate a clip to decode. Encoding is software and that is fine -
|
|
248
|
+
it happens once per probe, not per stream."""
|
|
249
|
+
enc = "libx264" if codec == "h264" else "libx265"
|
|
250
|
+
try:
|
|
251
|
+
r = subprocess.run(
|
|
252
|
+
[_ffmpeg(), "-hide_banner", "-loglevel", "error",
|
|
253
|
+
"-f", "lavfi", "-i", _SAMPLE,
|
|
254
|
+
"-c:v", enc, "-preset", "ultrafast", "-g", "15",
|
|
255
|
+
"-f", codec, "-y", path],
|
|
256
|
+
capture_output=True, text=True, timeout=_PROBE_TIMEOUT_S,
|
|
257
|
+
)
|
|
258
|
+
return r.returncode == 0 and os.path.getsize(path) > 0
|
|
259
|
+
except Exception:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _try_decoder(cand: List[str], sample: str) -> Optional[float]:
|
|
264
|
+
"""Decode the sample with ``cand``; seconds taken, or None if it cannot.
|
|
265
|
+
|
|
266
|
+
Decodes to rawvideo and requires bytes to come out, not merely a zero exit.
|
|
267
|
+
A decoder that opens, consumes the input and emits nothing would otherwise
|
|
268
|
+
look like a pass - and that is precisely the failure that matters here,
|
|
269
|
+
since it presents downstream as a permanently black picture rather than as
|
|
270
|
+
an error. A non-zero exit or anything on stderr also disqualifies it: a
|
|
271
|
+
decoder that warns its way through is not one to rely on.
|
|
272
|
+
"""
|
|
273
|
+
t0 = time.monotonic()
|
|
274
|
+
try:
|
|
275
|
+
r = subprocess.run(
|
|
276
|
+
[_ffmpeg(), "-hide_banner", "-loglevel", "error",
|
|
277
|
+
*cand, "-i", sample,
|
|
278
|
+
"-f", "rawvideo", "-pix_fmt", "yuv420p", "-"],
|
|
279
|
+
capture_output=True, timeout=_PROBE_TIMEOUT_S,
|
|
280
|
+
)
|
|
281
|
+
except Exception:
|
|
282
|
+
return None
|
|
283
|
+
if r.returncode != 0 or r.stderr.strip() or not r.stdout:
|
|
284
|
+
return None
|
|
285
|
+
return time.monotonic() - t0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def probe_decoder(codec: str, force: bool = False) -> List[str]:
|
|
289
|
+
"""ffmpeg input arguments for the fastest way to decode ``codec`` here.
|
|
290
|
+
|
|
291
|
+
Returns ``["-hwaccel", m]`` or ``["-c:v", name]`` for a hardware path that
|
|
292
|
+
was proven to work on this machine, or ``[]`` for plain software decoding -
|
|
293
|
+
which is also the answer when acceleration is disabled or nothing else
|
|
294
|
+
qualifies. The result always belongs BEFORE ``-i``.
|
|
295
|
+
"""
|
|
296
|
+
codec = codec.lower()
|
|
297
|
+
if codec not in _CANDIDATES:
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
forced = os.environ.get(_ENV_OVERRIDE, "").strip()
|
|
301
|
+
if forced:
|
|
302
|
+
# Accept either form: a bare name is a decoder, "hwaccel:x" a method.
|
|
303
|
+
if forced.startswith("hwaccel:"):
|
|
304
|
+
return ["-hwaccel", forced.split(":", 1)[1]]
|
|
305
|
+
return ["-c:v", forced]
|
|
306
|
+
if os.environ.get(_ENV_DISABLE, "").strip().lower() in ("1", "true", "yes", "on"):
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
key = f"v{_CACHE_SCHEMA}:{codec}:{_ffmpeg_identity()}"
|
|
310
|
+
if not force and key in _cache_mem:
|
|
311
|
+
return list(_cache_mem[key])
|
|
312
|
+
|
|
313
|
+
cache = _load_cache()
|
|
314
|
+
if not force and isinstance(cache.get(key), list):
|
|
315
|
+
_cache_mem[key] = list(cache[key])
|
|
316
|
+
return list(cache[key])
|
|
317
|
+
|
|
318
|
+
# Everything below spawns processes, so decide first whether it is worth
|
|
319
|
+
# spawning any. A machine with no video hardware - a VPS, a container, most
|
|
320
|
+
# cloud installs - can be answered here for free, and encoding a sample it
|
|
321
|
+
# would never decode is the single largest cost in this module.
|
|
322
|
+
hw = [c for c in _CANDIDATES[codec] if c and _plausible(c) and _available(c)]
|
|
323
|
+
if not hw:
|
|
324
|
+
_LOGGER.debug("decoder probe: no hardware candidate is possible for %s "
|
|
325
|
+
"on this machine; software, nothing probed", codec)
|
|
326
|
+
cache[key] = []
|
|
327
|
+
_cache_mem[key] = []
|
|
328
|
+
_save_cache(cache)
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
import tempfile
|
|
332
|
+
winner: List[str] = []
|
|
333
|
+
best = None
|
|
334
|
+
with tempfile.TemporaryDirectory() as td:
|
|
335
|
+
sample = os.path.join(td, f"probe.{codec}")
|
|
336
|
+
if not _make_sample(codec, sample):
|
|
337
|
+
_LOGGER.debug("decoder probe: could not build a %s sample", codec)
|
|
338
|
+
return []
|
|
339
|
+
for cand in hw:
|
|
340
|
+
took = _try_decoder(cand, sample)
|
|
341
|
+
if took is None:
|
|
342
|
+
_LOGGER.debug("decoder probe: %s cannot decode %s here",
|
|
343
|
+
" ".join(cand), codec)
|
|
344
|
+
continue
|
|
345
|
+
if best is None or took < best:
|
|
346
|
+
winner, best = list(cand), took
|
|
347
|
+
# Software is only worth timing if something beat it to the line. When
|
|
348
|
+
# every hardware candidate failed - the common case on small boards -
|
|
349
|
+
# software wins by default and the measurement would change nothing.
|
|
350
|
+
if winner:
|
|
351
|
+
sw = _try_decoder([], sample)
|
|
352
|
+
if sw is not None and sw <= (best or 0.0):
|
|
353
|
+
_LOGGER.debug("decoder probe: software (%.2fs) beats %s (%.2fs) "
|
|
354
|
+
"for %s", sw, " ".join(winner), best or 0.0, codec)
|
|
355
|
+
winner, best = [], sw
|
|
356
|
+
|
|
357
|
+
if winner:
|
|
358
|
+
_LOGGER.info("decoder probe: using %s for %s (%.2fs on the probe clip)",
|
|
359
|
+
" ".join(winner), codec, best or 0.0)
|
|
360
|
+
else:
|
|
361
|
+
_LOGGER.debug("decoder probe: software decoding for %s", codec)
|
|
362
|
+
cache[key] = winner
|
|
363
|
+
_cache_mem[key] = list(winner)
|
|
364
|
+
_save_cache(cache)
|
|
365
|
+
return list(winner)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def warm_decoder_cache(codecs: tuple = ("h264",)) -> "threading.Thread":
|
|
369
|
+
"""Probe in the background so the first real use is already cached.
|
|
370
|
+
|
|
371
|
+
Probing costs about ten seconds per codec the first time on a small ARM
|
|
372
|
+
board, which is far too long to block a startup path - but it only ever
|
|
373
|
+
happens once per host per ffmpeg build, and the answer is wanted before the
|
|
374
|
+
first stream rather than during it. So run it on a daemon thread and let the
|
|
375
|
+
caller carry on: anything asking for a decoder meanwhile simply computes it
|
|
376
|
+
inline, and the result is shared through the same cache either way.
|
|
377
|
+
|
|
378
|
+
Only H.264 is warmed by default, because only H.264 is ingested: probing
|
|
379
|
+
H.265 as well doubled the cost of this module on a Pi 4 (22s of 47s wall,
|
|
380
|
+
15s of 26s CPU) to answer a question nothing asks. If H.265 ingest lands,
|
|
381
|
+
add it here - and until then a stray H.265 stream still works, it just lets
|
|
382
|
+
ffmpeg choose on the first session while the answer is worked out.
|
|
383
|
+
|
|
384
|
+
Idempotent: a second call while the first is still running is a no-op, so
|
|
385
|
+
every config entry can call it at setup without spawning a probe each.
|
|
386
|
+
|
|
387
|
+
Returns the thread so a caller that wants to can join it; ignoring it is the
|
|
388
|
+
expected use. Returns the in-flight thread if one is already running.
|
|
389
|
+
"""
|
|
390
|
+
global _warm_thread
|
|
391
|
+
with _warm_lock:
|
|
392
|
+
if _warm_thread is not None and _warm_thread.is_alive():
|
|
393
|
+
return _warm_thread
|
|
394
|
+
|
|
395
|
+
def _run() -> None:
|
|
396
|
+
# Prime the in-memory cache from disk HERE, on this thread, so
|
|
397
|
+
# cached_decoder() never has to read a file on the event loop.
|
|
398
|
+
try:
|
|
399
|
+
for k, v in _load_cache().items():
|
|
400
|
+
if isinstance(v, list):
|
|
401
|
+
_cache_mem.setdefault(k, list(v))
|
|
402
|
+
except Exception:
|
|
403
|
+
_LOGGER.debug("decoder cache preload failed", exc_info=True)
|
|
404
|
+
for codec in codecs:
|
|
405
|
+
try:
|
|
406
|
+
probe_decoder(codec)
|
|
407
|
+
except Exception:
|
|
408
|
+
_LOGGER.debug("decoder probe failed for %s", codec, exc_info=True)
|
|
409
|
+
|
|
410
|
+
with _warm_lock:
|
|
411
|
+
if _warm_thread is not None and _warm_thread.is_alive():
|
|
412
|
+
return _warm_thread
|
|
413
|
+
_warm_thread = threading.Thread(
|
|
414
|
+
target=_run, name="aidot-decoder-probe", daemon=True)
|
|
415
|
+
_warm_thread.start()
|
|
416
|
+
return _warm_thread
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def cached_decoder(codec: str) -> Optional[List[str]]:
|
|
420
|
+
"""The cached verdict for ``codec``, or None if it has not been probed yet.
|
|
421
|
+
|
|
422
|
+
Never probes, so it is safe to call from an event loop - probing shells out
|
|
423
|
+
for several seconds on a first run and would stall every other camera. A
|
|
424
|
+
caller that gets None should simply let ffmpeg choose, which is the
|
|
425
|
+
behaviour that existed before any of this; ``warm_decoder_cache()`` fills
|
|
426
|
+
the cache in the background so the answer is normally already there.
|
|
427
|
+
|
|
428
|
+
An empty list is a real answer meaning "software decoding", and is
|
|
429
|
+
deliberately distinct from None.
|
|
430
|
+
"""
|
|
431
|
+
codec = codec.lower()
|
|
432
|
+
if codec not in _CANDIDATES:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
forced = os.environ.get(_ENV_OVERRIDE, "").strip()
|
|
436
|
+
if forced:
|
|
437
|
+
if forced.startswith("hwaccel:"):
|
|
438
|
+
return ["-hwaccel", forced.split(":", 1)[1]]
|
|
439
|
+
return ["-c:v", forced]
|
|
440
|
+
if os.environ.get(_ENV_DISABLE, "").strip().lower() in ("1", "true", "yes", "on"):
|
|
441
|
+
return []
|
|
442
|
+
|
|
443
|
+
# MEMORY ONLY. This runs on the event loop, so it must not touch the disk:
|
|
444
|
+
# Home Assistant detects a blocking open() here and reports it as a
|
|
445
|
+
# stability problem, correctly. The disk cache is read by
|
|
446
|
+
# warm_decoder_cache() on its own thread, which populates _cache_mem; a miss
|
|
447
|
+
# here simply means "not known yet", and the caller lets ffmpeg choose,
|
|
448
|
+
# which is the behaviour that existed before any of this.
|
|
449
|
+
key = f"v{_CACHE_SCHEMA}:{codec}:{_ffmpeg_identity()}"
|
|
450
|
+
if key in _cache_mem:
|
|
451
|
+
return list(_cache_mem[key])
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def decoder_args(codec: str) -> List[str]:
|
|
456
|
+
"""Input arguments for decoding ``codec``, probing if necessary."""
|
|
457
|
+
return probe_decoder(codec)
|
{python_aidot_cameras-0.15.0b2 → python_aidot_cameras-0.15.1}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -4389,6 +4389,16 @@ class _SdesOpenMixin:
|
|
|
4389
4389
|
# gates audio via sdes_audio in _build_sdes_serve_cmd.)
|
|
4390
4390
|
_is_push = bool(rtsp_push_url) and not rtsp_push_url.startswith("http")
|
|
4391
4391
|
_push_video_only = _is_push and _keep_a is None
|
|
4392
|
+
# Only the -f null drain decodes; the rest are -c copy, so this is the
|
|
4393
|
+
# one place a decoder choice can pay. Read the cache only - probing
|
|
4394
|
+
# shells out for several seconds on a cold host and would stall the
|
|
4395
|
+
# event loop for every camera; None just means "let ffmpeg choose", as
|
|
4396
|
+
# before, and an empty list is a real "software decoding" answer.
|
|
4397
|
+
from .hwaccel import cached_decoder # lazy: keep import cost off setup
|
|
4398
|
+
_video_decoder = (
|
|
4399
|
+
cached_decoder("h264" if _keep_v == 96 else "hevc")
|
|
4400
|
+
if _keep_v is not None else None
|
|
4401
|
+
)
|
|
4392
4402
|
cmd = _build_sdes_serve_cmd(
|
|
4393
4403
|
sdp_path=sdp_path,
|
|
4394
4404
|
rtsp_push_url=rtsp_push_url,
|
|
@@ -4397,6 +4407,7 @@ class _SdesOpenMixin:
|
|
|
4397
4407
|
sdes_audio=_serve_audio,
|
|
4398
4408
|
audio_gain_db=self._resolve_sdes_audio_gain_db(),
|
|
4399
4409
|
push_video_only=_push_video_only,
|
|
4410
|
+
video_decoder=_video_decoder,
|
|
4400
4411
|
)
|
|
4401
4412
|
# Audio matters even more than video here. A multi-PT m-line makes ffmpeg
|
|
4402
4413
|
# bind the depacketizer to the FIRST payload type and silently discard the
|
|
@@ -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.1"
|
|
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.0b2 → python_aidot_cameras-0.15.1/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.1
|
|
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
|
|
@@ -167,6 +167,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
167
167
|
|
|
168
168
|
| Variable | Purpose | Default |
|
|
169
169
|
| --- | --- | --- |
|
|
170
|
+
| `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
|
|
171
|
+
| `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
|
|
170
172
|
| `AIDOT_MAX_CONCURRENT_OPENS` | Caps how many stream opens run concurrently across all cameras. | `2` |
|
|
171
173
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
172
174
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
@@ -53,6 +53,7 @@ aidot_cameras/camera/client.py
|
|
|
53
53
|
aidot_cameras/camera/constants.py
|
|
54
54
|
aidot_cameras/camera/controls.py
|
|
55
55
|
aidot_cameras/camera/go2rtc.py
|
|
56
|
+
aidot_cameras/camera/hwaccel.py
|
|
56
57
|
aidot_cameras/camera/lan_control.py
|
|
57
58
|
aidot_cameras/camera/models.py
|
|
58
59
|
aidot_cameras/camera/playback.py
|
|
@@ -95,6 +96,7 @@ tests/test_go2rtc.py
|
|
|
95
96
|
tests/test_go2rtc_cli.py
|
|
96
97
|
tests/test_h264_decode_rate_limit.py
|
|
97
98
|
tests/test_highport_nomination.py
|
|
99
|
+
tests/test_hwaccel_probe.py
|
|
98
100
|
tests/test_ice_config_cache.py
|
|
99
101
|
tests/test_ice_peer_reflexive.py
|
|
100
102
|
tests/test_idle_release_both_loops.py
|