python-aidot-cameras 0.15.7__tar.gz → 0.16.0__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.7/python_aidot_cameras.egg-info → python_aidot_cameras-0.16.0}/PKG-INFO +1 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/client.py +30 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/protocol.py +232 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/sdes.py +8 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/sdes_open.py +60 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/webrtc.py +10 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/webrtc_open.py +13 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/pyproject.toml +1 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/SOURCES.txt +5 -0
- python_aidot_cameras-0.16.0/tests/test_avio_request.py +221 -0
- python_aidot_cameras-0.16.0/tests/test_avio_response_parse.py +91 -0
- python_aidot_cameras-0.16.0/tests/test_avio_response_router.py +207 -0
- python_aidot_cameras-0.16.0/tests/test_sdes_avio_dispatch.py +117 -0
- python_aidot_cameras-0.16.0/tests/test_serve_loss_tolerance.py +70 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/LICENSE +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/README.md +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/setup.cfg +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.15.7/python_aidot_cameras.egg-info → python_aidot_cameras-0.16.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
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
|
|
@@ -365,6 +365,18 @@ def _is_self_referential_source(source: str, stream_name: str) -> bool:
|
|
|
365
365
|
return path == stream_name
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
#: Packets the serve's demuxer may hold while it waits for a gap to fill. A
|
|
369
|
+
#: keyframe on an A001064 is 146-190 KB - roughly 130 packets - so anything
|
|
370
|
+
#: smaller than a few hundred cannot ride out a burst that arrives out of order.
|
|
371
|
+
#: Env-overridable because the right value depends on the link, not on us.
|
|
372
|
+
_SERVE_REORDER_QUEUE = int(os.environ.get("AIDOT_SERVE_REORDER_QUEUE", "500"))
|
|
373
|
+
|
|
374
|
+
#: How long (microseconds) the demuxer waits for a missing packet before moving
|
|
375
|
+
#: on. Bounded on purpose: waiting forever turns loss into a stall, which is the
|
|
376
|
+
#: failure this is meant to prevent.
|
|
377
|
+
_SERVE_MAX_DELAY_US = int(os.environ.get("AIDOT_SERVE_MAX_DELAY_US", "500000"))
|
|
378
|
+
|
|
379
|
+
|
|
368
380
|
def _build_sdes_serve_cmd(
|
|
369
381
|
*,
|
|
370
382
|
sdp_path: str,
|
|
@@ -470,9 +482,26 @@ def _build_sdes_serve_cmd(
|
|
|
470
482
|
# 2 s analyzeduration: the camera sends SPS+PPS+IDR in the first burst;
|
|
471
483
|
# 15 s consumed nearly all packets during analysis. PLI re-arms an IDR
|
|
472
484
|
# every 5 s so parameters always re-arrive.
|
|
473
|
-
|
|
485
|
+
#
|
|
486
|
+
# ``discardcorrupt`` is the loss half of this: both ends of this path are
|
|
487
|
+
# wireless (the camera, and typically the host), so RTP arrives with real
|
|
488
|
+
# gaps. Measured on the reference install: `RTP: missed N packets` with
|
|
489
|
+
# ZERO drops on every socket in the path, which means the packets were
|
|
490
|
+
# lost before they ever reached the box. Without this ffmpeg passes the
|
|
491
|
+
# damaged frame downstream and then backs its input queue up behind the
|
|
492
|
+
# reordering it thinks it needs - "max delay reached. need to consume
|
|
493
|
+
# packet" - and the serve eventually dies, taking every viewer with it.
|
|
494
|
+
# A brief artefact is a far better outcome than a dropped stream.
|
|
495
|
+
"-fflags", "+nobuffer+genpts+discardcorrupt",
|
|
474
496
|
"-analyzeduration", "2000000",
|
|
475
497
|
"-probesize", "500000",
|
|
498
|
+
# Reordering headroom. The default (-1) leaves the demuxer with almost
|
|
499
|
+
# none, so a burst that arrives out of order reads as loss. A keyframe
|
|
500
|
+
# here is 146-190 KB - about 130 packets - so the queue has to be able to
|
|
501
|
+
# hold more than one of them, and max_delay bounds how long it will wait
|
|
502
|
+
# before giving up on a gap rather than stalling the pipeline.
|
|
503
|
+
"-reorder_queue_size", str(_SERVE_REORDER_QUEUE),
|
|
504
|
+
"-max_delay", str(_SERVE_MAX_DELAY_US),
|
|
476
505
|
"-i", sdp_path,
|
|
477
506
|
*dest_args,
|
|
478
507
|
]
|
{python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -8,6 +8,7 @@ imports client.py -- the import edge is one-way (client -> protocol).
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import concurrent.futures as _cfutures
|
|
11
12
|
import hashlib
|
|
12
13
|
import ipaddress
|
|
13
14
|
import json
|
|
@@ -22,7 +23,7 @@ import tempfile
|
|
|
22
23
|
import threading
|
|
23
24
|
import time
|
|
24
25
|
import zlib
|
|
25
|
-
from typing import Any, Callable, Optional
|
|
26
|
+
from typing import Any, Callable, Optional, NamedTuple
|
|
26
27
|
|
|
27
28
|
from .constants import TALK_PCM_FRAME_BYTES, TALK_PCM_RATE
|
|
28
29
|
|
|
@@ -2176,3 +2177,233 @@ def _dedup_bundle_candidates(sdp: str) -> str:
|
|
|
2176
2177
|
else:
|
|
2177
2178
|
result.extend(ln for ln in sec if not ln.startswith('a=candidate:'))
|
|
2178
2179
|
return '\r\n'.join(result)
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
#: Header of an AVIO control frame: seq, command, timestamp(ms), payload length,
|
|
2183
|
+
#: reserved, then 4 bytes of padding. Same layout we already build when sending.
|
|
2184
|
+
_AVIO_RESP_HDR = struct.Struct("<IIqII4x")
|
|
2185
|
+
|
|
2186
|
+
#: Bytes of header before an AVIO payload begins. Exported because the SDES
|
|
2187
|
+
#: receive path has to walk past one frame to reach the next in the same chunk.
|
|
2188
|
+
AVIO_HDR_LEN = _AVIO_RESP_HDR.size
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
class AvioResponse(NamedTuple):
|
|
2192
|
+
"""A decoded AVIO control response."""
|
|
2193
|
+
|
|
2194
|
+
seq: int
|
|
2195
|
+
command: int
|
|
2196
|
+
payload: bytes
|
|
2197
|
+
|
|
2198
|
+
|
|
2199
|
+
def parse_avio_response(frame: bytes) -> "Optional[AvioResponse]":
|
|
2200
|
+
"""Decode an AVIO control response, or None if this is not a valid one.
|
|
2201
|
+
|
|
2202
|
+
Returns None rather than raising: this runs on the bridge's receive path,
|
|
2203
|
+
where anything can arrive, and a malformed frame must be ignored rather than
|
|
2204
|
+
kill the loop. A declared length that overruns the buffer is rejected too -
|
|
2205
|
+
trusting it would hand the caller a truncated payload and, for example, read
|
|
2206
|
+
a stream profile out of bytes the camera never sent.
|
|
2207
|
+
"""
|
|
2208
|
+
if not frame or len(frame) < _AVIO_RESP_HDR.size:
|
|
2209
|
+
return None
|
|
2210
|
+
seq, command, _ts, length, _reserved = _AVIO_RESP_HDR.unpack_from(frame, 0)
|
|
2211
|
+
body = frame[_AVIO_RESP_HDR.size:]
|
|
2212
|
+
if len(body) < length:
|
|
2213
|
+
return None
|
|
2214
|
+
return AvioResponse(seq=seq, command=command, payload=body[:length])
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
class _AvioWaiter:
|
|
2218
|
+
"""A single outstanding question, handed back by ``AvioResponseRouter.expect``.
|
|
2219
|
+
|
|
2220
|
+
Built on a ``concurrent.futures.Future`` rather than an ``asyncio`` one on
|
|
2221
|
+
purpose: the reply is dispatched from the SDES bridge thread while the
|
|
2222
|
+
caller awaits on the event loop, and setting an asyncio future's result from
|
|
2223
|
+
a foreign thread does not wake the awaiting coroutine - the camera would
|
|
2224
|
+
look silent.
|
|
2225
|
+
"""
|
|
2226
|
+
|
|
2227
|
+
def __init__(self, router: "AvioResponseRouter", command: int) -> None:
|
|
2228
|
+
self._router = router
|
|
2229
|
+
self._command = command
|
|
2230
|
+
self._future = _cfutures.Future()
|
|
2231
|
+
|
|
2232
|
+
def cancel(self) -> None:
|
|
2233
|
+
"""Withdraw the question - used when the command could not be sent."""
|
|
2234
|
+
self._router._forget(self)
|
|
2235
|
+
|
|
2236
|
+
async def result(self, timeout: float) -> "Optional[AvioResponse]":
|
|
2237
|
+
"""The camera's reply, or None if it did not answer within ``timeout``.
|
|
2238
|
+
|
|
2239
|
+
None is a real answer, not an error: this firmware may not implement a
|
|
2240
|
+
response for every command we send, and a control call must never block
|
|
2241
|
+
a view while we find out.
|
|
2242
|
+
"""
|
|
2243
|
+
if self._future.done():
|
|
2244
|
+
# A wait that timed out cancels its own wrapper, and that
|
|
2245
|
+
# cancellation can propagate down to this future - asking again
|
|
2246
|
+
# would then raise where the first call returned None.
|
|
2247
|
+
if self._future.cancelled():
|
|
2248
|
+
return None
|
|
2249
|
+
try:
|
|
2250
|
+
return self._future.result()
|
|
2251
|
+
except Exception:
|
|
2252
|
+
return None
|
|
2253
|
+
try:
|
|
2254
|
+
return await asyncio.wait_for(
|
|
2255
|
+
asyncio.wrap_future(self._future), timeout
|
|
2256
|
+
)
|
|
2257
|
+
except TimeoutError:
|
|
2258
|
+
# Stop listening. _avio_cmd runs on the keepalive path, so a
|
|
2259
|
+
# registration left behind on every unanswered command would grow
|
|
2260
|
+
# for as long as the session lives.
|
|
2261
|
+
self._router._forget(self)
|
|
2262
|
+
return None
|
|
2263
|
+
except asyncio.CancelledError:
|
|
2264
|
+
# Cancellation is the caller's task being stopped (entity teardown,
|
|
2265
|
+
# config-entry unload), NOT the camera declining to answer. Drop
|
|
2266
|
+
# the registration, then let it through: swallowing it would leave
|
|
2267
|
+
# the caller running inside a task the loop has already given up on.
|
|
2268
|
+
self._router._forget(self)
|
|
2269
|
+
raise
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
class AvioResponseRouter:
|
|
2273
|
+
"""Match inbound AVIO responses to the commands that asked for them.
|
|
2274
|
+
|
|
2275
|
+
The control channel also carries traffic nobody asked for (session-mode
|
|
2276
|
+
notifications, track switches), so "the next frame in" is not the answer to
|
|
2277
|
+
the last question out. Register interest with ``expect()`` *before* sending,
|
|
2278
|
+
then await the waiter; ``dispatch()`` is safe to call from the bridge thread
|
|
2279
|
+
and never raises or blocks it.
|
|
2280
|
+
"""
|
|
2281
|
+
|
|
2282
|
+
def __init__(self) -> None:
|
|
2283
|
+
self._lock = threading.Lock()
|
|
2284
|
+
self._waiting: "dict[int, list[_AvioWaiter]]" = {}
|
|
2285
|
+
|
|
2286
|
+
def expect(self, command: int) -> _AvioWaiter:
|
|
2287
|
+
"""Register interest in the next response carrying ``command``.
|
|
2288
|
+
|
|
2289
|
+
Call this before sending the request: the camera can answer before the
|
|
2290
|
+
sender returns, and a registration made afterwards would miss the reply.
|
|
2291
|
+
"""
|
|
2292
|
+
waiter = _AvioWaiter(self, command)
|
|
2293
|
+
with self._lock:
|
|
2294
|
+
self._waiting.setdefault(command, []).append(waiter)
|
|
2295
|
+
return waiter
|
|
2296
|
+
|
|
2297
|
+
def dispatch(self, frame: bytes) -> bool:
|
|
2298
|
+
"""Hand ``frame`` to whoever is waiting for it.
|
|
2299
|
+
|
|
2300
|
+
True if it answered an outstanding question. False - not an error - for
|
|
2301
|
+
junk, for a command nobody asked about, and for a retransmitted copy of
|
|
2302
|
+
a reply already delivered.
|
|
2303
|
+
"""
|
|
2304
|
+
try:
|
|
2305
|
+
resp = parse_avio_response(frame)
|
|
2306
|
+
if resp is None:
|
|
2307
|
+
return False
|
|
2308
|
+
with self._lock:
|
|
2309
|
+
queue = self._waiting.get(resp.command)
|
|
2310
|
+
waiter = None
|
|
2311
|
+
while queue:
|
|
2312
|
+
candidate = queue.pop(0)
|
|
2313
|
+
if not candidate._future.done():
|
|
2314
|
+
waiter = candidate
|
|
2315
|
+
break
|
|
2316
|
+
if queue is not None and not queue:
|
|
2317
|
+
self._waiting.pop(resp.command, None)
|
|
2318
|
+
if waiter is None:
|
|
2319
|
+
return False
|
|
2320
|
+
waiter._future.set_result(resp)
|
|
2321
|
+
return True
|
|
2322
|
+
except Exception:
|
|
2323
|
+
# Runs inline on the bridge's receive path; a bad frame must never
|
|
2324
|
+
# take the media loop down with it.
|
|
2325
|
+
return False
|
|
2326
|
+
|
|
2327
|
+
def _forget(self, waiter: "_AvioWaiter") -> None:
|
|
2328
|
+
with self._lock:
|
|
2329
|
+
queue = self._waiting.get(waiter._command)
|
|
2330
|
+
if not queue:
|
|
2331
|
+
return
|
|
2332
|
+
try:
|
|
2333
|
+
queue.remove(waiter)
|
|
2334
|
+
except ValueError:
|
|
2335
|
+
return
|
|
2336
|
+
if not queue:
|
|
2337
|
+
self._waiting.pop(waiter._command, None)
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
def _avio_cmd_id(frame: bytes) -> "Optional[int]":
|
|
2341
|
+
"""The command id of an AVIO frame, or None if it does not look like one.
|
|
2342
|
+
|
|
2343
|
+
For the receive-path logs. Which ids this firmware actually sends is not
|
|
2344
|
+
documented anywhere we trust - the list we work from was assembled from
|
|
2345
|
+
frames seen on the wire - so naming them as they arrive is what turns the
|
|
2346
|
+
next question into a log grep instead of another live session.
|
|
2347
|
+
"""
|
|
2348
|
+
resp = parse_avio_response(bytes(frame))
|
|
2349
|
+
return None if resp is None else resp.command
|
|
2350
|
+
|
|
2351
|
+
|
|
2352
|
+
#: How long a control call waits for the camera before reporting "no reply".
|
|
2353
|
+
#: Short on purpose: these run behind Home Assistant service calls, and a
|
|
2354
|
+
#: firmware that implements the request but not the response must not be able to
|
|
2355
|
+
#: hold up a view while we find that out.
|
|
2356
|
+
AVIO_RESPONSE_TIMEOUT_S = 2.0
|
|
2357
|
+
|
|
2358
|
+
|
|
2359
|
+
class AvioRequestMixin:
|
|
2360
|
+
"""Ask an AVIO command and wait for the camera's reply.
|
|
2361
|
+
|
|
2362
|
+
Mixed into both session types. ``_avio_cmd`` itself is left exactly as it
|
|
2363
|
+
was - fire-and-forget, synchronous, returns a bool - because every camera on
|
|
2364
|
+
both transports goes through it, for the speaker, the keepalive heartbeat
|
|
2365
|
+
and resolution alike. Wanting an answer is opt-in and additive.
|
|
2366
|
+
|
|
2367
|
+
A session that mixes this in must create its router in ``__init__``:
|
|
2368
|
+
dispatch arrives from the bridge thread and creating one lazily could race
|
|
2369
|
+
with the event loop and lose the reply to a second router.
|
|
2370
|
+
"""
|
|
2371
|
+
|
|
2372
|
+
_avio_responses: "AvioResponseRouter"
|
|
2373
|
+
|
|
2374
|
+
def dispatch_avio_frame(self, frame: bytes) -> bool:
|
|
2375
|
+
"""Offer an inbound control frame to whoever is waiting for it.
|
|
2376
|
+
|
|
2377
|
+
Called from the transports' receive paths (the DTLS DataChannel message
|
|
2378
|
+
handler, the SDES bridge thread). False - not an error - when nothing
|
|
2379
|
+
was waiting for it, which is the common case: the camera sends session
|
|
2380
|
+
notifications and track switches nobody asked for.
|
|
2381
|
+
"""
|
|
2382
|
+
return self._avio_responses.dispatch(frame)
|
|
2383
|
+
|
|
2384
|
+
async def async_avio_request(
|
|
2385
|
+
self,
|
|
2386
|
+
cmd: int,
|
|
2387
|
+
payload: bytes = b"",
|
|
2388
|
+
*,
|
|
2389
|
+
response_cmd: int,
|
|
2390
|
+
timeout: float = AVIO_RESPONSE_TIMEOUT_S,
|
|
2391
|
+
) -> "Optional[AvioResponse]":
|
|
2392
|
+
"""Send ``cmd`` and return the camera's ``response_cmd`` reply, or None.
|
|
2393
|
+
|
|
2394
|
+
None means the camera did not answer in time. That is a real result,
|
|
2395
|
+
not a failure: this firmware may implement a request without a response,
|
|
2396
|
+
and no reply at all points at the channel rather than the firmware.
|
|
2397
|
+
"""
|
|
2398
|
+
waiter = self._avio_responses.expect(response_cmd)
|
|
2399
|
+
# Registered BEFORE the send: a camera on the LAN can answer before the
|
|
2400
|
+
# sending call returns, and a reply that arrives with nobody listening
|
|
2401
|
+
# would be reported as silence.
|
|
2402
|
+
try:
|
|
2403
|
+
sent = self._avio_cmd(cmd, payload)
|
|
2404
|
+
except Exception:
|
|
2405
|
+
sent = False
|
|
2406
|
+
if not sent:
|
|
2407
|
+
waiter.cancel()
|
|
2408
|
+
return None
|
|
2409
|
+
return await waiter.result(timeout)
|
|
@@ -10,11 +10,12 @@ import logging
|
|
|
10
10
|
from typing import Callable, Optional
|
|
11
11
|
|
|
12
12
|
from .constants import SDES_TALK_PUMP_IDLE_TICK
|
|
13
|
+
from .protocol import AvioRequestMixin, AvioResponseRouter
|
|
13
14
|
|
|
14
15
|
_LOGGER = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class SdesSession:
|
|
18
|
+
class SdesSession(AvioRequestMixin):
|
|
18
19
|
"""Active SDES-SRTP stream session managed by an ffmpeg subprocess.
|
|
19
20
|
|
|
20
21
|
Obtain via ``await DeviceClient.async_open_webrtc_stream(...)`` when the
|
|
@@ -39,6 +40,7 @@ class SdesSession:
|
|
|
39
40
|
first_video_pt=None,
|
|
40
41
|
first_audio_pt=None,
|
|
41
42
|
device_id=None,
|
|
43
|
+
responses=None,
|
|
42
44
|
) -> None:
|
|
43
45
|
# Which camera this session belongs to, for logging only. Optional so
|
|
44
46
|
# an existing caller that does not pass it still works; the logs then
|
|
@@ -80,6 +82,11 @@ class SdesSession:
|
|
|
80
82
|
# Mutable one-element list shared with the bridge thread. Bridge sets
|
|
81
83
|
# [0] to a callable(cmd, payload) once the SCTP channel is up.
|
|
82
84
|
self._cmd_chan = cmd_chan if cmd_chan is not None else [None]
|
|
85
|
+
# Matches inbound AVIO replies to the commands that asked for them.
|
|
86
|
+
# Created here, not lazily: the bridge thread dispatches into it.
|
|
87
|
+
self._avio_responses = (
|
|
88
|
+
responses if responses is not None else AvioResponseRouter()
|
|
89
|
+
)
|
|
83
90
|
# Shared talk state (dict) for outbound two-way audio, or None when the
|
|
84
91
|
# session was not opened talk-capable (offer stayed recvonly). Populated
|
|
85
92
|
# by the bridge (camera audio addr) and by async_start_talk (provider).
|
{python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -24,7 +24,10 @@ from .constants import (
|
|
|
24
24
|
from .models import VideoFrame # noqa: F401 - forward-ref annotation
|
|
25
25
|
from .sdes import SdesSession
|
|
26
26
|
from .protocol import (
|
|
27
|
+
AVIO_HDR_LEN,
|
|
28
|
+
AvioResponseRouter,
|
|
27
29
|
_build_sprop,
|
|
30
|
+
parse_avio_response,
|
|
28
31
|
_build_stun_binding_success_response,
|
|
29
32
|
_extract_param_sets_from_rtp,
|
|
30
33
|
_grab_free_port,
|
|
@@ -45,6 +48,40 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
45
48
|
_MEDIA_RCVBUF_BYTES = 4 * 1024 * 1024
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
def _dispatch_sctp_avio(responses, payload) -> bool:
|
|
52
|
+
"""Offer an inbound SCTP DATA payload to the AVIO response router.
|
|
53
|
+
|
|
54
|
+
This is where the camera's replies actually arrive on SDES: an encrypted
|
|
55
|
+
SCTP DATA chunk (PPID 53) on the same channel we send LIVING, the heartbeat
|
|
56
|
+
and SPEAKERSTART on - not the TUTK-audio framing the audio forward path
|
|
57
|
+
watches. Wiring only the latter is why a camera that answered SPEAKERSTART
|
|
58
|
+
in moments looked silent.
|
|
59
|
+
|
|
60
|
+
True if it answered something we were waiting for. Never raises and never
|
|
61
|
+
blocks: it runs inline on the bridge receive loop, where a bad frame must
|
|
62
|
+
not be able to take the media path down with it.
|
|
63
|
+
"""
|
|
64
|
+
if responses is None or not payload:
|
|
65
|
+
return False
|
|
66
|
+
answered = False
|
|
67
|
+
try:
|
|
68
|
+
view = memoryview(payload)
|
|
69
|
+
while len(view) >= AVIO_HDR_LEN:
|
|
70
|
+
frame = parse_avio_response(bytes(view))
|
|
71
|
+
if frame is None:
|
|
72
|
+
break
|
|
73
|
+
if responses.dispatch(bytes(view)):
|
|
74
|
+
answered = True
|
|
75
|
+
# One chunk can carry more than one frame: 5377 declares a 12-byte
|
|
76
|
+
# payload and has been seen arriving in a 140-byte chunk. Reading
|
|
77
|
+
# only the first would lose a reply batched behind a notify, which
|
|
78
|
+
# presents as the camera intermittently not answering.
|
|
79
|
+
view = view[AVIO_HDR_LEN + len(frame.payload):]
|
|
80
|
+
except Exception:
|
|
81
|
+
return answered
|
|
82
|
+
return answered
|
|
83
|
+
|
|
84
|
+
|
|
48
85
|
def _widen_media_rcvbuf(sock, kind: str, device_id: str = "?") -> int:
|
|
49
86
|
"""Ask for a large receive buffer on a camera media socket.
|
|
50
87
|
|
|
@@ -2756,6 +2793,12 @@ class _SdesOpenMixin:
|
|
|
2756
2793
|
# once the SCTP DataChannel is established. SdesSession reads it via
|
|
2757
2794
|
# _cmd_chan[0] to dispatch PTZ / IOCtrl commands from the main thread.
|
|
2758
2795
|
_cmd_chan: list = [None]
|
|
2796
|
+
# The other direction: matches the camera's AVIO replies to the commands
|
|
2797
|
+
# that asked for them. Created here, not in SdesSession, because the
|
|
2798
|
+
# bridge thread starts dispatching into it before the session object
|
|
2799
|
+
# exists; the session is handed the same instance so both sides share
|
|
2800
|
+
# one registry.
|
|
2801
|
+
_avio_responses = AvioResponseRouter()
|
|
2759
2802
|
# Proc holder: set to the ffmpeg Popen object after launch so the
|
|
2760
2803
|
# bridge thread can poll for exit without a NameError race.
|
|
2761
2804
|
_proc_holder: list = [None]
|
|
@@ -3807,9 +3850,17 @@ class _SdesOpenMixin:
|
|
|
3807
3850
|
if len(_pd_plain) >= 28 else 0)
|
|
3808
3851
|
_sc_cmd = (int.from_bytes(_sc_pay[4:8], 'little')
|
|
3809
3852
|
if len(_sc_pay) >= 8 else 0)
|
|
3853
|
+
# This is where the camera's answers
|
|
3854
|
+
# come back on SDES. They were parsed
|
|
3855
|
+
# and logged here long before anything
|
|
3856
|
+
# could receive them; hand them to
|
|
3857
|
+
# whoever asked.
|
|
3858
|
+
_sc_answered = _dispatch_sctp_avio(
|
|
3859
|
+
_avio_responses, _sc_pay)
|
|
3810
3860
|
_status(
|
|
3811
3861
|
f"SDES DC: enc DATA ppid={_sc_ppid}"
|
|
3812
3862
|
f" cmd={_sc_cmd} {len(_sc_pay)}B"
|
|
3863
|
+
f"{' (answered a request)' if _sc_answered else ''}"
|
|
3813
3864
|
f" {_sc_pay[:32].hex()}"
|
|
3814
3865
|
)
|
|
3815
3866
|
else:
|
|
@@ -3878,6 +3929,14 @@ class _SdesOpenMixin:
|
|
|
3878
3929
|
# f0.java:3224): the camera can send it unsolicited, and
|
|
3879
3930
|
# without it here an 804 frame is misrouted as PCMA noise.
|
|
3880
3931
|
_avio_cmds = {5376, 5377, 5156, 5157, 768, 769, 511, 804}
|
|
3932
|
+
# No response dispatch here. Measured
|
|
3933
|
+
# 2026-08-07: the camera's replies come back
|
|
3934
|
+
# as encrypted SCTP DATA (see
|
|
3935
|
+
# _dispatch_sctp_avio), never in this
|
|
3936
|
+
# framing. Offering every audio packet to
|
|
3937
|
+
# the router would be per-packet work on the
|
|
3938
|
+
# media path for something that has never
|
|
3939
|
+
# arrived on it.
|
|
3881
3940
|
if _fwd_cmd not in _avio_cmds:
|
|
3882
3941
|
try:
|
|
3883
3942
|
_lo_a.sendto(
|
|
@@ -4964,4 +5023,5 @@ class _SdesOpenMixin:
|
|
|
4964
5023
|
first_video_pt=_first_video_pt,
|
|
4965
5024
|
first_audio_pt=_first_audio_pt,
|
|
4966
5025
|
device_id=getattr(self, "device_id", None),
|
|
5026
|
+
responses=_avio_responses,
|
|
4967
5027
|
)
|
|
@@ -12,10 +12,12 @@ import struct
|
|
|
12
12
|
import time
|
|
13
13
|
from typing import Any, Callable, Optional
|
|
14
14
|
|
|
15
|
+
from .protocol import AvioRequestMixin, AvioResponseRouter
|
|
16
|
+
|
|
15
17
|
_LOGGER = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
class WebRTCSession:
|
|
20
|
+
class WebRTCSession(AvioRequestMixin):
|
|
19
21
|
"""Active WebRTC live-stream session for a liveType=2 AiDot camera.
|
|
20
22
|
|
|
21
23
|
Obtain via ``await DeviceClient.async_open_webrtc_stream(...)``.
|
|
@@ -34,6 +36,7 @@ class WebRTCSession:
|
|
|
34
36
|
audio_sender: Any = None,
|
|
35
37
|
talk_track: Any = None,
|
|
36
38
|
talk_holder: Any = None,
|
|
39
|
+
responses: Any = None,
|
|
37
40
|
) -> None:
|
|
38
41
|
self._pc = pc
|
|
39
42
|
self._outgoing_q = outgoing_q
|
|
@@ -41,6 +44,12 @@ class WebRTCSession:
|
|
|
41
44
|
self._recorder = recorder
|
|
42
45
|
self._track_tasks = track_tasks
|
|
43
46
|
self._dc = dc # RTCDataChannel for AVIO IOCtrl commands
|
|
47
|
+
# Matches inbound AVIO replies to the commands that asked for them.
|
|
48
|
+
# Created here, not lazily: the DataChannel message handler can
|
|
49
|
+
# dispatch from outside this constructor's thread.
|
|
50
|
+
self._avio_responses = (
|
|
51
|
+
responses if responses is not None else AvioResponseRouter()
|
|
52
|
+
)
|
|
44
53
|
# Two-way audio (talk): the sendrecv audio RTCRtpSender, an idle PCMA talk
|
|
45
54
|
# track, and a mutable {"provider": callable|None} holder the track reads from.
|
|
46
55
|
self._audio_sender = audio_sender
|
{python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
@@ -21,6 +21,8 @@ from .constants import _LIVE_PLAY_NOT_READY, fallback_turn_uris, stun_server_uri
|
|
|
21
21
|
from .models import VideoFrame
|
|
22
22
|
from .webrtc import WebRTCSession
|
|
23
23
|
from .protocol import (
|
|
24
|
+
AvioResponseRouter,
|
|
25
|
+
_avio_cmd_id,
|
|
24
26
|
_compress_sdp_for_camera,
|
|
25
27
|
_dedup_bundle_candidates,
|
|
26
28
|
_filter_sdp_candidates,
|
|
@@ -1729,6 +1731,12 @@ class _WebRTCOpenMixin:
|
|
|
1729
1731
|
_status(f"talk: SPEAKER{'START' if start else 'STOP'} failed:"
|
|
1730
1732
|
f" {_spk_exc}")
|
|
1731
1733
|
|
|
1734
|
+
# Matches the camera's AVIO replies to the commands that asked for
|
|
1735
|
+
# them. Created here rather than in the session because the message
|
|
1736
|
+
# handlers below are registered while the session does not exist yet;
|
|
1737
|
+
# it is handed to the session at the end so both sides share one.
|
|
1738
|
+
_avio_responses = AvioResponseRouter()
|
|
1739
|
+
|
|
1732
1740
|
# Camera-initiated DCEP path: KVS firmware on A000088 doesn't include
|
|
1733
1741
|
# m=application in its answer SDP, suggesting the data channel may
|
|
1734
1742
|
# actually be opened by the camera (master), not the viewer. Hook
|
|
@@ -1748,8 +1756,10 @@ class _WebRTCOpenMixin:
|
|
|
1748
1756
|
def _on_remote_dc_message(message) -> None:
|
|
1749
1757
|
try:
|
|
1750
1758
|
if isinstance(message, (bytes, bytearray)):
|
|
1759
|
+
_avio_responses.dispatch(bytes(message))
|
|
1751
1760
|
_status(
|
|
1752
1761
|
f"DC[remote:{channel.label}] RX {len(message)}B"
|
|
1762
|
+
f" cmd={_avio_cmd_id(message)}"
|
|
1753
1763
|
f" hex={bytes(message)[:32].hex()}"
|
|
1754
1764
|
)
|
|
1755
1765
|
else:
|
|
@@ -1874,8 +1884,10 @@ class _WebRTCOpenMixin:
|
|
|
1874
1884
|
def _on_kvs_dc_message(message) -> None:
|
|
1875
1885
|
try:
|
|
1876
1886
|
if isinstance(message, (bytes, bytearray)):
|
|
1887
|
+
_avio_responses.dispatch(bytes(message))
|
|
1877
1888
|
_status(
|
|
1878
1889
|
f"DC[{_dc_label}] RX {len(message)}B"
|
|
1890
|
+
f" cmd={_avio_cmd_id(message)}"
|
|
1879
1891
|
f" hex={bytes(message)[:32].hex()}"
|
|
1880
1892
|
)
|
|
1881
1893
|
else:
|
|
@@ -3716,4 +3728,5 @@ class _WebRTCOpenMixin:
|
|
|
3716
3728
|
audio_sender=_audio_sender,
|
|
3717
3729
|
talk_track=_talk_track,
|
|
3718
3730
|
talk_holder=_talk_holder,
|
|
3731
|
+
responses=_avio_responses,
|
|
3719
3732
|
)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.16.0"
|
|
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.7 → python_aidot_cameras-0.16.0/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.
|
|
3
|
+
Version: 0.16.0
|
|
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
|
|
@@ -73,6 +73,9 @@ tests/test_aioice_compat.py
|
|
|
73
73
|
tests/test_alarm_event.py
|
|
74
74
|
tests/test_answer_sdp_video_pt.py
|
|
75
75
|
tests/test_audio_mux_offline.py
|
|
76
|
+
tests/test_avio_request.py
|
|
77
|
+
tests/test_avio_response_parse.py
|
|
78
|
+
tests/test_avio_response_router.py
|
|
76
79
|
tests/test_backoff.py
|
|
77
80
|
tests/test_battery_detection.py
|
|
78
81
|
tests/test_battery_evidence_corroboration.py
|
|
@@ -133,6 +136,7 @@ tests/test_restart_cycle.py
|
|
|
133
136
|
tests/test_retry_policy.py
|
|
134
137
|
tests/test_scripts_importable.py
|
|
135
138
|
tests/test_sdes_adaptive.py
|
|
139
|
+
tests/test_sdes_avio_dispatch.py
|
|
136
140
|
tests/test_sdes_bridge_break_gate.py
|
|
137
141
|
tests/test_sdes_echo_wait_timeout.py
|
|
138
142
|
tests/test_sdes_fast_liveplay.py
|
|
@@ -152,6 +156,7 @@ tests/test_sdp_narrowing_properties.py
|
|
|
152
156
|
tests/test_sdp_payload_type_narrowing.py
|
|
153
157
|
tests/test_security_hardening.py
|
|
154
158
|
tests/test_serializable_login_info.py
|
|
159
|
+
tests/test_serve_loss_tolerance.py
|
|
155
160
|
tests/test_serve_relay.py
|
|
156
161
|
tests/test_session_stats.py
|
|
157
162
|
tests/test_shared_house_seam.py
|