python-aidot-cameras 0.17.3b2__tar.gz → 1.0.0b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-0.17.3b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b2}/PKG-INFO +3 -1
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/README.md +2 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/client.py +13 -4
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/protocol.py +12 -4
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes.py +28 -8
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes_open.py +371 -10
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info}/PKG-INFO +3 -1
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/SOURCES.txt +9 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_control_verdicts.py +12 -2
- python_aidot_cameras-1.0.0b2/tests/test_dtls_resolution_ack.py +238 -0
- python_aidot_cameras-1.0.0b2/tests/test_feature_probe_needs_a_live_session.py +104 -0
- python_aidot_cameras-1.0.0b2/tests/test_gate_requires_a_decoded_frame.py +101 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_live_validate_policy.py +182 -0
- python_aidot_cameras-1.0.0b2/tests/test_no_key_material_in_logs.py +269 -0
- python_aidot_cameras-1.0.0b2/tests/test_ptz_probe_reads_what_it_is_told.py +88 -0
- python_aidot_cameras-1.0.0b2/tests/test_sdes_first_media_stall_report.py +344 -0
- python_aidot_cameras-1.0.0b2/tests/test_sdes_offer_video_order.py +263 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_talk.py +16 -0
- python_aidot_cameras-1.0.0b2/tests/test_sprop_write_is_reported_truthfully.py +102 -0
- python_aidot_cameras-1.0.0b2/tests/test_talk_reports_a_speaker_that_never_opened.py +170 -0
- python_aidot_cameras-1.0.0b2/tests/test_tutk_path_is_out_of_reach.py +78 -0
- python_aidot_cameras-0.17.3b2/tests/test_no_key_material_in_logs.py +0 -140
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/LICENSE +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/setup.cfg +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_remb.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_webrtc_answer_race.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0b2
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -209,6 +209,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
209
209
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
210
210
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
211
211
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
212
|
+
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
213
|
+
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. | unset (`96 97`) |
|
|
212
214
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
213
215
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
214
216
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
@@ -178,6 +178,8 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
178
178
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
179
179
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
180
180
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
181
|
+
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
182
|
+
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. | unset (`96 97`) |
|
|
181
183
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
182
184
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
183
185
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
{python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/client.py
RENAMED
|
@@ -1823,13 +1823,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1823
1823
|
self.device_id, len(data))
|
|
1824
1824
|
elif isinstance(body, dict):
|
|
1825
1825
|
data = body.get("data") or {}
|
|
1826
|
+
# Keys, never values: this response can carry the per-device
|
|
1827
|
+
# streaming credentials (tutkAccount / tutkPassword - see
|
|
1828
|
+
# docs/DEFERRED_FEATURES.md), and the warning below is logged
|
|
1829
|
+
# at default level, so a whole-body dump lands in every user's
|
|
1830
|
+
# home-assistant.log. The matched-device log further down
|
|
1831
|
+
# already prints keys only; these two did not.
|
|
1826
1832
|
if data:
|
|
1827
|
-
_LOGGER.debug(
|
|
1828
|
-
|
|
1833
|
+
_LOGGER.debug(
|
|
1834
|
+
"batchGetDeviceUserInfo response for %s (status=%d):"
|
|
1835
|
+
" keys=%s, %d item(s)",
|
|
1836
|
+
self.device_id, status, sorted(body.keys()), len(data),
|
|
1837
|
+
)
|
|
1829
1838
|
else:
|
|
1830
1839
|
_LOGGER.warning(
|
|
1831
|
-
"batchGetDeviceUserInfo no data for %s (status=%d):
|
|
1832
|
-
self.device_id, status, body,
|
|
1840
|
+
"batchGetDeviceUserInfo no data for %s (status=%d): keys=%s",
|
|
1841
|
+
self.device_id, status, sorted(body.keys()),
|
|
1833
1842
|
)
|
|
1834
1843
|
else:
|
|
1835
1844
|
data = {}
|
{python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -662,8 +662,14 @@ def _load_sprop(devid: str) -> "Optional[str]":
|
|
|
662
662
|
return None
|
|
663
663
|
|
|
664
664
|
|
|
665
|
-
def _save_sprop(devid: str, sprop: str) ->
|
|
666
|
-
"""Persist the ``sprop-parameter-sets``
|
|
665
|
+
def _save_sprop(devid: str, sprop: str) -> bool:
|
|
666
|
+
"""Persist the ``sprop-parameter-sets`` for ``devid``. True if it wrote.
|
|
667
|
+
|
|
668
|
+
Returns whether anything was actually cached, because two of the three
|
|
669
|
+
outcomes here write nothing and the caller announces the write in the only
|
|
670
|
+
log line this feature has. An unconditional announcement claims a cache
|
|
671
|
+
that an unstable camera provably does not have, which is the observable an
|
|
672
|
+
investigation reads first.
|
|
667
673
|
|
|
668
674
|
If a DIFFERENT value was already cached, this camera does not keep its
|
|
669
675
|
parameter sets stable: mark it and drop the cache, so nothing is injected
|
|
@@ -672,7 +678,7 @@ def _save_sprop(devid: str, sprop: str) -> None:
|
|
|
672
678
|
loses nothing by the injection being skipped.
|
|
673
679
|
"""
|
|
674
680
|
if _sprop_is_unstable(devid):
|
|
675
|
-
return
|
|
681
|
+
return False # already known unstable; nothing to cache for this camera
|
|
676
682
|
try:
|
|
677
683
|
prev = _load_sprop(devid)
|
|
678
684
|
if prev is not None and prev != sprop:
|
|
@@ -689,17 +695,19 @@ def _save_sprop(devid: str, sprop: str) -> None:
|
|
|
689
695
|
os.remove(_sprop_cache_path(devid))
|
|
690
696
|
except OSError:
|
|
691
697
|
pass
|
|
692
|
-
return
|
|
698
|
+
return False
|
|
693
699
|
os.makedirs(_SPROP_DIR, exist_ok=True)
|
|
694
700
|
tmp = _sprop_cache_path(devid) + ".tmp"
|
|
695
701
|
with open(tmp, "w") as fh:
|
|
696
702
|
fh.write(sprop)
|
|
697
703
|
os.replace(tmp, _sprop_cache_path(devid))
|
|
704
|
+
return True
|
|
698
705
|
except OSError as exc:
|
|
699
706
|
# Surface (don't swallow): if the cache dir isn't writable the whole
|
|
700
707
|
# sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
|
|
701
708
|
_LOGGER.warning("sprop cache write failed (%s): %s - set AIDOT_SPROP_DIR "
|
|
702
709
|
"to a writable path", _SPROP_DIR, exc)
|
|
710
|
+
return False
|
|
703
711
|
|
|
704
712
|
|
|
705
713
|
def _inject_sprop(sdp: str, devid: str) -> str:
|
|
@@ -133,12 +133,19 @@ class SdesSession(AvioRequestMixin):
|
|
|
133
133
|
def talk_supported(self) -> bool:
|
|
134
134
|
"""True if two-way audio can be used on this SDES session.
|
|
135
135
|
|
|
136
|
-
Requires a talk-capable open (offer advertised sendrecv + a=ssrc)
|
|
137
|
-
camera audio address is filled in by
|
|
138
|
-
|
|
139
|
-
the address before emitting.
|
|
136
|
+
Requires a talk-capable open (offer advertised sendrecv + a=ssrc) AND a
|
|
137
|
+
session that is still running. The camera audio address is filled in by
|
|
138
|
+
the bridge on first inbound audio; async_start_talk tolerates being
|
|
139
|
+
called before that - the pump waits for the address before emitting.
|
|
140
|
+
|
|
141
|
+
Liveness is part of the answer because the talk state is a plain dict
|
|
142
|
+
that outlives ffmpeg, while everything that acts on it - the bridge
|
|
143
|
+
thread that sends SPEAKERSTART, the socket the pump writes to - dies
|
|
144
|
+
with ffmpeg. ``async_speak`` reuses the warm session whenever this is
|
|
145
|
+
true, so answering from the dict alone routed talk into a session that
|
|
146
|
+
could no longer carry it instead of opening a fresh one.
|
|
140
147
|
"""
|
|
141
|
-
return bool(self._talk_state)
|
|
148
|
+
return bool(self._talk_state) and self.is_alive
|
|
142
149
|
|
|
143
150
|
async def async_start_talk(
|
|
144
151
|
self, pcm_provider: "Callable[[], Optional[bytes]]"
|
|
@@ -170,10 +177,23 @@ class SdesSession(AvioRequestMixin):
|
|
|
170
177
|
target=_run_sdes_talk_pump, args=(self._talk_state,), daemon=True
|
|
171
178
|
)
|
|
172
179
|
self._talk_thread.start()
|
|
173
|
-
|
|
180
|
+
accepted = await _speaker_ack_accepted(waiter, SDES_SPEAKER_ACK_TIMEOUT_S)
|
|
181
|
+
# _speaker_ack_accepted is generous by design: it treats an unfamiliar or
|
|
182
|
+
# absent ack as acceptance, because no genuine refusal has ever been
|
|
183
|
+
# identified and guessing at one would break working cameras. That
|
|
184
|
+
# generosity is about INTERPRETING the camera's answer, and it has
|
|
185
|
+
# nothing to say about the case where the question was never asked.
|
|
186
|
+
# `speaker_on` is set on exactly one line - immediately after the bridge
|
|
187
|
+
# thread dispatches SPEAKERSTART(848) - so a falsy flag here means our
|
|
188
|
+
# own bridge never sent the command, which happens whenever ffmpeg (and
|
|
189
|
+
# with it that thread) has already exited. The ack budget above already
|
|
190
|
+
# covers SDES_SPEAKERSTART_DELAY, so a live bridge has always set it by
|
|
191
|
+
# now and this cannot race a working session.
|
|
192
|
+
if accepted and self._talk_state.get("speaker_on"):
|
|
174
193
|
return True
|
|
175
|
-
#
|
|
176
|
-
#
|
|
194
|
+
# Either the camera refused, or the speaker command never left this
|
|
195
|
+
# process. Stop the microphone rather than stream viewer audio at a
|
|
196
|
+
# speaker that never opened.
|
|
177
197
|
self._talk_state["want_speaker"] = False
|
|
178
198
|
self._talk_state["provider"] = None
|
|
179
199
|
return False
|
{python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -494,6 +494,107 @@ def _resolve_sdes_video_pt() -> Optional[int]:
|
|
|
494
494
|
return pt if pt in _SDES_ANSWER_VIDEO_PTS else None
|
|
495
495
|
|
|
496
496
|
|
|
497
|
+
#: The video codecs the SDES OFFER advertises, keyed by payload type, with the
|
|
498
|
+
#: rtpmap/fmtp lines that describe each one. Deliberately NOT reusing
|
|
499
|
+
#: ``_SDES_ANSWER_VIDEO_PTS``: that tuple is what the answer template carries
|
|
500
|
+
#: and what the pin is validated against, and coupling the offer's wire order to
|
|
501
|
+
#: it means a later reorder of one silently reorders the other.
|
|
502
|
+
_SDES_OFFER_VIDEO_CODECS = {
|
|
503
|
+
96: (
|
|
504
|
+
"a=rtpmap:96 H264/90000\r\n"
|
|
505
|
+
"a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;"
|
|
506
|
+
"profile-level-id=42e01f\r\n"
|
|
507
|
+
),
|
|
508
|
+
97: (
|
|
509
|
+
"a=rtpmap:97 H265/90000\r\n"
|
|
510
|
+
"a=fmtp:97 level-id=93\r\n"
|
|
511
|
+
),
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#: The order those codecs go on the wire today: H264 first, H265 second. This
|
|
515
|
+
#: is the shipped default and changing it changes every SDES camera's
|
|
516
|
+
#: negotiation, so it is overridden per-run by env and never edited here.
|
|
517
|
+
_SDES_OFFER_VIDEO_PT_ORDER = (96, 97)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _resolve_sdes_video_pt_order() -> tuple:
|
|
521
|
+
"""EXPERIMENTAL (opt-in, default off): reorder the OFFER's video codec list.
|
|
522
|
+
|
|
523
|
+
RFC 3264 section 5.1 makes the ``m=video`` payload-type list a *preference*
|
|
524
|
+
list, most-preferred first. The offer this module sends carries ``96 97``,
|
|
525
|
+
i.e. it already states a preference for H264 -- so the often-repeated
|
|
526
|
+
shorthand that our offer "expresses no preference" is not what the SDP says.
|
|
527
|
+
What is true is that nothing here ever *chose* that order: the line arrived
|
|
528
|
+
verbatim when this path was extracted from ``client.py`` and has never been
|
|
529
|
+
varied.
|
|
530
|
+
|
|
531
|
+
Measured on an A001064, the camera answers H264 most of the time and H265
|
|
532
|
+
occasionally for an otherwise identical request (nine and two across eleven
|
|
533
|
+
sessions one afternoon). Read against the offer, that is a camera which
|
|
534
|
+
honours our stated first choice most of the time and disregards it some of
|
|
535
|
+
the time -- which is a reason to expect *less* of reordering than of the
|
|
536
|
+
pin, and is recorded here so nobody reads this knob as established.
|
|
537
|
+
|
|
538
|
+
Why it is worth having anyway: the efficient profile (hevc 2560x1440 at
|
|
539
|
+
~1.1 Mbps against h264 1280x720 at 2.5-4.0 Mbps) has only ever appeared when
|
|
540
|
+
BOTH codecs are on the wire. ``AIDOT_SDES_VIDEO_PT=97`` narrows to H265 and
|
|
541
|
+
returns no video at all, 3 of 3 rounds -- narrowing removes the option
|
|
542
|
+
rather than selecting it. Reordering is the only untried lever that leaves
|
|
543
|
+
both codecs offered, so the camera can still fall back to H264.
|
|
544
|
+
|
|
545
|
+
Accepts a comma- or space-separated payload-type list (``97,96``, ``97``).
|
|
546
|
+
Whatever is named goes first, in the order named; every advertised codec not
|
|
547
|
+
named is appended in the default order. So this can express a preference
|
|
548
|
+
and can never narrow the offer: the result is always a permutation of the
|
|
549
|
+
full advertised set, and an empty or entirely unusable value yields exactly
|
|
550
|
+
today's order. Narrowing already has its own variable, and the one time it
|
|
551
|
+
was measured it cost the picture.
|
|
552
|
+
|
|
553
|
+
Unknown payload types and duplicates are dropped rather than honoured: a
|
|
554
|
+
payload type this offer does not advertise has no rtpmap to go with it, and
|
|
555
|
+
listing one on the m-line would name a codec the camera was never given the
|
|
556
|
+
parameters for.
|
|
557
|
+
"""
|
|
558
|
+
raw = os.environ.get("AIDOT_SDES_VIDEO_PT_ORDER", "") or ""
|
|
559
|
+
named: list = []
|
|
560
|
+
for tok in raw.replace(",", " ").split():
|
|
561
|
+
if not tok.isdigit():
|
|
562
|
+
continue
|
|
563
|
+
pt = int(tok)
|
|
564
|
+
if pt in _SDES_OFFER_VIDEO_CODECS and pt not in named:
|
|
565
|
+
named.append(pt)
|
|
566
|
+
return tuple(named) + tuple(
|
|
567
|
+
pt for pt in _SDES_OFFER_VIDEO_PT_ORDER if pt not in named
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _sdes_offer_video_codec_lines(order=None) -> tuple:
|
|
572
|
+
"""Build the offer's video codec list and its rtpmap/fmtp block.
|
|
573
|
+
|
|
574
|
+
Returns ``(pt_list, attrs)`` where ``pt_list`` is the payload-type list for
|
|
575
|
+
the ``m=video`` line ("96 97") and ``attrs`` is the rtpmap/fmtp lines for
|
|
576
|
+
those payload types, in the same order. Both have to move together: an
|
|
577
|
+
m-line naming a payload type whose rtpmap was left behind is an offer the
|
|
578
|
+
camera cannot act on.
|
|
579
|
+
|
|
580
|
+
``order`` of None is today's shipped order, so the default output is
|
|
581
|
+
byte-identical to the literal this replaced. Anything not advertised is
|
|
582
|
+
dropped, and an order that ends up empty falls back to the default -- an
|
|
583
|
+
``m=video`` line with no payload type at all leaves the camera nothing to
|
|
584
|
+
send, which is the one outcome worse than an unpinned choice.
|
|
585
|
+
"""
|
|
586
|
+
pts = tuple(
|
|
587
|
+
pt for pt in (order if order is not None else _SDES_OFFER_VIDEO_PT_ORDER)
|
|
588
|
+
if pt in _SDES_OFFER_VIDEO_CODECS
|
|
589
|
+
)
|
|
590
|
+
if not pts:
|
|
591
|
+
pts = _SDES_OFFER_VIDEO_PT_ORDER
|
|
592
|
+
return (
|
|
593
|
+
" ".join(str(pt) for pt in pts),
|
|
594
|
+
"".join(_SDES_OFFER_VIDEO_CODECS[pt] for pt in pts),
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
497
598
|
def narrow_sdp_payload_types(sdp_text: str, keep_video=None, keep_audio=None) -> str:
|
|
498
599
|
"""Rewrite an SDP to advertise a single payload type per media line.
|
|
499
600
|
|
|
@@ -767,6 +868,108 @@ def _record_peer_reflexive(known, discovered, observed, is_self=None):
|
|
|
767
868
|
return [*discovered, observed]
|
|
768
869
|
|
|
769
870
|
|
|
871
|
+
#: How many distinct inbound probe sources the stall report will name. Beyond
|
|
872
|
+
#: this the count alone is kept - a stall is explained by the first few sources,
|
|
873
|
+
#: and an unbounded list would turn one WARNING into a wall of text.
|
|
874
|
+
_MAX_PROBE_SOURCES = 6
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _probe_source_verdict(src, turn_peer_ip, turn_peer_port, *,
|
|
878
|
+
cam_peer, observed, known, learned):
|
|
879
|
+
"""Say why one inbound STUN probe's source was, or was not, learned.
|
|
880
|
+
|
|
881
|
+
Nomination can only aim at an address, and when the camera's answer lists
|
|
882
|
+
one this host cannot route to, the only usable addresses are the ones its
|
|
883
|
+
own probes arrive from. Two vetoes drop a relay-carried probe before it can
|
|
884
|
+
become a peer-reflexive candidate, and both are silent in the code they
|
|
885
|
+
guard, so a session that stalls for one reason is indistinguishable from a
|
|
886
|
+
session that stalled for the other. This names them apart.
|
|
887
|
+
|
|
888
|
+
Takes the values the bridge already computed rather than re-deriving them:
|
|
889
|
+
``cam_peer`` is ``_br_cam_peer`` (None once ``_is_self_peer_ip`` has refused
|
|
890
|
+
the XOR-PEER-ADDRESS) and ``observed`` is ``_br_obs`` (None once the
|
|
891
|
+
``_bsrc`` fallback has refused the TURN server's own address too). A copy
|
|
892
|
+
of the decision here would rot the first time either site changed.
|
|
893
|
+
|
|
894
|
+
``known`` is "this address is already in the nomination set" and ``learned``
|
|
895
|
+
is "this call grew the peer-reflexive set"; without both, an address that
|
|
896
|
+
``_record_peer_reflexive`` refused on its own policy would be reported as
|
|
897
|
+
one we are already nominating, which is the opposite of the truth.
|
|
898
|
+
|
|
899
|
+
Pure, so the classification is testable without a camera.
|
|
900
|
+
"""
|
|
901
|
+
if observed:
|
|
902
|
+
if learned:
|
|
903
|
+
return "learned"
|
|
904
|
+
if known:
|
|
905
|
+
return "known"
|
|
906
|
+
# _record_peer_reflexive took it and gave nothing back: its own self-IP
|
|
907
|
+
# check, or the peer-reflexive cap.
|
|
908
|
+
return "prflx-refused"
|
|
909
|
+
if turn_peer_ip and not turn_peer_port:
|
|
910
|
+
return "vetoed-no-peer-port"
|
|
911
|
+
if turn_peer_ip and not cam_peer:
|
|
912
|
+
# _is_self_peer_ip matched the XOR-PEER-ADDRESS.
|
|
913
|
+
return "vetoed-self-ip"
|
|
914
|
+
# No usable peer address at all, and the packet came from the relay, so the
|
|
915
|
+
# _bsrc fallback had nothing to offer but the TURN server itself.
|
|
916
|
+
return "vetoed-turn-source"
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _first_media_stall_report(device_id, waited_s, nominated,
|
|
920
|
+
use_candidate_sent, binding_success,
|
|
921
|
+
trigger_sent, probes, probes_dropped=0):
|
|
922
|
+
"""Build the one line a first-media stall emits.
|
|
923
|
+
|
|
924
|
+
A session that never delivers a byte looks, in a log, exactly like one that
|
|
925
|
+
delivered late: the wait expires, the serve launches with unknown payload
|
|
926
|
+
types, and nothing states the reason. The reason is knowable at that
|
|
927
|
+
moment. Media only ever follows the AVIO LIVING trigger, that trigger is
|
|
928
|
+
armed only by an inbound STUN Binding Success Response, and that response
|
|
929
|
+
only comes back if something we nominated was reachable - so the nominated
|
|
930
|
+
set, the Binding Success count, the trigger flag and the verdict on every
|
|
931
|
+
inbound probe source are between them the whole explanation.
|
|
932
|
+
|
|
933
|
+
Addresses and counts only. This line reaches ``home-assistant.log`` and
|
|
934
|
+
users paste that file into public issue reports.
|
|
935
|
+
|
|
936
|
+
``probes`` is a sequence of ``(source_label, verdict)`` from
|
|
937
|
+
:func:`_probe_source_verdict`. Pure, so the wording is testable.
|
|
938
|
+
"""
|
|
939
|
+
_cands = ", ".join(f"{_ip}:{_port}" for _ip, _port in (nominated or []))
|
|
940
|
+
_probes = "; ".join(
|
|
941
|
+
f"{_where} -> {_verdict}" for _where, _verdict in (probes or [])
|
|
942
|
+
)
|
|
943
|
+
if probes_dropped:
|
|
944
|
+
_probes += f"; (+{probes_dropped} more source(s))"
|
|
945
|
+
_why = ""
|
|
946
|
+
if not binding_success:
|
|
947
|
+
_why = (
|
|
948
|
+
" No inbound STUN Binding Success arrived, so the AVIO LIVING"
|
|
949
|
+
" trigger was never armed and the camera never started sending."
|
|
950
|
+
)
|
|
951
|
+
elif not trigger_sent:
|
|
952
|
+
_why = (
|
|
953
|
+
" A Binding Success arrived and the trigger still did not go -"
|
|
954
|
+
" the trigger is not gated on anything else."
|
|
955
|
+
)
|
|
956
|
+
return (
|
|
957
|
+
"camera %s: SDES first media never arrived (%.0fs)."
|
|
958
|
+
" nominated=%s; use-candidate=%s; binding-success=%d; trigger=%s;"
|
|
959
|
+
" probes=%s.%s"
|
|
960
|
+
% (
|
|
961
|
+
device_id,
|
|
962
|
+
waited_s,
|
|
963
|
+
_cands or "none",
|
|
964
|
+
"sent" if use_candidate_sent else "not-sent",
|
|
965
|
+
binding_success,
|
|
966
|
+
"sent" if trigger_sent else "not-sent",
|
|
967
|
+
_probes or "none",
|
|
968
|
+
_why,
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
|
|
770
973
|
def _ensure_srtp_rx_session(holder, key, build, *, on_built=None, on_error=None):
|
|
771
974
|
"""Return the bridge's SRTP receive session, rebuilt when ``key`` changes.
|
|
772
975
|
|
|
@@ -1380,6 +1583,13 @@ class _SdesOpenMixin:
|
|
|
1380
1583
|
"spk_eligible_ts": None, # bridge: first time SPEAKERSTART is eligible
|
|
1381
1584
|
"stop": False,
|
|
1382
1585
|
} if _talk_offer else None
|
|
1586
|
+
# Video codec preference, expressed by m-line order (RFC 3264 5.1).
|
|
1587
|
+
# Default is today's 96 97 and the bytes are identical to the literal
|
|
1588
|
+
# this replaced; AIDOT_SDES_VIDEO_PT_ORDER reorders it without ever
|
|
1589
|
+
# narrowing it.
|
|
1590
|
+
_video_pt_order = _resolve_sdes_video_pt_order()
|
|
1591
|
+
_video_pt_list, _video_codec_attrs = _sdes_offer_video_codec_lines(
|
|
1592
|
+
_video_pt_order)
|
|
1383
1593
|
sdes_offer_sdp = (
|
|
1384
1594
|
"v=0\r\n"
|
|
1385
1595
|
f"o=- {ts} {ts} IN IP4 {local_ip}\r\n"
|
|
@@ -1418,16 +1628,13 @@ class _SdesOpenMixin:
|
|
|
1418
1628
|
if _audio_sock in _relay_addrs else ""
|
|
1419
1629
|
)
|
|
1420
1630
|
# video m-section
|
|
1421
|
-
+ f"m=video {_offer_video_port} RTP/SAVPF
|
|
1631
|
+
+ f"m=video {_offer_video_port} RTP/SAVPF {_video_pt_list}\r\n"
|
|
1422
1632
|
f"c=IN IP4 {_offer_video_ip}\r\n"
|
|
1423
1633
|
"a=recvonly\r\n"
|
|
1424
1634
|
"a=mid:1\r\n"
|
|
1425
1635
|
f"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:{srtp_key_video}\r\n"
|
|
1426
|
-
|
|
1427
|
-
"a=
|
|
1428
|
-
"a=rtpmap:97 H265/90000\r\n"
|
|
1429
|
-
"a=fmtp:97 level-id=93\r\n"
|
|
1430
|
-
"a=rtcp-mux\r\n"
|
|
1636
|
+
+ _video_codec_attrs
|
|
1637
|
+
+ "a=rtcp-mux\r\n"
|
|
1431
1638
|
f"a=ice-ufrag:{_ufrag_v}\r\n"
|
|
1432
1639
|
f"a=ice-pwd:{_pwd_v}\r\n"
|
|
1433
1640
|
f"a=candidate:1 1 udp 2130706431 {local_ip} {video_port} typ host\r\n"
|
|
@@ -1460,8 +1667,21 @@ class _SdesOpenMixin:
|
|
|
1460
1667
|
)
|
|
1461
1668
|
)
|
|
1462
1669
|
|
|
1463
|
-
#
|
|
1464
|
-
#
|
|
1670
|
+
# Receipt for the codec order above, emitted only when it differs from
|
|
1671
|
+
# the shipped one. A run that cannot show the knob was applied cannot
|
|
1672
|
+
# tell a result from a coincidence: an earlier attempt on this question
|
|
1673
|
+
# read as a confirmed effect for two sessions before the missing receipt
|
|
1674
|
+
# showed the pin had never reached the SDP at all. Ordering happens
|
|
1675
|
+
# before the pin below, so with both set the pin wins and the order is
|
|
1676
|
+
# moot - which is why both lines print rather than one.
|
|
1677
|
+
if _video_pt_order != _SDES_OFFER_VIDEO_PT_ORDER and _status:
|
|
1678
|
+
_status(f"SDES: offer video codec order={_video_pt_list}")
|
|
1679
|
+
|
|
1680
|
+
# Opt-in: NARROW the OFFER to one video codec rather than advertising
|
|
1681
|
+
# both 96/97 and letting the camera decide in its answer. Distinct from
|
|
1682
|
+
# the ordering above, which only states which of the two we would
|
|
1683
|
+
# rather have and always leaves both on the wire; this one takes the
|
|
1684
|
+
# other away, which for 97 took the video with it.
|
|
1465
1685
|
#
|
|
1466
1686
|
# The offer is the SDP that matters here. Traced live 2026-08-07 with
|
|
1467
1687
|
# every status line printed: this path sends webrtcReq carrying OUR
|
|
@@ -2864,6 +3084,17 @@ class _SdesOpenMixin:
|
|
|
2864
3084
|
if not _is_self_peer_ip(cam_addr[0]):
|
|
2865
3085
|
_turn_send_indication(sock, cam_addr[0], cam_addr[1], _req)
|
|
2866
3086
|
|
|
3087
|
+
# Every candidate this session actually sent USE-CANDIDATE to, in the
|
|
3088
|
+
# order it was nominated. Instrumentation only, and it exists because
|
|
3089
|
+
# _bridge_uc_info cannot answer the question on the shape that stalls:
|
|
3090
|
+
# on a late answer - measured at +1.3 s, missing the pre-launch snapshot
|
|
3091
|
+
# - it is _nominate_from_answer_sdp below that nominates, and it neither
|
|
3092
|
+
# appends to _bridge_uc_info["cands"] nor flips ["sent"]. Reporting the
|
|
3093
|
+
# stall from that dict alone would print "nominated=none" on an open
|
|
3094
|
+
# that did nominate, and make "the answer carried no ICE credentials"
|
|
3095
|
+
# indistinguishable from "the answer carried one unroutable candidate".
|
|
3096
|
+
_nominated_seen: list = []
|
|
3097
|
+
|
|
2867
3098
|
def _nominate_from_answer_sdp(sdp_text) -> int:
|
|
2868
3099
|
"""Nominate every candidate in ``sdp_text``; return how many.
|
|
2869
3100
|
|
|
@@ -2903,6 +3134,8 @@ class _SdesOpenMixin:
|
|
|
2903
3134
|
_audio_sock, _ufrag_a, _pwd_a, _u, _p, (_c_ip, _c_port))
|
|
2904
3135
|
_send_use_candidate(
|
|
2905
3136
|
_video_sock, _ufrag_v, _pwd_v, _u, _p, (_c_ip, _c_port))
|
|
3137
|
+
if (_c_ip, _c_port) not in _nominated_seen:
|
|
3138
|
+
_nominated_seen.append((_c_ip, _c_port))
|
|
2906
3139
|
return len(_cands)
|
|
2907
3140
|
|
|
2908
3141
|
# Open the TURN relay's door for the camera BEFORE probing it.
|
|
@@ -3293,6 +3526,22 @@ class _SdesOpenMixin:
|
|
|
3293
3526
|
_s.sendto(_data, _addr)
|
|
3294
3527
|
_br_stun_resp_count = 0
|
|
3295
3528
|
_tutk_trigger_sent = False
|
|
3529
|
+
# Instrumentation for the first-media stall report. These four are
|
|
3530
|
+
# bridge-thread locals that the main coroutine has to be able to
|
|
3531
|
+
# read when the wait expires, so each is mirrored onto _bridge_fn at
|
|
3532
|
+
# its write site - the same publication _sprop_done uses. Nothing
|
|
3533
|
+
# here is read by the bridge itself; they only ever describe.
|
|
3534
|
+
_br_binding_success_count = 0 # inbound STUN Binding Success (0x0101)
|
|
3535
|
+
_br_probe_verdicts: dict = {} # probe source -> why it was/wasn't learned
|
|
3536
|
+
# A SET, not a counter: one unrecorded source probing fifty times
|
|
3537
|
+
# is one source, and a per-packet count would put a wrong number in
|
|
3538
|
+
# the report for someone to reason from.
|
|
3539
|
+
_br_probe_overflow: set = set() # sources past _MAX_PROBE_SOURCES
|
|
3540
|
+
_bridge_fn._tutk_trigger_sent = False
|
|
3541
|
+
_bridge_fn._br_stun_resp_count = 0
|
|
3542
|
+
_bridge_fn._br_binding_success_count = 0
|
|
3543
|
+
_bridge_fn._br_probe_verdicts = {}
|
|
3544
|
+
_bridge_fn._br_probe_overflow = 0
|
|
3296
3545
|
_last_trigger_ts = 0.0 # time of last AVIO LIVING send
|
|
3297
3546
|
_trigger_bs = None # socket used for trigger
|
|
3298
3547
|
_trigger_bsrc = None # camera addr for trigger
|
|
@@ -3799,6 +4048,49 @@ class _SdesOpenMixin:
|
|
|
3799
4048
|
f" (not in the {len(_bridge_uc_info['cands'])}"
|
|
3800
4049
|
f" advertised candidate(s)) - will nominate it"
|
|
3801
4050
|
)
|
|
4051
|
+
# Record why this probe's source was, or was not,
|
|
4052
|
+
# usable. Both refusals above are silent, and which
|
|
4053
|
+
# of them fired decides what a fix would change - so
|
|
4054
|
+
# the fact is kept here and reported once, on the
|
|
4055
|
+
# stall path, rather than logged per packet.
|
|
4056
|
+
try:
|
|
4057
|
+
_br_pv_where = f"{_bsrc[0]}:{_bsrc[1]}"
|
|
4058
|
+
if _br_turn_peer_ip:
|
|
4059
|
+
_br_pv_where += (
|
|
4060
|
+
f" via {_br_turn_peer_ip}"
|
|
4061
|
+
f":{_br_turn_peer_port}"
|
|
4062
|
+
)
|
|
4063
|
+
if _br_pv_where in _br_probe_verdicts:
|
|
4064
|
+
pass
|
|
4065
|
+
elif len(_br_probe_verdicts) >= _MAX_PROBE_SOURCES:
|
|
4066
|
+
_br_probe_overflow.add(_br_pv_where)
|
|
4067
|
+
_bridge_fn._br_probe_overflow = len(
|
|
4068
|
+
_br_probe_overflow)
|
|
4069
|
+
else:
|
|
4070
|
+
_br_probe_verdicts[_br_pv_where] = (
|
|
4071
|
+
_probe_source_verdict(
|
|
4072
|
+
_bsrc,
|
|
4073
|
+
_br_turn_peer_ip,
|
|
4074
|
+
_br_turn_peer_port,
|
|
4075
|
+
cam_peer=_br_cam_peer,
|
|
4076
|
+
observed=_br_obs,
|
|
4077
|
+
known=bool(
|
|
4078
|
+
_br_obs is not None
|
|
4079
|
+
and (_br_obs in _br_prflx_was
|
|
4080
|
+
or _br_obs in
|
|
4081
|
+
_bridge_uc_info["cands"])
|
|
4082
|
+
),
|
|
4083
|
+
learned=(
|
|
4084
|
+
_br_prflx_now is not _br_prflx_was
|
|
4085
|
+
),
|
|
4086
|
+
)
|
|
4087
|
+
)
|
|
4088
|
+
# Rebind, never mutate: the reader is the
|
|
4089
|
+
# main coroutine on another thread.
|
|
4090
|
+
_bridge_fn._br_probe_verdicts = dict(
|
|
4091
|
+
_br_probe_verdicts)
|
|
4092
|
+
except Exception:
|
|
4093
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_probe_source_verdict', exc_info=True)
|
|
3802
4094
|
try:
|
|
3803
4095
|
if _br_turn_peer_ip is None and _bsrc[0] != _hp_host:
|
|
3804
4096
|
_br_prefer_direct_stun[_bs] = True
|
|
@@ -3864,6 +4156,8 @@ class _SdesOpenMixin:
|
|
|
3864
4156
|
else:
|
|
3865
4157
|
_bs.sendto(_bresp, _bsrc)
|
|
3866
4158
|
_br_stun_resp_count += 1
|
|
4159
|
+
_bridge_fn._br_stun_resp_count = (
|
|
4160
|
+
_br_stun_resp_count)
|
|
3867
4161
|
# Late USE-CANDIDATE: send when answer arrived
|
|
3868
4162
|
# after bridge started (empty at setup time).
|
|
3869
4163
|
if not _bridge_uc_info["sent"] and _bridge_uc_info["ufrag"]:
|
|
@@ -3957,6 +4251,16 @@ class _SdesOpenMixin:
|
|
|
3957
4251
|
f" relay path unusable"
|
|
3958
4252
|
)
|
|
3959
4253
|
continue
|
|
4254
|
+
# Count the inbound Binding Success BEFORE the
|
|
4255
|
+
# trigger gate below. Counting inside it would make
|
|
4256
|
+
# this an alias for _tutk_trigger_sent and collapse
|
|
4257
|
+
# two different diagnoses - "none ever arrived" and
|
|
4258
|
+
# "one arrived and the trigger still did not go" -
|
|
4259
|
+
# into a single indistinguishable state.
|
|
4260
|
+
if _bpkt[:2] == b'\x01\x01':
|
|
4261
|
+
_br_binding_success_count += 1
|
|
4262
|
+
_bridge_fn._br_binding_success_count = (
|
|
4263
|
+
_br_binding_success_count)
|
|
3960
4264
|
# STUN BindingSuccess (0x0101) from camera: ICE complete.
|
|
3961
4265
|
# Send AES-128-CBC encrypted SESSION_MODE_REQ (AVIO LIVING).
|
|
3962
4266
|
#
|
|
@@ -3970,6 +4274,7 @@ class _SdesOpenMixin:
|
|
|
3970
4274
|
if (_use_plain_rtp and not _tutk_trigger_sent
|
|
3971
4275
|
and _bpkt[:2] == b'\x01\x01'):
|
|
3972
4276
|
_tutk_trigger_sent = True
|
|
4277
|
+
_bridge_fn._tutk_trigger_sent = True
|
|
3973
4278
|
import struct as _st_tk
|
|
3974
4279
|
import random as _rand_tk
|
|
3975
4280
|
_ts_ms = int(_time_br.time() * 1000)
|
|
@@ -4700,9 +5005,23 @@ class _SdesOpenMixin:
|
|
|
4700
5005
|
if 7 in _psc and 8 in _psc:
|
|
4701
5006
|
_sprop_new = _build_sprop(_psc[7], _psc[8])
|
|
4702
5007
|
if _sprop_new != _load_sprop(self.device_id):
|
|
4703
|
-
|
|
4704
|
-
|
|
5008
|
+
# Gated on the result: two of
|
|
5009
|
+
# _save_sprop's three outcomes write
|
|
5010
|
+
# nothing, and this is the only line
|
|
5011
|
+
# the sprop path ever emits.
|
|
5012
|
+
if _save_sprop(self.device_id,
|
|
5013
|
+
_sprop_new):
|
|
5014
|
+
_status(
|
|
5015
|
+
"bridge: cached sprop-parameter"
|
|
4705
5016
|
f"-sets for {self.device_id}")
|
|
5017
|
+
else:
|
|
5018
|
+
_status(
|
|
5019
|
+
"bridge: parameter sets NOT "
|
|
5020
|
+
f"cached for {self.device_id} "
|
|
5021
|
+
"(camera marked unstable, or "
|
|
5022
|
+
"the cache is unwritable) - "
|
|
5023
|
+
"out-of-band injection stays "
|
|
5024
|
+
"off for it")
|
|
4706
5025
|
_bridge_fn._sprop_done = True
|
|
4707
5026
|
# Rebase RTP timestamps to start near 0. Camera picks a
|
|
4708
5027
|
# random starting timestamp (RFC 3550 section 5.1); the 90 kHz
|
|
@@ -5037,6 +5356,48 @@ class _SdesOpenMixin:
|
|
|
5037
5356
|
" camera's answer during the first-media wait" % _n_nom
|
|
5038
5357
|
)
|
|
5039
5358
|
await asyncio.sleep(0.1)
|
|
5359
|
+
if _first_video_pt[0] is None:
|
|
5360
|
+
# The wait expired with nothing received. Everything needed to say
|
|
5361
|
+
# why is in hand at exactly this moment and was, until now, thrown
|
|
5362
|
+
# away: media only ever follows the AVIO LIVING trigger, that
|
|
5363
|
+
# trigger is armed only by an inbound STUN Binding Success, and
|
|
5364
|
+
# that response only comes back if something we nominated was
|
|
5365
|
+
# reachable. When it was not, the only usable addresses are the
|
|
5366
|
+
# ones the camera's own probes arrived from - and two silent vetoes
|
|
5367
|
+
# can drop a relay-carried probe. One WARNING, on this path only:
|
|
5368
|
+
# an open that delivers media reaches none of this.
|
|
5369
|
+
try:
|
|
5370
|
+
_stall_probes = list(
|
|
5371
|
+
(getattr(_bridge_fn, "_br_probe_verdicts", None) or {}).items()
|
|
5372
|
+
)
|
|
5373
|
+
# Union of every nomination source, deduped and order-preserving:
|
|
5374
|
+
# the answer-peek path records into _nominated_seen, the setup /
|
|
5375
|
+
# trickle / late-answer paths into _bridge_uc_info, and the
|
|
5376
|
+
# bridge's peer-reflexive learning into ["prflx"]. Any one of
|
|
5377
|
+
# them alone under-reports on some shape.
|
|
5378
|
+
_stall_nominated: list = []
|
|
5379
|
+
for _sn in (*_nominated_seen, *_bridge_uc_info["cands"],
|
|
5380
|
+
*_bridge_uc_info["prflx"]):
|
|
5381
|
+
if _sn not in _stall_nominated:
|
|
5382
|
+
_stall_nominated.append(_sn)
|
|
5383
|
+
_LOGGER.warning("%s", _first_media_stall_report(
|
|
5384
|
+
device_id=getattr(self, "device_id", "?"),
|
|
5385
|
+
waited_s=_FIRST_MEDIA_WAIT_S,
|
|
5386
|
+
nominated=_stall_nominated,
|
|
5387
|
+
use_candidate_sent=bool(
|
|
5388
|
+
_bridge_uc_info["sent"] or _nominated_seen),
|
|
5389
|
+
binding_success=int(
|
|
5390
|
+
getattr(_bridge_fn, "_br_binding_success_count", 0)),
|
|
5391
|
+
trigger_sent=bool(
|
|
5392
|
+
getattr(_bridge_fn, "_tutk_trigger_sent", False)),
|
|
5393
|
+
probes=_stall_probes,
|
|
5394
|
+
probes_dropped=int(
|
|
5395
|
+
getattr(_bridge_fn, "_br_probe_overflow", 0)),
|
|
5396
|
+
))
|
|
5397
|
+
except Exception:
|
|
5398
|
+
_LOGGER.debug("camera %s: swallowed exception in %s",
|
|
5399
|
+
getattr(self, "device_id", "?"),
|
|
5400
|
+
'_first_media_stall_report', exc_info=True)
|
|
5040
5401
|
if _serve_audio and _first_audio_pt[0] is None:
|
|
5041
5402
|
_apt_deadline = time.monotonic() + _AUDIO_PT_GRACE_S
|
|
5042
5403
|
while _first_audio_pt[0] is None and time.monotonic() < _apt_deadline:
|
|
@@ -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 = "1.0.0b2"
|
|
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"
|