python-aidot-cameras 1.0.0b28__tar.gz → 1.0.0b30__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.0b28/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b30}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/__main__.py +10 -4
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/protocol.py +7 -7
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sdes_open.py +108 -78
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/cloud_auth.py +45 -8
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offer_bandwidth_line.py +5 -2
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_remb_goes_out_the_relay_aware_sender.py +18 -9
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_tmmbr_bridge.py +16 -7
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_self_peer_is_an_address_not_an_ip.py +1 -1
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_validator_shares_one_token.py +59 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/README.md +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/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: 1.0.
|
|
3
|
+
Version: 1.0.0b30
|
|
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
|
|
@@ -77,6 +77,7 @@ import aiohttp
|
|
|
77
77
|
|
|
78
78
|
from .client import AidotClient
|
|
79
79
|
from .cloud_auth import ( # noqa: F401 (re-exported for callers and tests)
|
|
80
|
+
CloudAuthUnavailable,
|
|
80
81
|
_install_token_cache,
|
|
81
82
|
_make_client,
|
|
82
83
|
_read_token_file,
|
|
@@ -86,8 +87,6 @@ from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID, CONF_NAME
|
|
|
86
87
|
|
|
87
88
|
_LOGGER = logging.getLogger("aidot.go2rtc")
|
|
88
89
|
|
|
89
|
-
DEFAULT_COUNTRY = os.environ.get("AIDOT_COUNTRY", "US")
|
|
90
|
-
|
|
91
90
|
|
|
92
91
|
def _env_bool(name: str) -> bool | None:
|
|
93
92
|
"""Tri-state env flag: '1' -> True, '0' -> False, unset -> None (library default)."""
|
|
@@ -131,7 +130,11 @@ def _go2rtc_source(dev_id: str, is_sdes: bool) -> str:
|
|
|
131
130
|
async def cmd_list() -> int:
|
|
132
131
|
"""Print device id + transport for every camera, plus paste-ready go2rtc YAML."""
|
|
133
132
|
async with aiohttp.ClientSession() as session:
|
|
134
|
-
|
|
133
|
+
# CloudAuthUnavailable is the library-side signal; a CLI exits on it.
|
|
134
|
+
try:
|
|
135
|
+
client = await _make_client(session)
|
|
136
|
+
except CloudAuthUnavailable as exc:
|
|
137
|
+
sys.exit(str(exc))
|
|
135
138
|
cams = await _cameras(client)
|
|
136
139
|
if not cams:
|
|
137
140
|
print("No IPC cameras found on this account.")
|
|
@@ -158,7 +161,10 @@ async def cmd_list() -> int:
|
|
|
158
161
|
async def cmd_stream(dev_id: str, output_url: str) -> int:
|
|
159
162
|
"""Open one camera's keepalive and push/serve it to ``output_url``."""
|
|
160
163
|
async with aiohttp.ClientSession() as session:
|
|
161
|
-
|
|
164
|
+
try:
|
|
165
|
+
client = await _make_client(session)
|
|
166
|
+
except CloudAuthUnavailable as exc:
|
|
167
|
+
sys.exit(str(exc))
|
|
162
168
|
cams = await _cameras(client)
|
|
163
169
|
device = next((d for d in cams if d[CONF_ID] == dev_id), None)
|
|
164
170
|
if device is None:
|
{python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -2950,6 +2950,12 @@ def decode_remb_bitrate(pkt: bytes) -> int:
|
|
|
2950
2950
|
return mantissa << exp
|
|
2951
2951
|
|
|
2952
2952
|
|
|
2953
|
+
#: RTPFB feedback message types (RFC 4585 s6.1, RFC 5104 s4.2).
|
|
2954
|
+
_RTPFB_PT = 205
|
|
2955
|
+
_FMT_NACK = 1
|
|
2956
|
+
_FMT_TMMBR = 3
|
|
2957
|
+
|
|
2958
|
+
|
|
2953
2959
|
def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
|
|
2954
2960
|
"""An RTCP Generic NACK (RFC 4585 s6.2.1) asking for ``lost_seqs`` again.
|
|
2955
2961
|
|
|
@@ -2985,7 +2991,7 @@ def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
|
|
|
2985
2991
|
i += 1
|
|
2986
2992
|
fci.append(struct.pack("!HH", pid, blp))
|
|
2987
2993
|
|
|
2988
|
-
pkt = (struct.pack("!BBH", 0x80 |
|
|
2994
|
+
pkt = (struct.pack("!BBH", 0x80 | _FMT_NACK, _RTPFB_PT, 0)
|
|
2989
2995
|
+ struct.pack("!II", sender_ssrc & 0xFFFFFFFF,
|
|
2990
2996
|
media_ssrc & 0xFFFFFFFF)
|
|
2991
2997
|
+ b"".join(fci))
|
|
@@ -2993,12 +2999,6 @@ def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
|
|
|
2993
2999
|
return pkt[:2] + struct.pack("!H", len(pkt) // 4 - 1) + pkt[4:]
|
|
2994
3000
|
|
|
2995
3001
|
|
|
2996
|
-
#: RTPFB feedback message types (RFC 4585 s6.1, RFC 5104 s4.2).
|
|
2997
|
-
_RTPFB_PT = 205
|
|
2998
|
-
_FMT_NACK = 1
|
|
2999
|
-
_FMT_TMMBR = 3
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
3002
|
def build_tmmbr(sender_ssrc: int, media_ssrc: int, bitrate_bps: int,
|
|
3003
3003
|
overhead: int = 60) -> bytes:
|
|
3004
3004
|
"""A TMMBR (RFC 5104 s4.2.1) telling ``media_ssrc`` not to exceed a rate.
|
{python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -241,19 +241,28 @@ def _widen_media_rcvbuf(sock, kind: str, device_id: str = "?") -> int:
|
|
|
241
241
|
return got
|
|
242
242
|
|
|
243
243
|
|
|
244
|
-
def
|
|
245
|
-
"""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
def _env_positive(name: str, cast=int, off=None):
|
|
245
|
+
"""Parse ``name`` as a positive number; anything else means ``off``.
|
|
246
|
+
|
|
247
|
+
One body for the experiment knobs below - the third copy of this parser
|
|
248
|
+
was the rule-of-three moment. ``off`` is the knob's disabled value (None
|
|
249
|
+
for the ints, 0.0 for the delay), and unparseable input is disabled rather
|
|
250
|
+
than an error: these are read while sessions are being built, and a typo
|
|
251
|
+
must not take a camera off the air.
|
|
250
252
|
"""
|
|
251
|
-
raw = os.environ.get(
|
|
253
|
+
raw = os.environ.get(name)
|
|
254
|
+
if raw is None:
|
|
255
|
+
return off
|
|
252
256
|
try:
|
|
253
|
-
|
|
254
|
-
except
|
|
255
|
-
return
|
|
256
|
-
return
|
|
257
|
+
value = cast(raw.strip())
|
|
258
|
+
except ValueError:
|
|
259
|
+
return off
|
|
260
|
+
return value if value > 0 else off
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _sdes_offer_bandwidth_kbps():
|
|
264
|
+
"""A receive-bandwidth ceiling for the offer, in kbps, or None for none."""
|
|
265
|
+
return _env_positive("AIDOT_SDES_OFFER_BANDWIDTH_KBPS")
|
|
257
266
|
|
|
258
267
|
|
|
259
268
|
def _offer_bandwidth_line(kbps) -> str:
|
|
@@ -381,13 +390,8 @@ def _send_video_nack(send, srtcp_sess, sender_ssrc: int,
|
|
|
381
390
|
"""
|
|
382
391
|
if not lost_seqs:
|
|
383
392
|
return False
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
send(srtcp_sess.protect_rtcp(raw) if srtcp_sess is not None else raw)
|
|
387
|
-
return True
|
|
388
|
-
except Exception:
|
|
389
|
-
_LOGGER.debug("NACK send failed", exc_info=True)
|
|
390
|
-
return False
|
|
393
|
+
return _send_rtcp_fb(
|
|
394
|
+
send, srtcp_sess, build_nack(sender_ssrc, media_ssrc, lost_seqs))
|
|
391
395
|
|
|
392
396
|
|
|
393
397
|
def _sdes_tmmbr_bps():
|
|
@@ -396,11 +400,7 @@ def _sdes_tmmbr_bps():
|
|
|
396
400
|
Off unless ``AIDOT_SDES_TMMBR_BPS`` names a positive integer. Unparseable
|
|
397
401
|
is off rather than an error: this is read while a session is running.
|
|
398
402
|
"""
|
|
399
|
-
|
|
400
|
-
bps = int(str(os.environ.get("AIDOT_SDES_TMMBR_BPS")).strip())
|
|
401
|
-
except (TypeError, ValueError, AttributeError):
|
|
402
|
-
return None
|
|
403
|
-
return bps if bps > 0 else None
|
|
403
|
+
return _env_positive("AIDOT_SDES_TMMBR_BPS")
|
|
404
404
|
|
|
405
405
|
|
|
406
406
|
def _sdes_tmmbr_after_s() -> float:
|
|
@@ -413,11 +413,7 @@ def _sdes_tmmbr_after_s() -> float:
|
|
|
413
413
|
working SD control. Both windows of one session share the codec and the
|
|
414
414
|
scene, so neither can confound it.
|
|
415
415
|
"""
|
|
416
|
-
|
|
417
|
-
after = float(str(os.environ.get("AIDOT_SDES_TMMBR_AFTER_S")).strip())
|
|
418
|
-
except (TypeError, ValueError, AttributeError):
|
|
419
|
-
return 0.0
|
|
420
|
-
return after if after > 0 else 0.0
|
|
416
|
+
return _env_positive("AIDOT_SDES_TMMBR_AFTER_S", cast=float, off=0.0)
|
|
421
417
|
|
|
422
418
|
|
|
423
419
|
def _tmmbr_ready(first_video_ts, now: float, after_s: float) -> bool:
|
|
@@ -431,6 +427,38 @@ def _tmmbr_ready(first_video_ts, now: float, after_s: float) -> bool:
|
|
|
431
427
|
return (now - first_video_ts) >= after_s
|
|
432
428
|
|
|
433
429
|
|
|
430
|
+
def _send_rtcp_fb(send, srtcp_sess, raw: bytes) -> bool:
|
|
431
|
+
"""Protect-and-send tail shared by the NACK, TMMBR and REMB helpers.
|
|
432
|
+
|
|
433
|
+
One body for the ``protect_rtcp``-if-session-else-raw dance and the
|
|
434
|
+
swallow-and-debug-log error policy. It was written three times before
|
|
435
|
+
this, and the copy that drifted (REMB's, still inline in the loop body)
|
|
436
|
+
is the one where the raw-socket relay bug hid. Never raises: these run
|
|
437
|
+
inside the bridge's packet loop.
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
send(srtcp_sess.protect_rtcp(raw) if srtcp_sess is not None else raw)
|
|
441
|
+
return True
|
|
442
|
+
except Exception:
|
|
443
|
+
_LOGGER.debug("RTCP feedback send failed", exc_info=True)
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _send_video_remb(send, srtcp_sess, sender_ssrc: int,
|
|
448
|
+
media_ssrc: int, target_bps) -> bool:
|
|
449
|
+
"""Put one REMB on the camera's RTCP path. True if it went out.
|
|
450
|
+
|
|
451
|
+
Same shape and same relay-aware ``send`` contract as
|
|
452
|
+
:func:`_send_video_nack` - the REMB logic previously lived inline in the
|
|
453
|
+
loop body, and being the only feedback path without a helper is exactly
|
|
454
|
+
how it kept the raw-socket write long after the NACK path was fixed.
|
|
455
|
+
"""
|
|
456
|
+
if not target_bps or target_bps <= 0:
|
|
457
|
+
return False
|
|
458
|
+
return _send_rtcp_fb(
|
|
459
|
+
send, srtcp_sess, build_remb(sender_ssrc, [media_ssrc], target_bps))
|
|
460
|
+
|
|
461
|
+
|
|
434
462
|
def _send_video_tmmbr(send, srtcp_sess, sender_ssrc: int,
|
|
435
463
|
media_ssrc: int, bitrate_bps) -> bool:
|
|
436
464
|
"""Put one TMMBR on the camera's RTCP path. True if it went out.
|
|
@@ -446,13 +474,8 @@ def _send_video_tmmbr(send, srtcp_sess, sender_ssrc: int,
|
|
|
446
474
|
"""
|
|
447
475
|
if not bitrate_bps or bitrate_bps <= 0:
|
|
448
476
|
return False
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
send(srtcp_sess.protect_rtcp(raw) if srtcp_sess is not None else raw)
|
|
452
|
-
return True
|
|
453
|
-
except Exception:
|
|
454
|
-
_LOGGER.debug("TMMBR send failed", exc_info=True)
|
|
455
|
-
return False
|
|
477
|
+
return _send_rtcp_fb(
|
|
478
|
+
send, srtcp_sess, build_tmmbr(sender_ssrc, media_ssrc, bitrate_bps))
|
|
456
479
|
|
|
457
480
|
|
|
458
481
|
#: Sender SSRC on every RTCP we send the camera. Load-bearing, not cosmetic:
|
|
@@ -2008,6 +2031,11 @@ class _SdesOpenMixin:
|
|
|
2008
2031
|
# this replaced; AIDOT_SDES_VIDEO_PT_ORDER reorders it without ever
|
|
2009
2032
|
# narrowing it.
|
|
2010
2033
|
_video_pt_order = _resolve_sdes_video_pt_order()
|
|
2034
|
+
# Read ONCE and feed both the SDP line and the status receipt below:
|
|
2035
|
+
# two independent env reads let the receipt name a value that never
|
|
2036
|
+
# reached the wire, which is the false-receipt trap the receipt
|
|
2037
|
+
# exists to close.
|
|
2038
|
+
_bw_kbps = _sdes_offer_bandwidth_kbps()
|
|
2011
2039
|
_video_pt_list, _video_codec_attrs = _sdes_offer_video_codec_lines(
|
|
2012
2040
|
_video_pt_order)
|
|
2013
2041
|
sdes_offer_sdp = (
|
|
@@ -2050,7 +2078,7 @@ class _SdesOpenMixin:
|
|
|
2050
2078
|
# video m-section
|
|
2051
2079
|
+ f"m=video {_offer_video_port} RTP/SAVPF {_video_pt_list}\r\n"
|
|
2052
2080
|
f"c=IN IP4 {_offer_video_ip}\r\n"
|
|
2053
|
-
+ _offer_bandwidth_line(
|
|
2081
|
+
+ _offer_bandwidth_line(_bw_kbps)
|
|
2054
2082
|
+ "a=recvonly\r\n"
|
|
2055
2083
|
"a=mid:1\r\n"
|
|
2056
2084
|
f"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:{srtp_key_video}\r\n"
|
|
@@ -2103,7 +2131,6 @@ class _SdesOpenMixin:
|
|
|
2103
2131
|
# is not evidence the line reached the wire. Measured 2026-08-23: a
|
|
2104
2132
|
# b=AS arm scored identically to its control and the only available
|
|
2105
2133
|
# check for the knob was reading the env back out of the harness.
|
|
2106
|
-
_bw_kbps = _sdes_offer_bandwidth_kbps()
|
|
2107
2134
|
if _bw_kbps and _status:
|
|
2108
2135
|
_status(f"SDES: offer receive-bandwidth ceiling b=AS:{_bw_kbps}")
|
|
2109
2136
|
|
|
@@ -4199,33 +4226,29 @@ class _SdesOpenMixin:
|
|
|
4199
4226
|
# skipping a tick until the sender exists costs a second,
|
|
4200
4227
|
# falling back costs correctness on the very sessions this
|
|
4201
4228
|
# is for.
|
|
4202
|
-
|
|
4229
|
+
# Guard order is cost order: the module constant first,
|
|
4230
|
+
# so the default REMB-off config pays nothing per packet
|
|
4231
|
+
# (the sender lookup used to sit above this line and ran
|
|
4232
|
+
# ~300x/s for a feature that ships disabled).
|
|
4203
4233
|
if (REMB_TARGET_BPS > 0
|
|
4204
4234
|
and hasattr(_bridge_fn, '_cam_video_ssrc')
|
|
4205
|
-
and _remb_send
|
|
4235
|
+
and (_remb_send := getattr(
|
|
4236
|
+
_bridge_fn, '_send_to_cam', None)) is not None
|
|
4206
4237
|
and _time_br.time() - getattr(
|
|
4207
4238
|
_bridge_fn, '_last_remb_ts', 0.0) >= 1.0):
|
|
4208
4239
|
_bridge_fn._last_remb_ts = _time_br.time()
|
|
4209
|
-
|
|
4210
|
-
|
|
4240
|
+
if _send_video_remb(
|
|
4241
|
+
_remb_send,
|
|
4242
|
+
getattr(_bridge_fn, '_pli_tx_sess', None),
|
|
4211
4243
|
_CAM_RTCP_SENDER_SSRC,
|
|
4212
|
-
|
|
4213
|
-
REMB_TARGET_BPS
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
_bridge_fn.
|
|
4220
|
-
_status(
|
|
4221
|
-
f"SDES: sent REMB {REMB_TARGET_BPS // 1000} kbps"
|
|
4222
|
-
f" for video SSRC"
|
|
4223
|
-
f" 0x{_bridge_fn._cam_video_ssrc:08x}")
|
|
4224
|
-
except Exception:
|
|
4225
|
-
_LOGGER.debug(
|
|
4226
|
-
"camera %s: swallowed exception in %s",
|
|
4227
|
-
getattr(self, "device_id", "?"), '_remb',
|
|
4228
|
-
exc_info=True)
|
|
4244
|
+
_bridge_fn._cam_video_ssrc,
|
|
4245
|
+
REMB_TARGET_BPS,
|
|
4246
|
+
) and not getattr(_bridge_fn, '_remb_logged', False):
|
|
4247
|
+
_bridge_fn._remb_logged = True
|
|
4248
|
+
_status(
|
|
4249
|
+
f"SDES: sent REMB {REMB_TARGET_BPS // 1000} kbps"
|
|
4250
|
+
f" for video SSRC"
|
|
4251
|
+
f" 0x{_bridge_fn._cam_video_ssrc:08x}")
|
|
4229
4252
|
|
|
4230
4253
|
# TMMBR: a BOUND, where REMB above is an ESTIMATE. Two
|
|
4231
4254
|
# different RFC 5104 / 4585 messages, and this firmware can
|
|
@@ -4236,17 +4259,23 @@ class _SdesOpenMixin:
|
|
|
4236
4259
|
# every bitrate control tried on this camera so far has
|
|
4237
4260
|
# been acked and ignored, and an unmeasured one must not
|
|
4238
4261
|
# reach the four cameras that stream fine today.
|
|
4262
|
+
# Same cost ordering: the per-session bps attr is the
|
|
4263
|
+
# one cheap read the off-default pays; the cadence gate
|
|
4264
|
+
# runs before _tmmbr_ready so the readiness math and the
|
|
4265
|
+
# sender lookup happen at most once a second, not per
|
|
4266
|
+
# packet, and one time.time() serves both checks.
|
|
4239
4267
|
_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)
|
|
4240
|
-
|
|
4241
|
-
if (_tmmbr_bps and _tmmbr_send is not None
|
|
4268
|
+
if (_tmmbr_bps
|
|
4242
4269
|
and hasattr(_bridge_fn, '_cam_video_ssrc')
|
|
4270
|
+
and (_tmmbr_now := _time_br.time()) - getattr(
|
|
4271
|
+
_bridge_fn, '_last_tmmbr_ts', 0.0) >= 1.0
|
|
4243
4272
|
and _tmmbr_ready(
|
|
4244
4273
|
getattr(_bridge_fn, '_first_video_ts', None),
|
|
4245
|
-
|
|
4274
|
+
_tmmbr_now,
|
|
4246
4275
|
getattr(_bridge_fn, '_tmmbr_after_s', 0.0))
|
|
4247
|
-
and
|
|
4248
|
-
_bridge_fn, '
|
|
4249
|
-
_bridge_fn._last_tmmbr_ts =
|
|
4276
|
+
and (_tmmbr_send := getattr(
|
|
4277
|
+
_bridge_fn, '_send_to_cam', None)) is not None):
|
|
4278
|
+
_bridge_fn._last_tmmbr_ts = _tmmbr_now
|
|
4250
4279
|
if _send_video_tmmbr(
|
|
4251
4280
|
_tmmbr_send,
|
|
4252
4281
|
getattr(_bridge_fn, '_pli_tx_sess', None),
|
|
@@ -5477,23 +5506,24 @@ class _SdesOpenMixin:
|
|
|
5477
5506
|
'!I', _bpkt, 8)[0]
|
|
5478
5507
|
_bridge_fn._cam_srtp_src = _bsrc
|
|
5479
5508
|
_bridge_fn._cam_srtp_sock = _bs
|
|
5480
|
-
# Publish the relay-aware sender so the RTCP
|
|
5481
|
-
# cadence (which runs outside this loop and has
|
|
5482
|
-
# no socket/peer in scope) can reach the camera
|
|
5483
|
-
# the same way the NACK does. Rebuilt only when
|
|
5484
|
-
# the address changes: this runs ~300x/s per
|
|
5485
|
-
# camera, which is why _nack_on is resolved once
|
|
5486
|
-
# per session rather than read per packet.
|
|
5487
5509
|
if getattr(_bridge_fn, '_first_video_ts',
|
|
5488
5510
|
None) is None:
|
|
5489
5511
|
_bridge_fn._first_video_ts = _time_br.time()
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5512
|
+
# Publish the relay-aware sender so the RTCP
|
|
5513
|
+
# cadence blocks (which have no socket/peer in
|
|
5514
|
+
# scope) can reach the camera the way the NACK
|
|
5515
|
+
# does. This branch runs ONCE per session (it
|
|
5516
|
+
# sits inside the first-video gate), so the
|
|
5517
|
+
# sender is frozen at the first video packet's
|
|
5518
|
+
# source - the same adoption `_cam_srtp_src`
|
|
5519
|
+
# has always used. A 1 Hz cadence send to a
|
|
5520
|
+
# stale address after a mid-session address
|
|
5521
|
+
# change just drops; the NACK path keeps its
|
|
5522
|
+
# own per-event binding for exactly that case.
|
|
5523
|
+
_bridge_fn._send_to_cam = (
|
|
5524
|
+
lambda _d, _s=_bs, _a=_bsrc,
|
|
5525
|
+
_p=_br_cam_peer: _br_send_to_cam(
|
|
5526
|
+
_s, _d, _a, _p))
|
|
5497
5527
|
# Schedule immediate PLI so IDR+SPS arrives in
|
|
5498
5528
|
# the analyzeduration window.
|
|
5499
5529
|
_bridge_fn._last_pli_ts = 0.0
|
|
@@ -18,7 +18,6 @@ import asyncio
|
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
|
-
import sys
|
|
22
21
|
import tempfile
|
|
23
22
|
|
|
24
23
|
import aiohttp
|
|
@@ -27,7 +26,25 @@ from .client import AidotClient
|
|
|
27
26
|
|
|
28
27
|
_LOGGER = logging.getLogger("aidot.cloud_auth")
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
|
|
30
|
+
def _default_country() -> str:
|
|
31
|
+
"""The AIDOT_COUNTRY fallback, read at CALL time.
|
|
32
|
+
|
|
33
|
+
This used to be a module-level constant bound at import, which made a
|
|
34
|
+
caller's later os.environ tweak silently ineffective: a creds file with a
|
|
35
|
+
non-US country logged in as US on the password path. Reading it when the
|
|
36
|
+
client is built keeps environment and parameters equivalent.
|
|
37
|
+
"""
|
|
38
|
+
return os.environ.get("AIDOT_COUNTRY", "US")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CloudAuthUnavailable(RuntimeError):
|
|
42
|
+
"""No usable credentials: no readable token file and no username/password.
|
|
43
|
+
|
|
44
|
+
Raised instead of ``sys.exit`` so library-side callers (the release
|
|
45
|
+
validator among them) keep their own error reporting; the go2rtc CLI
|
|
46
|
+
translates it to an exit in ``__main__``.
|
|
47
|
+
"""
|
|
31
48
|
|
|
32
49
|
|
|
33
50
|
def _read_token_file(path: str) -> dict:
|
|
@@ -85,19 +102,39 @@ def _install_token_cache(client: AidotClient, path: str) -> None:
|
|
|
85
102
|
client.set_token_fresh_cb(_cb)
|
|
86
103
|
|
|
87
104
|
|
|
88
|
-
async def _make_client(
|
|
89
|
-
|
|
105
|
+
async def _make_client(
|
|
106
|
+
session: aiohttp.ClientSession,
|
|
107
|
+
*,
|
|
108
|
+
username: "str | None" = None,
|
|
109
|
+
password: "str | None" = None,
|
|
110
|
+
country: "str | None" = None,
|
|
111
|
+
) -> AidotClient:
|
|
112
|
+
"""Authenticate to the AiDot cloud, from parameters or the environment.
|
|
90
113
|
|
|
91
114
|
Prefers a stored token (AIDOT_TOKEN_FILE) - the same login_info dict the HA
|
|
92
115
|
integration persists - which carries access/refresh tokens so no password
|
|
93
116
|
round-trip is needed. Falls back to username/password login otherwise. In
|
|
94
117
|
both cases, if AIDOT_TOKEN_FILE is set we register a write-back cache so
|
|
95
118
|
rotations persist.
|
|
119
|
+
|
|
120
|
+
Credentials a caller already holds are passed explicitly; each parameter
|
|
121
|
+
left as None falls back to its environment variable. The parameters exist
|
|
122
|
+
so a caller never has to publish secrets into ``os.environ`` (where every
|
|
123
|
+
spawned child inherits them) just to reach this function - the shim that
|
|
124
|
+
did exactly that also carried a dead country assignment, because the old
|
|
125
|
+
module-level default was bound at import time.
|
|
126
|
+
|
|
127
|
+
Raises :class:`CloudAuthUnavailable` when neither a readable token file
|
|
128
|
+
nor a username/password pair is available.
|
|
96
129
|
"""
|
|
97
130
|
loop = asyncio.get_running_loop()
|
|
98
131
|
token_file = os.environ.get("AIDOT_TOKEN_FILE")
|
|
99
|
-
username
|
|
100
|
-
|
|
132
|
+
if username is None:
|
|
133
|
+
username = os.environ.get("AIDOT_USERNAME")
|
|
134
|
+
if password is None:
|
|
135
|
+
password = os.environ.get("AIDOT_PASSWORD")
|
|
136
|
+
if country is None:
|
|
137
|
+
country = _default_country()
|
|
101
138
|
|
|
102
139
|
if token_file and os.path.exists(token_file):
|
|
103
140
|
try:
|
|
@@ -113,14 +150,14 @@ async def _make_client(session: aiohttp.ClientSession) -> AidotClient:
|
|
|
113
150
|
return client
|
|
114
151
|
|
|
115
152
|
if not username or not password:
|
|
116
|
-
|
|
153
|
+
raise CloudAuthUnavailable(
|
|
117
154
|
"Set AIDOT_TOKEN_FILE, or AIDOT_USERNAME and AIDOT_PASSWORD, in the "
|
|
118
155
|
"environment."
|
|
119
156
|
)
|
|
120
157
|
|
|
121
158
|
client = AidotClient(
|
|
122
159
|
session=session,
|
|
123
|
-
country_code=
|
|
160
|
+
country_code=country,
|
|
124
161
|
username=username,
|
|
125
162
|
password=password,
|
|
126
163
|
)
|
|
@@ -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.0b30"
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b30
|
|
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.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offer_bandwidth_line.py
RENAMED
|
@@ -132,8 +132,11 @@ def test_applying_a_ceiling_leaves_a_receipt_in_the_log():
|
|
|
132
132
|
|
|
133
133
|
src = (pathlib.Path(__file__).resolve().parents[1] / "aidot_cameras"
|
|
134
134
|
/ "camera" / "sdes_open.py").read_text()
|
|
135
|
-
m = re.search(r"_bw_kbps
|
|
136
|
-
r"\s*if _bw_kbps and _status:\s*\n"
|
|
135
|
+
m = re.search(r"if _bw_kbps and _status:\s*\n"
|
|
137
136
|
r'\s*_status\(f"[^"]*b=AS:\{_bw_kbps\}"\)', src)
|
|
138
137
|
assert m, ("applying a receive-bandwidth ceiling must emit a status "
|
|
139
138
|
"receipt naming the value that reached the offer")
|
|
139
|
+
assert src.count("_sdes_offer_bandwidth_kbps()") == 2, (
|
|
140
|
+
"the knob must be read ONCE per offer (its definition plus one call "
|
|
141
|
+
"site); a second read lets the receipt disagree with the SDP actually "
|
|
142
|
+
"sent, which is the false-receipt trap the receipt exists to close")
|
|
@@ -50,12 +50,17 @@ _SRC = (pathlib.Path(__file__).resolve().parents[1] / "aidot_cameras"
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def _remb_block() -> str:
|
|
53
|
-
"""The REMB
|
|
53
|
+
"""The REMB cadence block: its guard through to the TMMBR block's start.
|
|
54
|
+
|
|
55
|
+
Sliced anchor-to-structural-terminator rather than anchor+N characters -
|
|
56
|
+
a fixed-width window goes quietly vacuous when comments grow, and a
|
|
57
|
+
vacuous window makes the negative assertions below pass without guarding
|
|
58
|
+
anything.
|
|
59
|
+
"""
|
|
54
60
|
src = _SRC.read_text()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return src[start:start + 1400]
|
|
61
|
+
start = src.index("if (REMB_TARGET_BPS > 0")
|
|
62
|
+
end = src.index("_tmmbr_bps = getattr(", start)
|
|
63
|
+
return src[start:end]
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
def test_remb_does_not_write_to_the_raw_socket():
|
|
@@ -77,15 +82,19 @@ def test_remb_does_not_fall_back_to_the_socket_when_the_sender_is_missing():
|
|
|
77
82
|
second; falling back to `sendto` costs correctness on every relayed camera.
|
|
78
83
|
"""
|
|
79
84
|
block = _remb_block()
|
|
80
|
-
assert re.search(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"the guard must
|
|
85
|
+
assert re.search(
|
|
86
|
+
r"_remb_send\s*:=\s*getattr\(\s*\n?\s*_bridge_fn,\s*'_send_to_cam',"
|
|
87
|
+
r"\s*None\s*\)+\s*\)?\s*is not None", block), (
|
|
88
|
+
"the guard must resolve the relay-aware sender with a None default "
|
|
89
|
+
"and require it, not fall back to a socket")
|
|
84
90
|
|
|
85
91
|
|
|
86
92
|
def test_remb_uses_the_keyed_sender_ssrc_constant():
|
|
87
93
|
block = _remb_block()
|
|
88
94
|
assert "_CAM_RTCP_SENDER_SSRC" in block
|
|
95
|
+
assert "_send_video_remb(" in block, (
|
|
96
|
+
"REMB must go through its helper like NACK and TMMBR do; the inline "
|
|
97
|
+
"copy is where the raw-socket bug hid")
|
|
89
98
|
assert "0xAB12CD34" not in block, (
|
|
90
99
|
"the SRTP TX policy is keyed on _CAM_RTCP_SENDER_SSRC; a literal here "
|
|
91
100
|
"drifts and the camera drops the packet")
|
{python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_tmmbr_bridge.py
RENAMED
|
@@ -95,9 +95,12 @@ def test_the_call_site_uses_the_relay_aware_sender_and_the_keyed_ssrc():
|
|
|
95
95
|
if src.startswith("_send_video_tmmbr(", i)
|
|
96
96
|
and not src[:i].endswith("def ")]
|
|
97
97
|
assert calls, "the bridge must actually send the TMMBR it can build"
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
|
|
98
|
+
# Window from the cadence block's start to the PLI block that follows,
|
|
99
|
+
# so it covers how the sender is resolved as well as how it is used -
|
|
100
|
+
# structural terminators, not a fixed character count that goes vacuous
|
|
101
|
+
# as comments grow.
|
|
102
|
+
start = src.index("_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)")
|
|
103
|
+
args = src[start:src.index("_pli_done", start)]
|
|
101
104
|
assert "_cam_srtp_sock.sendto" not in args, (
|
|
102
105
|
"a raw socket write is silently inert on a TURN-relayed session -- "
|
|
103
106
|
"use the bridge's relay-aware sender, as the NACK path does")
|
|
@@ -164,8 +167,8 @@ def test_the_bridge_gates_the_send_on_it():
|
|
|
164
167
|
|
|
165
168
|
src = (pathlib.Path(__file__).resolve().parents[1] / "aidot_cameras"
|
|
166
169
|
/ "camera" / "sdes_open.py").read_text()
|
|
167
|
-
i = src.index("
|
|
168
|
-
window = src[i:i
|
|
170
|
+
i = src.index("_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)")
|
|
171
|
+
window = src[i:src.index("_pli_done", i)]
|
|
169
172
|
assert "_tmmbr_ready(" in window, (
|
|
170
173
|
"the TMMBR cadence must consult _tmmbr_ready, or AIDOT_SDES_TMMBR_AFTER_S "
|
|
171
174
|
"does nothing and the measurement is between-session again")
|
|
@@ -197,8 +200,14 @@ def test_the_switches_are_resolved_once_per_session_not_per_packet():
|
|
|
197
200
|
assert "_sdes_tmmbr_after_s()" in setup
|
|
198
201
|
|
|
199
202
|
# And the cadence reads the resolved value rather than the environment.
|
|
200
|
-
i = src.index("
|
|
201
|
-
cadence = src[i
|
|
203
|
+
i = src.index("_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)")
|
|
204
|
+
cadence = src[i:src.index("_pli_done", i)]
|
|
202
205
|
assert "_sdes_tmmbr_bps()" not in cadence, (
|
|
203
206
|
"the cadence must not call os.environ; it runs on the packet loop")
|
|
204
207
|
assert "_sdes_tmmbr_after_s()" not in cadence
|
|
208
|
+
# Cost ordering inside the guard: the once-a-second cadence gate must sit
|
|
209
|
+
# BEFORE the readiness math and the sender lookup, or both run per packet.
|
|
210
|
+
assert cadence.index("_last_tmmbr_ts") < cadence.index("_tmmbr_ready("), (
|
|
211
|
+
"the 1s cadence gate must run before _tmmbr_ready")
|
|
212
|
+
assert cadence.index("_tmmbr_ready(") < cadence.index("_send_to_cam"), (
|
|
213
|
+
"the sender lookup must be the last, rarest check")
|