python-aidot-cameras 1.0.0b2__tar.gz → 1.0.0b3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b3}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/client.py +16 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/sdes_open.py +61 -15
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_feature_probe_needs_a_live_session.py +28 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ice_peer_reflexive.py +7 -3
- python_aidot_cameras-1.0.0b3/tests/test_live_session_carries_talk.py +113 -0
- python_aidot_cameras-1.0.0b3/tests/test_self_peer_is_an_address_not_an_ip.py +101 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/README.md +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_webrtc_answer_race.py +0 -0
{python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b3
|
|
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
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/client.py
RENAMED
|
@@ -3477,6 +3477,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3477
3477
|
rtsp_push_url=self._keepalive_rtsp_url,
|
|
3478
3478
|
timeout=(_FAST_OPEN_TIMEOUT if _use_fast else 120.0),
|
|
3479
3479
|
reuse_peer_id=_loop_peer_id,
|
|
3480
|
+
# Ask for talk so THIS session can carry it. async_speak
|
|
3481
|
+
# reuses _stream_session only when it is talk-capable, and
|
|
3482
|
+
# an SDES session that did not ask never gets a return audio
|
|
3483
|
+
# track - so without this every press-to-talk opened a
|
|
3484
|
+
# second camera session alongside the live view, and the
|
|
3485
|
+
# camera holds a viewer slot for about 120 s after one ends.
|
|
3486
|
+
#
|
|
3487
|
+
# This changes the SDP offer on the path every SDES live
|
|
3488
|
+
# view takes, which is the kind of change that has caused
|
|
3489
|
+
# fleet-wide blackouts here, so it rests on evidence rather
|
|
3490
|
+
# than reasoning: the release harness has opened all seven
|
|
3491
|
+
# cameras talk-capable across three fleet runs with no
|
|
3492
|
+
# streaming regression. The flag reaches only the offer
|
|
3493
|
+
# (audio sendrecv + a=ssrc) and the shared talk state; it
|
|
3494
|
+
# does not touch the rtsp_push branch this loop uses.
|
|
3495
|
+
talk=True,
|
|
3480
3496
|
)
|
|
3481
3497
|
except asyncio.CancelledError:
|
|
3482
3498
|
self._fast_attempt_override = None
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -835,6 +835,42 @@ def _bridge_should_break(rc, teardown_requested: bool) -> bool:
|
|
|
835
835
|
_MAX_PRFLX_CANDS = 4
|
|
836
836
|
|
|
837
837
|
|
|
838
|
+
def _is_self_transport_address(ip, port, *, local_ip, public_ip, own_ports):
|
|
839
|
+
"""Is this address US, rather than a peer that merely looks like us?
|
|
840
|
+
|
|
841
|
+
The guard exists so we never nominate our own address: doing that has this
|
|
842
|
+
host answering its own connectivity check, which would arm the media trigger
|
|
843
|
+
at nobody. What it must NOT do is refuse the camera, and for one common
|
|
844
|
+
deployment it did - a camera in the same house reaches the TURN server from
|
|
845
|
+
the same public IP we do, so an address-only comparison called it us. Seven
|
|
846
|
+
first-media stalls were that (`vetoed-self-ip`, ROAD-TO-1.0 item 3): the
|
|
847
|
+
camera's relay-carried Binding Request could not be answered, because the
|
|
848
|
+
branch that wraps the response in a Send Indication is guarded by this, so
|
|
849
|
+
ICE never completed and no media ever started.
|
|
850
|
+
|
|
851
|
+
ICE identifies a candidate by its transport address, and so does this: our
|
|
852
|
+
own is one ip:PORT, and a peer sharing our NAT has the same ip with a
|
|
853
|
+
different port. Comparing the pair keeps the guard exactly as strong for
|
|
854
|
+
the case it was written for and stops it catching the camera.
|
|
855
|
+
|
|
856
|
+
``port`` may be None where a caller genuinely does not know it. That keeps
|
|
857
|
+
the old, conservative answer - widening a decision on an address we cannot
|
|
858
|
+
fully identify would be guessing, and this guard is the safe default.
|
|
859
|
+
|
|
860
|
+
Loopback and this host's own LAN address are us on ANY port: a different
|
|
861
|
+
port there is still this machine.
|
|
862
|
+
|
|
863
|
+
Pure, so the policy is testable without a camera.
|
|
864
|
+
"""
|
|
865
|
+
if not ip:
|
|
866
|
+
return False
|
|
867
|
+
if ip in {"127.0.0.1", "0.0.0.0", local_ip}:
|
|
868
|
+
return True
|
|
869
|
+
if public_ip and ip == public_ip:
|
|
870
|
+
return port is None or port in set(own_ports or ())
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
|
|
838
874
|
def _record_peer_reflexive(known, discovered, observed, is_self=None):
|
|
839
875
|
"""Learn the address a STUN probe actually arrived from.
|
|
840
876
|
|
|
@@ -859,7 +895,7 @@ def _record_peer_reflexive(known, discovered, observed, is_self=None):
|
|
|
859
895
|
_ip, _port = observed
|
|
860
896
|
if not _ip or not _port:
|
|
861
897
|
return discovered
|
|
862
|
-
if is_self is not None and is_self(_ip):
|
|
898
|
+
if is_self is not None and is_self(_ip, _port):
|
|
863
899
|
return discovered
|
|
864
900
|
if observed in known or observed in discovered:
|
|
865
901
|
return discovered
|
|
@@ -2386,14 +2422,18 @@ class _SdesOpenMixin:
|
|
|
2386
2422
|
+ (f" and :{_hp_port2}" if _hp_port != _hp_port2 else "")
|
|
2387
2423
|
)
|
|
2388
2424
|
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
return
|
|
2425
|
+
# The ports our own srflx candidates advertise - the only ports on the
|
|
2426
|
+
# public IP that are actually this host. Anything else there is another
|
|
2427
|
+
# device behind the same NAT, which for this fleet means the camera.
|
|
2428
|
+
_own_srflx_ports = (audio_port, video_port)
|
|
2429
|
+
|
|
2430
|
+
def _is_self_peer_ip(_ip: "Optional[str]", _port: "Optional[int]" = None
|
|
2431
|
+
) -> bool:
|
|
2432
|
+
return _is_self_transport_address(
|
|
2433
|
+
_ip, _port,
|
|
2434
|
+
local_ip=local_ip, public_ip=_public_ip,
|
|
2435
|
+
own_ports=_own_srflx_ports,
|
|
2436
|
+
)
|
|
2397
2437
|
|
|
2398
2438
|
_selfloop_drop_count = 0
|
|
2399
2439
|
_bridge_selfloop_drop_count = 0
|
|
@@ -2526,7 +2566,8 @@ class _SdesOpenMixin:
|
|
|
2526
2566
|
if _turn_peer_ip_sw and _prefer_direct_stun.get(_sock, False):
|
|
2527
2567
|
pass
|
|
2528
2568
|
elif (_turn_peer_ip_sw and _sock in _relay_addrs
|
|
2529
|
-
and not _is_self_peer_ip(_turn_peer_ip_sw
|
|
2569
|
+
and not _is_self_peer_ip(_turn_peer_ip_sw,
|
|
2570
|
+
_turn_peer_port_sw)):
|
|
2530
2571
|
# Arrived via TURN - respond via Send Indication
|
|
2531
2572
|
_ri_sw = _relay_addrs[_sock]
|
|
2532
2573
|
_t_host_sw, _t_port_sw = _ri_sw[4], _ri_sw[5]
|
|
@@ -2552,7 +2593,8 @@ class _SdesOpenMixin:
|
|
|
2552
2593
|
+ _STUN_MAGIC + os.urandom(12)
|
|
2553
2594
|
+ _si_body)
|
|
2554
2595
|
_sock.sendto(_send_ind, (_t_host_sw, _t_port_sw))
|
|
2555
|
-
elif _turn_peer_ip_sw and _is_self_peer_ip(
|
|
2596
|
+
elif _turn_peer_ip_sw and _is_self_peer_ip(
|
|
2597
|
+
_turn_peer_ip_sw, _turn_peer_port_sw):
|
|
2556
2598
|
# Self-loop Data Indication (peer == our own
|
|
2557
2599
|
# local/srflx address). Responding via TURN
|
|
2558
2600
|
# creates an endless STUN echo loop and no media.
|
|
@@ -3081,7 +3123,7 @@ class _SdesOpenMixin:
|
|
|
3081
3123
|
# addresses, and still ZERO relay-carried inbound packets.
|
|
3082
3124
|
# The direct send above is untouched, so directly reachable
|
|
3083
3125
|
# cameras behave exactly as before.
|
|
3084
|
-
if not _is_self_peer_ip(cam_addr[0]):
|
|
3126
|
+
if not _is_self_peer_ip(cam_addr[0], cam_addr[1]):
|
|
3085
3127
|
_turn_send_indication(sock, cam_addr[0], cam_addr[1], _req)
|
|
3086
3128
|
|
|
3087
3129
|
# Every candidate this session actually sent USE-CANDIDATE to, in the
|
|
@@ -4002,7 +4044,8 @@ class _SdesOpenMixin:
|
|
|
4002
4044
|
_br_cam_peer = (
|
|
4003
4045
|
(_br_turn_peer_ip, _br_turn_peer_port)
|
|
4004
4046
|
if _br_turn_peer_ip and _br_turn_peer_port
|
|
4005
|
-
and not _is_self_peer_ip(_br_turn_peer_ip
|
|
4047
|
+
and not _is_self_peer_ip(_br_turn_peer_ip,
|
|
4048
|
+
_br_turn_peer_port)
|
|
4006
4049
|
else None
|
|
4007
4050
|
)
|
|
4008
4051
|
|
|
@@ -4110,7 +4153,9 @@ class _SdesOpenMixin:
|
|
|
4110
4153
|
if _br_turn_peer_ip and _br_prefer_direct_stun.get(_bs, False):
|
|
4111
4154
|
pass
|
|
4112
4155
|
elif (_br_turn_peer_ip and _bs in _relay_addrs
|
|
4113
|
-
and not _is_self_peer_ip(
|
|
4156
|
+
and not _is_self_peer_ip(
|
|
4157
|
+
_br_turn_peer_ip,
|
|
4158
|
+
_br_turn_peer_port)):
|
|
4114
4159
|
# Arrived via TURN - respond via Send Indication
|
|
4115
4160
|
_bri = _relay_addrs[_bs]
|
|
4116
4161
|
_br_t_host, _br_t_port = _bri[4], _bri[5]
|
|
@@ -4143,7 +4188,8 @@ class _SdesOpenMixin:
|
|
|
4143
4188
|
+ _br_si_body
|
|
4144
4189
|
)
|
|
4145
4190
|
_bs.sendto(_br_send_ind, (_br_t_host, _br_t_port))
|
|
4146
|
-
elif _br_turn_peer_ip and _is_self_peer_ip(
|
|
4191
|
+
elif _br_turn_peer_ip and _is_self_peer_ip(
|
|
4192
|
+
_br_turn_peer_ip, _br_turn_peer_port):
|
|
4147
4193
|
_bridge_selfloop_drop_count += 1
|
|
4148
4194
|
if (_bridge_selfloop_drop_count <= 5
|
|
4149
4195
|
or _bridge_selfloop_drop_count % 50 == 0):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0b3"
|
|
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-1.0.0b2 → python_aidot_cameras-1.0.0b3/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: 1.0.
|
|
3
|
+
Version: 1.0.0b3
|
|
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
|
|
@@ -119,6 +119,7 @@ tests/test_keyframe_prompter.py
|
|
|
119
119
|
tests/test_lan_control.py
|
|
120
120
|
tests/test_lan_retry_reaches_the_storming_devices.py
|
|
121
121
|
tests/test_light_active_color_mode.py
|
|
122
|
+
tests/test_live_session_carries_talk.py
|
|
122
123
|
tests/test_live_stream_param.py
|
|
123
124
|
tests/test_live_validate_policy.py
|
|
124
125
|
tests/test_logging_caps.py
|
|
@@ -176,6 +177,7 @@ tests/test_sdes_watchdog.py
|
|
|
176
177
|
tests/test_sdp_narrowing_properties.py
|
|
177
178
|
tests/test_sdp_payload_type_narrowing.py
|
|
178
179
|
tests/test_security_hardening.py
|
|
180
|
+
tests/test_self_peer_is_an_address_not_an_ip.py
|
|
179
181
|
tests/test_serializable_login_info.py
|
|
180
182
|
tests/test_serve_loss_tolerance.py
|
|
181
183
|
tests/test_serve_relay.py
|
|
@@ -79,6 +79,34 @@ def test_ptz_is_not_run_when_the_session_is_already_closed():
|
|
|
79
79
|
assert "closed" in out["ptz_error"]
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def test_the_snapshot_reports_how_long_it_took():
|
|
83
|
+
# The SDES snapshot budget has been wrong twice and both times the only
|
|
84
|
+
# evidence was a verdict, so the replacement budget was a guess. Reporting
|
|
85
|
+
# the elapsed time makes the next runs answer it.
|
|
86
|
+
class _Snapper(_DeviceClient):
|
|
87
|
+
async def async_snapshot(self, path, timeout=10.0):
|
|
88
|
+
return False # verdict is irrelevant here; the timing is not
|
|
89
|
+
|
|
90
|
+
out = asyncio.run(probe_features(_Snapper(), {}, _Session(alive=True)))
|
|
91
|
+
assert "snapshot_s" in out
|
|
92
|
+
assert isinstance(out["snapshot_s"], float)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_the_snapshot_budget_clears_the_slowest_measured_snapshot():
|
|
96
|
+
# Measured across the fleet: SDES 17.2-23.6 s, DTLS 2.8-3.0 s. Two earlier
|
|
97
|
+
# budgets were set just above the then-known maximum and both timed out a
|
|
98
|
+
# camera afterwards, so this asserts headroom rather than a value.
|
|
99
|
+
from feature_probe import _snapshot_budget
|
|
100
|
+
|
|
101
|
+
slowest_sdes_seen = 23.6
|
|
102
|
+
sdes = _snapshot_budget({"modelId": "LK.IPC.A001064"}, 10.0)
|
|
103
|
+
assert sdes >= slowest_sdes_seen * 1.5, (
|
|
104
|
+
"an SDES budget without real headroom is how this failed twice")
|
|
105
|
+
# DTLS is a different path and an order of magnitude faster; widening it
|
|
106
|
+
# would only slow down reporting a camera that is genuinely broken.
|
|
107
|
+
assert _snapshot_budget({"modelId": "LK.IPC.A000088"}, 10.0) == 10.0
|
|
108
|
+
|
|
109
|
+
|
|
82
110
|
def test_a_session_that_does_not_publish_liveness_counts_as_live():
|
|
83
111
|
# Only SdesSession has is_alive - it tracks the ffmpeg the bridge dies with.
|
|
84
112
|
# The DTLS session keeps no ffmpeg and publishes nothing, and absence of the
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ice_peer_reflexive.py
RENAMED
|
@@ -14,7 +14,11 @@ from aidot_cameras.camera.sdes_open import (
|
|
|
14
14
|
ADVERTISED = [("192.168.100.13", 40000)]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def _never_self(_ip):
|
|
17
|
+
def _never_self(_ip, _port=None):
|
|
18
|
+
# The predicate takes a transport address, not an address: a peer behind
|
|
19
|
+
# our own NAT shares our public IP on a different port, and refusing it on
|
|
20
|
+
# the IP alone stalled real streams. See
|
|
21
|
+
# test_self_peer_is_an_address_not_an_ip.py.
|
|
18
22
|
return False
|
|
19
23
|
|
|
20
24
|
|
|
@@ -53,8 +57,8 @@ def test_our_own_address_is_never_learned():
|
|
|
53
57
|
|
|
54
58
|
Nominating ourselves would form a pair that can never carry camera media.
|
|
55
59
|
"""
|
|
56
|
-
def _is_self(ip):
|
|
57
|
-
return ip == "192.168.0.110"
|
|
60
|
+
def _is_self(ip, port=None):
|
|
61
|
+
return (ip, port) == ("192.168.0.110", 3478)
|
|
58
62
|
|
|
59
63
|
out = _record_peer_reflexive(
|
|
60
64
|
ADVERTISED, [], ("192.168.0.110", 3478), _is_self)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""The SDES live-view session is opened talk-capable, so talk reuses it.
|
|
2
|
+
|
|
3
|
+
`async_speak` reuses `_stream_session` when it is talk-capable and otherwise
|
|
4
|
+
opens a session of its own. None of the loops that set `_stream_session` opened
|
|
5
|
+
with `talk=True`, and on SDES a session that did not ask for talk never gets a
|
|
6
|
+
return audio track - so the reuse branch was unreachable and every press-to-talk
|
|
7
|
+
opened a SECOND camera session alongside the live view. The camera holds a viewer
|
|
8
|
+
slot for about 120 s after a session ends, so that is not free.
|
|
9
|
+
|
|
10
|
+
The offer change this rests on is not new code: the release harness has opened
|
|
11
|
+
every camera talk-capable across three fleet runs and seven cameras with no
|
|
12
|
+
streaming regression. What is new is the keepalive loop asking for it.
|
|
13
|
+
|
|
14
|
+
Two things are locked here, because either alone would rot:
|
|
15
|
+
- the SDES keepalive loop asks for talk;
|
|
16
|
+
- `async_speak` really does reuse a talk-capable live session instead of
|
|
17
|
+
opening another one.
|
|
18
|
+
"""
|
|
19
|
+
import asyncio
|
|
20
|
+
import inspect
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
25
|
+
|
|
26
|
+
import aidot_cameras.camera.client as cc
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fn_source(qualname: str) -> str:
|
|
30
|
+
for cls in vars(cc).values():
|
|
31
|
+
if isinstance(cls, type) and qualname in cls.__dict__:
|
|
32
|
+
return inspect.getsource(cls.__dict__[qualname])
|
|
33
|
+
raise AssertionError(f"{qualname} not found")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_the_sdes_keepalive_loop_opens_talk_capable():
|
|
37
|
+
src = _fn_source("_sdes_keepalive_loop_inner")
|
|
38
|
+
# The loop has exactly one async_open_webrtc_stream call; it must ask for
|
|
39
|
+
# talk, or the session it publishes as _stream_session cannot carry any.
|
|
40
|
+
assert src.count("async_open_webrtc_stream") == 1, (
|
|
41
|
+
"this test assumes a single open call in the loop - re-read it if that "
|
|
42
|
+
"changed")
|
|
43
|
+
assert "talk=True" in src, (
|
|
44
|
+
"the live-view session must be talk-capable or async_speak can never "
|
|
45
|
+
"reuse it")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Session:
|
|
49
|
+
"""A live, talk-capable session that records what was asked of it."""
|
|
50
|
+
|
|
51
|
+
talk_supported = True
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.started = 0
|
|
55
|
+
self.stopped = 0
|
|
56
|
+
|
|
57
|
+
async def async_start_talk(self, provider):
|
|
58
|
+
self.started += 1
|
|
59
|
+
while provider() is not None:
|
|
60
|
+
pass # drain the clip as the real pump would
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
async def async_stop_talk(self):
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
async def stop(self):
|
|
67
|
+
self.stopped += 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _Client:
|
|
71
|
+
"""Just enough of the device client for async_speak's reuse branch."""
|
|
72
|
+
|
|
73
|
+
device_id = "cam"
|
|
74
|
+
|
|
75
|
+
def __init__(self, session):
|
|
76
|
+
self._stream_session = session
|
|
77
|
+
self.opens = 0
|
|
78
|
+
|
|
79
|
+
async def async_open_webrtc_stream(self, *a, **kw):
|
|
80
|
+
self.opens += 1
|
|
81
|
+
raise AssertionError("async_speak must not open a second session")
|
|
82
|
+
|
|
83
|
+
async_speak = cc.CameraMixin.async_speak
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_async_speak_reuses_the_live_session_instead_of_opening_another():
|
|
87
|
+
session = _Session()
|
|
88
|
+
client = _Client(session)
|
|
89
|
+
frames = iter([b"\x00" * 320] * 3)
|
|
90
|
+
|
|
91
|
+
ok = asyncio.run(client.async_speak(lambda: next(frames, None),
|
|
92
|
+
max_seconds=5))
|
|
93
|
+
|
|
94
|
+
assert ok is True
|
|
95
|
+
assert client.opens == 0 # no second camera session
|
|
96
|
+
assert session.started == 1
|
|
97
|
+
# And it must not close the session it borrowed - the live view is using it.
|
|
98
|
+
assert session.stopped == 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
import traceback
|
|
103
|
+
_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
104
|
+
_fail = 0
|
|
105
|
+
for _fn in _fns:
|
|
106
|
+
try:
|
|
107
|
+
_fn()
|
|
108
|
+
print(f"PASS {_fn.__name__}")
|
|
109
|
+
except Exception:
|
|
110
|
+
_fail += 1
|
|
111
|
+
print(f"FAIL {_fn.__name__}")
|
|
112
|
+
traceback.print_exc()
|
|
113
|
+
raise SystemExit(1 if _fail else 0)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""A camera behind our own NAT is not us, and refusing it stalls the stream.
|
|
2
|
+
|
|
3
|
+
Seven first-media stall reports name `vetoed-self-ip` (see ROAD-TO-1.0 item 3).
|
|
4
|
+
The shape is always the same: the camera's connectivity check reaches us as a
|
|
5
|
+
TURN Data Indication whose XOR-PEER-ADDRESS is THIS host's public IP on a port
|
|
6
|
+
that is not ours - because the camera sits behind the same NAT, on another
|
|
7
|
+
subnet of the same house. `_is_self_peer_ip` compared the address alone, so it
|
|
8
|
+
answered "that is us".
|
|
9
|
+
|
|
10
|
+
Two things then break, and the second is the stall:
|
|
11
|
+
|
|
12
|
+
* the camera's address is never learned as a peer-reflexive candidate;
|
|
13
|
+
* the Binding Success Response is never sent back through the relay, because
|
|
14
|
+
the branch that wraps it in a Send Indication is guarded by the same check.
|
|
15
|
+
|
|
16
|
+
With no response, the camera's check never completes, ICE never succeeds, the
|
|
17
|
+
AVIO LIVING trigger is never armed and the camera never sends a byte. That is
|
|
18
|
+
exactly `binding-success=0; trigger=not-sent` with no media.
|
|
19
|
+
|
|
20
|
+
The safety property this check exists for is unchanged and must stay unchanged:
|
|
21
|
+
nominating OUR OWN address would have us answer our own check. Our own address
|
|
22
|
+
is one ip:PORT. A peer sharing our NAT has the same ip and a different port, so
|
|
23
|
+
comparing the pair keeps the guard and drops the false positive - which is also
|
|
24
|
+
what ICE means by a transport address.
|
|
25
|
+
"""
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
30
|
+
|
|
31
|
+
from aidot_cameras.camera.sdes_open import (
|
|
32
|
+
_is_self_transport_address,
|
|
33
|
+
_record_peer_reflexive,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
LOCAL = "192.168.0.50"
|
|
37
|
+
PUBLIC = "173.53.36.206"
|
|
38
|
+
OURS = (41000, 41002) # the ports our own srflx candidates advertise
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_self(ip, port=None):
|
|
42
|
+
return _is_self_transport_address(
|
|
43
|
+
ip, port, local_ip=LOCAL, public_ip=PUBLIC, own_ports=OURS)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_our_own_reflexive_address_is_still_refused():
|
|
47
|
+
# The guard's whole purpose. Both advertised ports, exactly as offered.
|
|
48
|
+
assert _is_self(PUBLIC, 41000) is True
|
|
49
|
+
assert _is_self(PUBLIC, 41002) is True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_loopback_and_this_hosts_lan_address_are_refused_on_any_port():
|
|
53
|
+
for ip in ("127.0.0.1", "0.0.0.0", LOCAL):
|
|
54
|
+
assert _is_self(ip, 41000) is True
|
|
55
|
+
assert _is_self(ip, 59999) is True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_a_camera_sharing_our_public_ip_on_another_port_is_not_us():
|
|
59
|
+
# The measured case: XOR-PEER-ADDRESS is our public IP, port 49887, which
|
|
60
|
+
# is the port the camera advertised on its own host candidate.
|
|
61
|
+
assert _is_self(PUBLIC, 49887) is False
|
|
62
|
+
assert _is_self(PUBLIC, 46836) is False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_an_unknown_port_still_refuses_the_public_ip():
|
|
66
|
+
# Call sites that cannot supply a port keep the old, conservative answer.
|
|
67
|
+
# Widening those blind would be guessing, and the guard is the safe default.
|
|
68
|
+
assert _is_self(PUBLIC, None) is True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_an_unrelated_address_is_never_us():
|
|
72
|
+
assert _is_self("54.144.38.43", 5349) is False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_the_learner_uses_the_port_too():
|
|
76
|
+
# _record_peer_reflexive is where the address becomes a nomination target,
|
|
77
|
+
# so it has to ask the same question the same way.
|
|
78
|
+
advertised = [("192.168.100.3", 34400)]
|
|
79
|
+
|
|
80
|
+
learned = _record_peer_reflexive(
|
|
81
|
+
advertised, [], (PUBLIC, 49887), _is_self)
|
|
82
|
+
assert learned == [(PUBLIC, 49887)], "a peer behind our NAT must be learnable"
|
|
83
|
+
|
|
84
|
+
refused = _record_peer_reflexive(
|
|
85
|
+
advertised, [], (PUBLIC, 41000), _is_self)
|
|
86
|
+
assert refused == [], "our own advertised address must never be learned"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
import traceback
|
|
91
|
+
_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
92
|
+
_fail = 0
|
|
93
|
+
for _fn in _fns:
|
|
94
|
+
try:
|
|
95
|
+
_fn()
|
|
96
|
+
print(f"PASS {_fn.__name__}")
|
|
97
|
+
except Exception:
|
|
98
|
+
_fail += 1
|
|
99
|
+
print(f"FAIL {_fn.__name__}")
|
|
100
|
+
traceback.print_exc()
|
|
101
|
+
raise SystemExit(1 if _fail else 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/clock.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/go2rtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/hwaccel.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/models.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/webrtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/device_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|