python-aidot-cameras 1.0.0b27__tar.gz → 1.0.0b28__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.0b27/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b28}/PKG-INFO +26 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/README.md +25 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/__main__.py +8 -113
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_upstream.py +2 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/controls.py +24 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/protocol.py +49 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/sdes_open.py +187 -8
- python_aidot_cameras-1.0.0b28/aidot_cameras/cloud_auth.py +139 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28/python_aidot_cameras.egg-info}/PKG-INFO +26 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/python_aidot_cameras.egg-info/SOURCES.txt +6 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_battery_evidence_corroboration.py +2 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_canary_log_names_the_camera.py +2 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_cloud_count.py +1 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_cloud_plan.py +1 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_direct_ts_serve.py +1 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_go2rtc_cli.py +3 -3
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_ice_peer_reflexive.py +4 -4
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_live_validate_policy.py +6 -6
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_no_self_referential_go2rtc.py +4 -4
- python_aidot_cameras-1.0.0b28/tests/test_offer_bandwidth_line.py +139 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_recent_recordings.py +2 -2
- python_aidot_cameras-1.0.0b28/tests/test_remb_goes_out_the_relay_aware_sender.py +98 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_ffmpeg_stderr_log.py +2 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_first_media_stall_report.py +15 -15
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_sprop.py +1 -1
- python_aidot_cameras-1.0.0b28/tests/test_sdes_tmmbr_bridge.py +204 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_self_peer_is_an_address_not_an_ip.py +2 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_serve_pt_respects_the_pin.py +1 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +1 -1
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stall_report_separates_silence_from_undecryptable.py +2 -2
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stall_report_survives_a_caller_giving_up.py +1 -1
- python_aidot_cameras-1.0.0b28/tests/test_tmmbr.py +116 -0
- python_aidot_cameras-1.0.0b28/tests/test_validator_shares_one_token.py +123 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_wake_gate_camera_evidence.py +4 -4
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/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.0b28
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -119,6 +119,27 @@ mid-session on the same transport, and it does not change the rate; the app's
|
|
|
119
119
|
does. Streaming is correct either way and the cost is bandwidth, which matters
|
|
120
120
|
on a metered or congested link.
|
|
121
121
|
|
|
122
|
+
**That 2:1 is reachable by another route: the codec.** Ten sessions on this
|
|
123
|
+
camera on 2026-08-23 separate perfectly by the video payload type the session
|
|
124
|
+
negotiated - H264 (pt=96) at 1597-1685 Kbps, H265 (pt=97) at 766-774 Kbps, no
|
|
125
|
+
overlap. An H.265 session costs about half an H.264 one, which is the same
|
|
126
|
+
ratio the app's SD tap achieves, and unlike a control byte the camera ignores,
|
|
127
|
+
the codec is chosen in our own offer.
|
|
128
|
+
|
|
129
|
+
Two things stand between that and a saving you can switch on:
|
|
130
|
+
|
|
131
|
+
- **We cannot select it.** Narrowing the offer to 97 does not select it, it
|
|
132
|
+
removes the option - no video at all in 3 of 3 rounds (see
|
|
133
|
+
`AIDOT_SDES_VIDEO_PT` below). Reordering the preference list without
|
|
134
|
+
narrowing it (`AIDOT_SDES_VIDEO_PT_ORDER=97,96`) was then tested directly:
|
|
135
|
+
eight sessions in blocks of two returned H265 in 2 of 4 with the reorder and
|
|
136
|
+
2 of 4 without, with receipts confirming the reordered list reached the SDP.
|
|
137
|
+
The camera chooses, roughly a coin flip, and neither knob moves it.
|
|
138
|
+
- **H.265 may not suit the consumer.** Browser HEVC support over Media Source
|
|
139
|
+
Extensions is far narrower than H.264, and MSE playback is the reason the
|
|
140
|
+
bitrate matters here at all. Halving the bitrate is no help if the client
|
|
141
|
+
that was failing cannot decode the result.
|
|
142
|
+
|
|
122
143
|
## Local (LAN) control and account ownership
|
|
123
144
|
|
|
124
145
|
Local control over the devices' TCP:10000 channel - both the light/plug protocol
|
|
@@ -398,7 +419,10 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
398
419
|
| `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) |
|
|
399
420
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
400
421
|
| `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) |
|
|
401
|
-
| `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`) |
|
|
422
|
+
| `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. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
423
|
+
| `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
|
|
424
|
+
| `AIDOT_SDES_TMMBR_BPS` | Ask the camera not to exceed a bitrate, in bits per second, via RTCP TMMBR (RFC 5104). Distinct from `AIDOT_REMB_TARGET_BPS`: REMB reports an *estimate* of available bandwidth, TMMBR states a *bound*, and firmware can honour one without the other. This camera acts on RTCP feedback it never negotiated (our offer advertises no `a=rtcp-fb` at all, yet NACK recovers 98.4% of losses), so the absence of `ccm tmmbr` from its answer is not a reason to assume it is ignored. **Measured on an A001064 and it does nothing** - within-session, 0.748 against a control of 0.705, i.e. marginally higher. Shipped off. | unset (off) |
|
|
425
|
+
| `AIDOT_SDES_TMMBR_AFTER_S` | Seconds of media to let pass before the first TMMBR, so a bound can be measured *within* one session - window A before it, window B after - instead of between sessions. Measured from the first video packet, not from the open, so a slow-waking camera does not spend window A already capped. | `0` (send from the first video packet) |
|
|
402
426
|
| `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) |
|
|
403
427
|
| `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) |
|
|
404
428
|
| `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) |
|
|
@@ -79,6 +79,27 @@ mid-session on the same transport, and it does not change the rate; the app's
|
|
|
79
79
|
does. Streaming is correct either way and the cost is bandwidth, which matters
|
|
80
80
|
on a metered or congested link.
|
|
81
81
|
|
|
82
|
+
**That 2:1 is reachable by another route: the codec.** Ten sessions on this
|
|
83
|
+
camera on 2026-08-23 separate perfectly by the video payload type the session
|
|
84
|
+
negotiated - H264 (pt=96) at 1597-1685 Kbps, H265 (pt=97) at 766-774 Kbps, no
|
|
85
|
+
overlap. An H.265 session costs about half an H.264 one, which is the same
|
|
86
|
+
ratio the app's SD tap achieves, and unlike a control byte the camera ignores,
|
|
87
|
+
the codec is chosen in our own offer.
|
|
88
|
+
|
|
89
|
+
Two things stand between that and a saving you can switch on:
|
|
90
|
+
|
|
91
|
+
- **We cannot select it.** Narrowing the offer to 97 does not select it, it
|
|
92
|
+
removes the option - no video at all in 3 of 3 rounds (see
|
|
93
|
+
`AIDOT_SDES_VIDEO_PT` below). Reordering the preference list without
|
|
94
|
+
narrowing it (`AIDOT_SDES_VIDEO_PT_ORDER=97,96`) was then tested directly:
|
|
95
|
+
eight sessions in blocks of two returned H265 in 2 of 4 with the reorder and
|
|
96
|
+
2 of 4 without, with receipts confirming the reordered list reached the SDP.
|
|
97
|
+
The camera chooses, roughly a coin flip, and neither knob moves it.
|
|
98
|
+
- **H.265 may not suit the consumer.** Browser HEVC support over Media Source
|
|
99
|
+
Extensions is far narrower than H.264, and MSE playback is the reason the
|
|
100
|
+
bitrate matters here at all. Halving the bitrate is no help if the client
|
|
101
|
+
that was failing cannot decode the result.
|
|
102
|
+
|
|
82
103
|
## Local (LAN) control and account ownership
|
|
83
104
|
|
|
84
105
|
Local control over the devices' TCP:10000 channel - both the light/plug protocol
|
|
@@ -358,7 +379,10 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
358
379
|
| `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) |
|
|
359
380
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
360
381
|
| `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) |
|
|
361
|
-
| `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`) |
|
|
382
|
+
| `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. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
383
|
+
| `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
|
|
384
|
+
| `AIDOT_SDES_TMMBR_BPS` | Ask the camera not to exceed a bitrate, in bits per second, via RTCP TMMBR (RFC 5104). Distinct from `AIDOT_REMB_TARGET_BPS`: REMB reports an *estimate* of available bandwidth, TMMBR states a *bound*, and firmware can honour one without the other. This camera acts on RTCP feedback it never negotiated (our offer advertises no `a=rtcp-fb` at all, yet NACK recovers 98.4% of losses), so the absence of `ccm tmmbr` from its answer is not a reason to assume it is ignored. **Measured on an A001064 and it does nothing** - within-session, 0.748 against a control of 0.705, i.e. marginally higher. Shipped off. | unset (off) |
|
|
385
|
+
| `AIDOT_SDES_TMMBR_AFTER_S` | Seconds of media to let pass before the first TMMBR, so a bound can be measured *within* one session - window A before it, window B after - instead of between sessions. Measured from the first video packet, not from the open, so a slow-waking camera does not spend window A already capped. | `0` (send from the first video packet) |
|
|
362
386
|
| `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) |
|
|
363
387
|
| `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) |
|
|
364
388
|
| `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) |
|
|
@@ -46,14 +46,14 @@ Optional stream knobs:
|
|
|
46
46
|
go2rtc.yaml example (SDES camera - native RTSP push, video + audio):
|
|
47
47
|
|
|
48
48
|
streams:
|
|
49
|
-
frontdoor: exec:aidot-go2rtc
|
|
49
|
+
frontdoor: exec:aidot-go2rtc 0a1b2c3d... {output}
|
|
50
50
|
|
|
51
51
|
A DTLS camera (A000088) has no RTSP-push path, but a stdout producer works the
|
|
52
52
|
same lazy way. Pass ``-`` as the output and this process writes MPEG-TS (video
|
|
53
53
|
plus AAC audio) to stdout, which go2rtc's exec: source reads directly:
|
|
54
54
|
|
|
55
55
|
streams:
|
|
56
|
-
|
|
56
|
+
garage: exec:aidot-go2rtc 0a1b2c3d... -
|
|
57
57
|
|
|
58
58
|
go2rtc then owns the lifecycle - spawn on the first viewer, kill when idle -
|
|
59
59
|
exactly as it does with ``{output}`` for an SDES camera. An
|
|
@@ -66,18 +66,22 @@ from __future__ import annotations
|
|
|
66
66
|
|
|
67
67
|
import argparse
|
|
68
68
|
import asyncio
|
|
69
|
-
import json
|
|
70
69
|
import logging
|
|
71
70
|
import os
|
|
72
71
|
import re
|
|
73
72
|
import signal
|
|
74
73
|
import sys
|
|
75
|
-
import tempfile
|
|
76
74
|
from typing import Optional
|
|
77
75
|
|
|
78
76
|
import aiohttp
|
|
79
77
|
|
|
80
78
|
from .client import AidotClient
|
|
79
|
+
from .cloud_auth import ( # noqa: F401 (re-exported for callers and tests)
|
|
80
|
+
_install_token_cache,
|
|
81
|
+
_make_client,
|
|
82
|
+
_read_token_file,
|
|
83
|
+
_write_token_file,
|
|
84
|
+
)
|
|
81
85
|
from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID, CONF_NAME
|
|
82
86
|
|
|
83
87
|
_LOGGER = logging.getLogger("aidot.go2rtc")
|
|
@@ -93,115 +97,6 @@ def _env_bool(name: str) -> bool | None:
|
|
|
93
97
|
return raw == "1"
|
|
94
98
|
|
|
95
99
|
|
|
96
|
-
def _read_token_file(path: str) -> dict:
|
|
97
|
-
"""Blocking read of a stored login_info dict (run via executor from async)."""
|
|
98
|
-
with open(path, encoding="utf-8") as fh:
|
|
99
|
-
return json.load(fh)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _write_token_file(path: str, data: dict) -> None:
|
|
103
|
-
"""Atomic, 0600-secure write of login_info to ``path``.
|
|
104
|
-
|
|
105
|
-
Serialize into a temp file in the same directory (mkstemp creates it 0600,
|
|
106
|
-
closing the world-readable window os.chmod-after-write leaves) and only then
|
|
107
|
-
os.replace it over ``path`` - so a serialization failure never truncates the
|
|
108
|
-
previously-valid token. On any error the temp file is removed and re-raised.
|
|
109
|
-
"""
|
|
110
|
-
directory = os.path.dirname(path) or "."
|
|
111
|
-
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".token-", suffix=".tmp")
|
|
112
|
-
try:
|
|
113
|
-
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
114
|
-
json.dump(data, fh)
|
|
115
|
-
os.replace(tmp, path)
|
|
116
|
-
except BaseException:
|
|
117
|
-
try:
|
|
118
|
-
os.unlink(tmp)
|
|
119
|
-
except OSError:
|
|
120
|
-
pass
|
|
121
|
-
raise
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _install_token_cache(client: AidotClient, path: str) -> None:
|
|
125
|
-
"""Persist rotated tokens to ``path`` so refreshes survive across restarts.
|
|
126
|
-
|
|
127
|
-
The library calls this no-arg callback after every successful token refresh
|
|
128
|
-
(client.py), having already updated ``client.login_info``. Mirrors HA's
|
|
129
|
-
coordinator.token_fresh_cb, but writes to our own file instead of an HA
|
|
130
|
-
config entry - so a standalone run never loses auth to a rotation and never
|
|
131
|
-
fights HA over a shared refresh token.
|
|
132
|
-
"""
|
|
133
|
-
def _cb() -> None:
|
|
134
|
-
try:
|
|
135
|
-
_write_token_file(path, client.serializable_login_info())
|
|
136
|
-
_LOGGER.debug("Cached refreshed token to %s", path)
|
|
137
|
-
except (OSError, TypeError) as exc:
|
|
138
|
-
# TypeError: a defensive belt-and-suspenders catch alongside
|
|
139
|
-
# serializable_login_info() itself - if a future runtime-only key
|
|
140
|
-
# is ever added to login_info without also being added to
|
|
141
|
-
# RUNTIME_ONLY_LOGIN_INFO_KEYS, this keeps a caching bug from
|
|
142
|
-
# ever propagating out of a callback and interrupting a token
|
|
143
|
-
# refresh that had otherwise already succeeded (confirmed live:
|
|
144
|
-
# this exact TypeError - "Object of type Lock is not JSON
|
|
145
|
-
# serializable" - previously escaped this callback uncaught).
|
|
146
|
-
_LOGGER.warning("Could not cache refreshed token to %s: %s", path, exc)
|
|
147
|
-
|
|
148
|
-
client.set_token_fresh_cb(_cb)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
async def _make_client(session: aiohttp.ClientSession) -> AidotClient:
|
|
152
|
-
"""Authenticate to the AiDot cloud from the environment.
|
|
153
|
-
|
|
154
|
-
Prefers a stored token (AIDOT_TOKEN_FILE) - the same login_info dict the HA
|
|
155
|
-
integration persists - which carries access/refresh tokens so no password
|
|
156
|
-
round-trip is needed. Falls back to username/password login otherwise. In
|
|
157
|
-
both cases, if AIDOT_TOKEN_FILE is set we register a write-back cache so
|
|
158
|
-
rotations persist.
|
|
159
|
-
"""
|
|
160
|
-
loop = asyncio.get_running_loop()
|
|
161
|
-
token_file = os.environ.get("AIDOT_TOKEN_FILE")
|
|
162
|
-
username = os.environ.get("AIDOT_USERNAME")
|
|
163
|
-
password = os.environ.get("AIDOT_PASSWORD")
|
|
164
|
-
|
|
165
|
-
if token_file and os.path.exists(token_file):
|
|
166
|
-
try:
|
|
167
|
-
token = await loop.run_in_executor(None, _read_token_file, token_file)
|
|
168
|
-
except (OSError, ValueError) as exc:
|
|
169
|
-
# ValueError covers json.JSONDecodeError (empty/partial/corrupt
|
|
170
|
-
# cache). Fall through to the username/password path rather than
|
|
171
|
-
# crash CLI startup.
|
|
172
|
-
_LOGGER.warning("ignoring unreadable token cache %s: %s", token_file, exc)
|
|
173
|
-
else:
|
|
174
|
-
client = AidotClient(session=session, token=token)
|
|
175
|
-
_install_token_cache(client, token_file)
|
|
176
|
-
return client
|
|
177
|
-
|
|
178
|
-
if not username or not password:
|
|
179
|
-
sys.exit(
|
|
180
|
-
"Set AIDOT_TOKEN_FILE, or AIDOT_USERNAME and AIDOT_PASSWORD, in the "
|
|
181
|
-
"environment."
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
client = AidotClient(
|
|
185
|
-
session=session,
|
|
186
|
-
country_code=DEFAULT_COUNTRY,
|
|
187
|
-
username=username,
|
|
188
|
-
password=password,
|
|
189
|
-
)
|
|
190
|
-
await client.async_post_login()
|
|
191
|
-
# If a token file path was given (but didn't exist yet), seed it now and
|
|
192
|
-
# register the write-back cache so a dedicated-login run persists its
|
|
193
|
-
# rotations across restarts.
|
|
194
|
-
if token_file:
|
|
195
|
-
_install_token_cache(client, token_file)
|
|
196
|
-
try:
|
|
197
|
-
await loop.run_in_executor(
|
|
198
|
-
None, _write_token_file, token_file, client.serializable_login_info()
|
|
199
|
-
)
|
|
200
|
-
except (OSError, TypeError) as exc:
|
|
201
|
-
_LOGGER.warning("Could not seed token cache %s: %s", token_file, exc)
|
|
202
|
-
return client
|
|
203
|
-
|
|
204
|
-
|
|
205
100
|
async def _cameras(client: AidotClient) -> list[dict]:
|
|
206
101
|
"""Return the raw device dicts for every camera on the account."""
|
|
207
102
|
data = await client.async_get_all_device()
|
|
@@ -93,8 +93,8 @@ def rsa_encrypt(message: str, public_key: Any = None) -> str:
|
|
|
93
93
|
with ``aidot.client.rsa_password_encrypt(message)``, which bakes the key in
|
|
94
94
|
and takes one argument. Neither signature is available on both shapes, and
|
|
95
95
|
``aidot_cameras.crypto.rsa_encrypt`` is public surface the integration repo
|
|
96
|
-
may import
|
|
97
|
-
is kept and satisfied locally. ``public_key`` defaults to upstream's
|
|
96
|
+
may import, and public surface is not changed to track an upstream rename,
|
|
97
|
+
so the two-argument form is kept and satisfied locally. ``public_key`` defaults to upstream's
|
|
98
98
|
``PUBLIC_KEY_PEM``, which is the only key any caller ever passed.
|
|
99
99
|
"""
|
|
100
100
|
from cryptography.hazmat.backends import default_backend
|
{python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/controls.py
RENAMED
|
@@ -196,8 +196,32 @@ class _CameraControlsMixin:
|
|
|
196
196
|
had been applied. Nothing else re-sent it, so "it will apply on the next
|
|
197
197
|
session" was not true of any code path.
|
|
198
198
|
|
|
199
|
+
**On the A001064 this is acked and inert - it does not change the
|
|
200
|
+
rate, on either codec.** Measured 2026-08-23 with the framing gap
|
|
201
|
+
closed (1.0.0b27, so the AVIO header including the live-play dSeq is
|
|
202
|
+
byte-identical to the app's), in-session with a control arm that waits
|
|
203
|
+
the same gap and sends nothing: sd 0.885 against control 0.863, where
|
|
204
|
+
a working SD is about 2:1. Re-measured the same night on the H265
|
|
205
|
+
encode specifically - the codec the vendor app's working toggle rides -
|
|
206
|
+
because every earlier measurement had landed on H264 sessions: hd on
|
|
207
|
+
H265 scored 0.703 against controls at 0.743-0.776. Inert there too. The camera answers `801 payload=0000000000000000` every time. The
|
|
208
|
+
vendor app's own SD tap DOES halve the rate on the same camera, so this
|
|
209
|
+
is not a firmware that cannot do SD; we cannot trigger it. See
|
|
210
|
+
docs/ROAD-TO-1.0.md item 2 for the full record, including the five
|
|
211
|
+
other bitrate controls that turned out to be dead and the codec
|
|
212
|
+
difference that is worth about the same 2:1.
|
|
213
|
+
|
|
214
|
+
The call is kept because it is correct, it is what the app sends, and
|
|
215
|
+
other models are not covered by that measurement. Callers should not
|
|
216
|
+
rely on it changing the bitrate.
|
|
217
|
+
|
|
199
218
|
Source: f0.java g3() -> X2(800, SetStreamCtrlReq.parseContent(0, quality)).
|
|
200
219
|
Payload <IB3x> = channel(0) + quality byte + 3 reserved.
|
|
220
|
+
The app has three settings, not two: Auto, SD and HD, which
|
|
221
|
+
`NewLivePresenter.setDefaultResolution` maps from the persistent
|
|
222
|
+
`props.StreamType` property to quality 16, 5 and 1. This library
|
|
223
|
+
implements only hd=1 and sd=5; 16 (Auto) was swept and did nothing, and
|
|
224
|
+
`StreamType` itself is acked-and-ignored on every write.
|
|
201
225
|
"""
|
|
202
226
|
q = _STREAM_QUALITY.get(quality.lower())
|
|
203
227
|
if q is None:
|
{python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -2993,6 +2993,55 @@ def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
|
|
|
2993
2993
|
return pkt[:2] + struct.pack("!H", len(pkt) // 4 - 1) + pkt[4:]
|
|
2994
2994
|
|
|
2995
2995
|
|
|
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
|
+
def build_tmmbr(sender_ssrc: int, media_ssrc: int, bitrate_bps: int,
|
|
3003
|
+
overhead: int = 60) -> bytes:
|
|
3004
|
+
"""A TMMBR (RFC 5104 s4.2.1) telling ``media_ssrc`` not to exceed a rate.
|
|
3005
|
+
|
|
3006
|
+
Distinct from REMB, which this camera was measured to ignore: REMB reports
|
|
3007
|
+
an *estimate* of what the path will bear, TMMBR states a *bound* the sender
|
|
3008
|
+
must respect. They are different messages and a firmware can honour one
|
|
3009
|
+
without the other.
|
|
3010
|
+
|
|
3011
|
+
The bound is encoded as a 17-bit mantissa scaled by a 6-bit exponent, so
|
|
3012
|
+
not every rate is representable. We round the mantissa DOWN, never up: a
|
|
3013
|
+
TMMBR that permits more than the caller asked for is worse than sending
|
|
3014
|
+
none at all on a link that is already saturating.
|
|
3015
|
+
|
|
3016
|
+
``overhead`` is Measured Overhead, per-packet transport overhead in bytes
|
|
3017
|
+
(9 bits). 60 is the usual IP+UDP+RTP+SRTP figure and is what browsers send.
|
|
3018
|
+
"""
|
|
3019
|
+
if bitrate_bps <= 0:
|
|
3020
|
+
raise ValueError(
|
|
3021
|
+
f"a TMMBR must name a positive bitrate, got {bitrate_bps}; a bound "
|
|
3022
|
+
f"of zero asks the camera to stop sending")
|
|
3023
|
+
exp = 0
|
|
3024
|
+
mantissa = int(bitrate_bps)
|
|
3025
|
+
while mantissa >= (1 << 17):
|
|
3026
|
+
mantissa >>= 1
|
|
3027
|
+
exp += 1
|
|
3028
|
+
fci = struct.pack("!II",
|
|
3029
|
+
media_ssrc & 0xFFFFFFFF,
|
|
3030
|
+
(exp << 26) | (mantissa << 9) | (max(0, overhead) & 0x1FF))
|
|
3031
|
+
# The header's media-source field is unused for TMMBR and SHALL be zero
|
|
3032
|
+
# (RFC 5104 s4.2.1.2); the limited SSRC travels in the FCI above.
|
|
3033
|
+
pkt = (struct.pack("!BBH", 0x80 | _FMT_TMMBR, _RTPFB_PT, 0)
|
|
3034
|
+
+ struct.pack("!II", sender_ssrc & 0xFFFFFFFF, 0)
|
|
3035
|
+
+ fci)
|
|
3036
|
+
return pkt[:2] + struct.pack("!H", len(pkt) // 4 - 1) + pkt[4:]
|
|
3037
|
+
|
|
3038
|
+
|
|
3039
|
+
def decode_tmmbr_bitrate(pkt: bytes) -> int:
|
|
3040
|
+
"""The bound carried by a TMMBR built above, in bits per second."""
|
|
3041
|
+
word = struct.unpack("!I", pkt[16:20])[0]
|
|
3042
|
+
return ((word >> 9) & 0x1FFFF) << (word >> 26)
|
|
3043
|
+
|
|
3044
|
+
|
|
2996
3045
|
def decode_nack_seqs(pkt: bytes) -> "list":
|
|
2997
3046
|
"""The sequence numbers a Generic NACK asks for. For tests and diagnostics."""
|
|
2998
3047
|
out = []
|
{python_aidot_cameras-1.0.0b27 → python_aidot_cameras-1.0.0b28}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -30,6 +30,7 @@ from .protocol import (
|
|
|
30
30
|
NackTracker,
|
|
31
31
|
build_nack,
|
|
32
32
|
build_remb,
|
|
33
|
+
build_tmmbr,
|
|
33
34
|
_build_sprop,
|
|
34
35
|
parse_avio_response,
|
|
35
36
|
_build_stun_binding_success_response,
|
|
@@ -240,6 +241,42 @@ def _widen_media_rcvbuf(sock, kind: str, device_id: str = "?") -> int:
|
|
|
240
241
|
return got
|
|
241
242
|
|
|
242
243
|
|
|
244
|
+
def _sdes_offer_bandwidth_kbps():
|
|
245
|
+
"""A receive-bandwidth ceiling for the offer, in kbps, or None for none.
|
|
246
|
+
|
|
247
|
+
Off unless ``AIDOT_SDES_OFFER_BANDWIDTH_KBPS`` names a positive integer.
|
|
248
|
+
An unparseable value is off rather than an error: this is read while an
|
|
249
|
+
offer is being built, and a typo must not take a camera off the air.
|
|
250
|
+
"""
|
|
251
|
+
raw = os.environ.get("AIDOT_SDES_OFFER_BANDWIDTH_KBPS")
|
|
252
|
+
try:
|
|
253
|
+
kbps = int(str(raw).strip())
|
|
254
|
+
except (TypeError, ValueError, AttributeError):
|
|
255
|
+
return None
|
|
256
|
+
return kbps if kbps > 0 else None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _offer_bandwidth_line(kbps) -> str:
|
|
260
|
+
"""``b=AS:<kbps>`` (RFC 4566 s5.8) for the offer, or "" for no ceiling.
|
|
261
|
+
|
|
262
|
+
A receiver telling a sender how much it is willing to accept. This is the
|
|
263
|
+
last standards-defined bitrate control left after the others were killed
|
|
264
|
+
on evidence, and the only one we emit ourselves rather than mirror from
|
|
265
|
+
the app -- the app sends no ``b=`` line at all.
|
|
266
|
+
|
|
267
|
+
Anything that is not a positive integer count of kilobits yields no line.
|
|
268
|
+
``b=AS:0`` is NOT "unlimited" in RFC 4566; it asks for zero bandwidth, so
|
|
269
|
+
the off case must omit the line entirely.
|
|
270
|
+
"""
|
|
271
|
+
try:
|
|
272
|
+
value = int(kbps)
|
|
273
|
+
except (TypeError, ValueError):
|
|
274
|
+
return ""
|
|
275
|
+
if value <= 0:
|
|
276
|
+
return ""
|
|
277
|
+
return f"b=AS:{value}\r\n"
|
|
278
|
+
|
|
279
|
+
|
|
243
280
|
def _sdes_nack_enabled() -> bool:
|
|
244
281
|
"""Whether to ask the camera to resend video packets that never arrived.
|
|
245
282
|
|
|
@@ -353,6 +390,71 @@ def _send_video_nack(send, srtcp_sess, sender_ssrc: int,
|
|
|
353
390
|
return False
|
|
354
391
|
|
|
355
392
|
|
|
393
|
+
def _sdes_tmmbr_bps():
|
|
394
|
+
"""The bitrate bound to ask the camera for, in bits/s, or None for none.
|
|
395
|
+
|
|
396
|
+
Off unless ``AIDOT_SDES_TMMBR_BPS`` names a positive integer. Unparseable
|
|
397
|
+
is off rather than an error: this is read while a session is running.
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
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
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _sdes_tmmbr_after_s() -> float:
|
|
407
|
+
"""Seconds of MEDIA to let pass before the first TMMBR. 0 = immediately.
|
|
408
|
+
|
|
409
|
+
Exists so the bound can be measured within a session -- window A before it,
|
|
410
|
+
window B after -- instead of between sessions. On this camera a
|
|
411
|
+
between-session comparison has twice produced a wrong answer: it read a
|
|
412
|
+
codec split as a bandwidth-cap effect, and the encoder's own drift as a
|
|
413
|
+
working SD control. Both windows of one session share the codec and the
|
|
414
|
+
scene, so neither can confound it.
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
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
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _tmmbr_ready(first_video_ts, now: float, after_s: float) -> bool:
|
|
424
|
+
"""Whether enough MEDIA has passed to start asking for the bound.
|
|
425
|
+
|
|
426
|
+
Measured from the first video packet, not from the open: a camera that
|
|
427
|
+
takes twelve seconds to wake would otherwise spend all of window A capped.
|
|
428
|
+
"""
|
|
429
|
+
if first_video_ts is None:
|
|
430
|
+
return False
|
|
431
|
+
return (now - first_video_ts) >= after_s
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _send_video_tmmbr(send, srtcp_sess, sender_ssrc: int,
|
|
435
|
+
media_ssrc: int, bitrate_bps) -> bool:
|
|
436
|
+
"""Put one TMMBR on the camera's RTCP path. True if it went out.
|
|
437
|
+
|
|
438
|
+
``send`` is the bridge's relay-aware sender, NOT a raw socket, for the
|
|
439
|
+
reason spelled out on :func:`_send_video_nack`: via TURN the address media
|
|
440
|
+
arrived from is the relay, and a raw write there is dropped as a malformed
|
|
441
|
+
STUN message while still reporting success. (``REMB`` still writes to the
|
|
442
|
+
socket directly and is inert on a relayed session for exactly that reason;
|
|
443
|
+
it is latent only because its target defaults to 0.)
|
|
444
|
+
|
|
445
|
+
Never raises: this runs inside the bridge's packet loop.
|
|
446
|
+
"""
|
|
447
|
+
if not bitrate_bps or bitrate_bps <= 0:
|
|
448
|
+
return False
|
|
449
|
+
try:
|
|
450
|
+
raw = build_tmmbr(sender_ssrc, media_ssrc, bitrate_bps)
|
|
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
|
|
456
|
+
|
|
457
|
+
|
|
356
458
|
#: Sender SSRC on every RTCP we send the camera. Load-bearing, not cosmetic:
|
|
357
459
|
#: the SRTP TX policy is keyed `ssrc_value=_CAM_RTCP_SENDER_SSRC`, so the PLI,
|
|
358
460
|
#: REMB, RR and NACK must all agree or the camera drops the packet.
|
|
@@ -1073,7 +1175,7 @@ def _record_peer_reflexive(known, discovered, observed, is_self=None):
|
|
|
1073
1175
|
route to, the nomination goes into a black hole, and the session sits in
|
|
1074
1176
|
ICE "Checking" while the camera's probes keep arriving from an address we
|
|
1075
1177
|
never nominate. Observed on the A001064 PTZ while it was on a separate
|
|
1076
|
-
192.168.
|
|
1178
|
+
192.168.9.0/24: it advertised 192.168.9.13 as its only candidate.
|
|
1077
1179
|
|
|
1078
1180
|
Returns the new discovered list, or ``discovered`` unchanged when there is
|
|
1079
1181
|
nothing to learn. Callers must REBIND rather than mutate in place - the
|
|
@@ -1948,7 +2050,8 @@ class _SdesOpenMixin:
|
|
|
1948
2050
|
# video m-section
|
|
1949
2051
|
+ f"m=video {_offer_video_port} RTP/SAVPF {_video_pt_list}\r\n"
|
|
1950
2052
|
f"c=IN IP4 {_offer_video_ip}\r\n"
|
|
1951
|
-
|
|
2053
|
+
+ _offer_bandwidth_line(_sdes_offer_bandwidth_kbps())
|
|
2054
|
+
+ "a=recvonly\r\n"
|
|
1952
2055
|
"a=mid:1\r\n"
|
|
1953
2056
|
f"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:{srtp_key_video}\r\n"
|
|
1954
2057
|
+ _video_codec_attrs
|
|
@@ -1995,6 +2098,15 @@ class _SdesOpenMixin:
|
|
|
1995
2098
|
if _video_pt_order != _SDES_OFFER_VIDEO_PT_ORDER and _status:
|
|
1996
2099
|
_status(f"SDES: offer video codec order={_video_pt_list}")
|
|
1997
2100
|
|
|
2101
|
+
# Same receipt, same reason, for the receive-bandwidth ceiling: the log
|
|
2102
|
+
# carries the camera's ANSWER, not our offer, so "the env var was set"
|
|
2103
|
+
# is not evidence the line reached the wire. Measured 2026-08-23: a
|
|
2104
|
+
# b=AS arm scored identically to its control and the only available
|
|
2105
|
+
# check for the knob was reading the env back out of the harness.
|
|
2106
|
+
_bw_kbps = _sdes_offer_bandwidth_kbps()
|
|
2107
|
+
if _bw_kbps and _status:
|
|
2108
|
+
_status(f"SDES: offer receive-bandwidth ceiling b=AS:{_bw_kbps}")
|
|
2109
|
+
|
|
1998
2110
|
# Opt-in: NARROW the OFFER to one video codec rather than advertising
|
|
1999
2111
|
# both 96/97 and letting the camera decide in its answer. Distinct from
|
|
2000
2112
|
# the ordering above, which only states which of the two we would
|
|
@@ -4076,23 +4188,33 @@ class _SdesOpenMixin:
|
|
|
4076
4188
|
#
|
|
4077
4189
|
# Own cadence: the PLI timer backs off to 30s once the
|
|
4078
4190
|
# stream is up, which is too slow to hold a rate.
|
|
4191
|
+
# Goes out the relay-aware sender, NOT the socket: via TURN
|
|
4192
|
+
# the address media arrived from is the relay, and a raw
|
|
4193
|
+
# write there is dropped as a malformed STUN message while
|
|
4194
|
+
# sendto still reports success. That is the bug the NACK
|
|
4195
|
+
# path already had and had fixed; REMB was left behind, so
|
|
4196
|
+
# on a relayed camera it never arrived -- which also means
|
|
4197
|
+
# "REMB was measured to do nothing" cannot have been a valid
|
|
4198
|
+
# measurement on such a session. No fallback to the socket:
|
|
4199
|
+
# skipping a tick until the sender exists costs a second,
|
|
4200
|
+
# falling back costs correctness on the very sessions this
|
|
4201
|
+
# is for.
|
|
4202
|
+
_remb_send = getattr(_bridge_fn, '_send_to_cam', None)
|
|
4079
4203
|
if (REMB_TARGET_BPS > 0
|
|
4080
4204
|
and hasattr(_bridge_fn, '_cam_video_ssrc')
|
|
4081
|
-
and
|
|
4205
|
+
and _remb_send is not None
|
|
4082
4206
|
and _time_br.time() - getattr(
|
|
4083
4207
|
_bridge_fn, '_last_remb_ts', 0.0) >= 1.0):
|
|
4084
4208
|
_bridge_fn._last_remb_ts = _time_br.time()
|
|
4085
4209
|
try:
|
|
4086
4210
|
_remb_raw = build_remb(
|
|
4087
|
-
|
|
4211
|
+
_CAM_RTCP_SENDER_SSRC,
|
|
4088
4212
|
[_bridge_fn._cam_video_ssrc],
|
|
4089
4213
|
REMB_TARGET_BPS)
|
|
4090
4214
|
_remb_sess = getattr(_bridge_fn, '_pli_tx_sess', None)
|
|
4091
|
-
|
|
4215
|
+
_remb_send(
|
|
4092
4216
|
_remb_sess.protect_rtcp(_remb_raw)
|
|
4093
|
-
if _remb_sess is not None else _remb_raw
|
|
4094
|
-
_bridge_fn._cam_srtp_src,
|
|
4095
|
-
)
|
|
4217
|
+
if _remb_sess is not None else _remb_raw)
|
|
4096
4218
|
if not getattr(_bridge_fn, '_remb_logged', False):
|
|
4097
4219
|
_bridge_fn._remb_logged = True
|
|
4098
4220
|
_status(
|
|
@@ -4105,6 +4227,39 @@ class _SdesOpenMixin:
|
|
|
4105
4227
|
getattr(self, "device_id", "?"), '_remb',
|
|
4106
4228
|
exc_info=True)
|
|
4107
4229
|
|
|
4230
|
+
# TMMBR: a BOUND, where REMB above is an ESTIMATE. Two
|
|
4231
|
+
# different RFC 5104 / 4585 messages, and this firmware can
|
|
4232
|
+
# honour one without the other -- it already acts on NACKs
|
|
4233
|
+
# it never negotiated (our SDES offer carries no a=rtcp-fb
|
|
4234
|
+
# line at all), so "the answer does not advertise ccm
|
|
4235
|
+
# tmmbr" is not a reason to withhold it. Off by default;
|
|
4236
|
+
# every bitrate control tried on this camera so far has
|
|
4237
|
+
# been acked and ignored, and an unmeasured one must not
|
|
4238
|
+
# reach the four cameras that stream fine today.
|
|
4239
|
+
_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)
|
|
4240
|
+
_tmmbr_send = getattr(_bridge_fn, '_send_to_cam', None)
|
|
4241
|
+
if (_tmmbr_bps and _tmmbr_send is not None
|
|
4242
|
+
and hasattr(_bridge_fn, '_cam_video_ssrc')
|
|
4243
|
+
and _tmmbr_ready(
|
|
4244
|
+
getattr(_bridge_fn, '_first_video_ts', None),
|
|
4245
|
+
_time_br.time(),
|
|
4246
|
+
getattr(_bridge_fn, '_tmmbr_after_s', 0.0))
|
|
4247
|
+
and _time_br.time() - getattr(
|
|
4248
|
+
_bridge_fn, '_last_tmmbr_ts', 0.0) >= 1.0):
|
|
4249
|
+
_bridge_fn._last_tmmbr_ts = _time_br.time()
|
|
4250
|
+
if _send_video_tmmbr(
|
|
4251
|
+
_tmmbr_send,
|
|
4252
|
+
getattr(_bridge_fn, '_pli_tx_sess', None),
|
|
4253
|
+
_CAM_RTCP_SENDER_SSRC,
|
|
4254
|
+
_bridge_fn._cam_video_ssrc,
|
|
4255
|
+
_tmmbr_bps,
|
|
4256
|
+
) and not getattr(_bridge_fn, '_tmmbr_logged', False):
|
|
4257
|
+
_bridge_fn._tmmbr_logged = True
|
|
4258
|
+
_status(
|
|
4259
|
+
f"SDES: sent TMMBR {_tmmbr_bps // 1000} kbps"
|
|
4260
|
+
f" for video SSRC"
|
|
4261
|
+
f" 0x{_bridge_fn._cam_video_ssrc:08x}")
|
|
4262
|
+
|
|
4108
4263
|
_pli_done = getattr(_bridge_fn, '_pli_count', 0)
|
|
4109
4264
|
_pli_interval = (_pli_gaps[_pli_done]
|
|
4110
4265
|
if _pli_done < len(_pli_gaps) else 30.0)
|
|
@@ -5322,6 +5477,23 @@ class _SdesOpenMixin:
|
|
|
5322
5477
|
'!I', _bpkt, 8)[0]
|
|
5323
5478
|
_bridge_fn._cam_srtp_src = _bsrc
|
|
5324
5479
|
_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
|
+
if getattr(_bridge_fn, '_first_video_ts',
|
|
5488
|
+
None) is None:
|
|
5489
|
+
_bridge_fn._first_video_ts = _time_br.time()
|
|
5490
|
+
if getattr(_bridge_fn, '_send_to_cam_src',
|
|
5491
|
+
None) != _bsrc:
|
|
5492
|
+
_bridge_fn._send_to_cam_src = _bsrc
|
|
5493
|
+
_bridge_fn._send_to_cam = (
|
|
5494
|
+
lambda _d, _s=_bs, _a=_bsrc,
|
|
5495
|
+
_p=_br_cam_peer: _br_send_to_cam(
|
|
5496
|
+
_s, _d, _a, _p))
|
|
5325
5497
|
# Schedule immediate PLI so IDR+SPS arrives in
|
|
5326
5498
|
# the analyzeduration window.
|
|
5327
5499
|
_bridge_fn._last_pli_ts = 0.0
|
|
@@ -5330,6 +5502,13 @@ class _SdesOpenMixin:
|
|
|
5330
5502
|
# os.environ.get costs ~250ns of it. A flip
|
|
5331
5503
|
# now takes effect on the next stream open.
|
|
5332
5504
|
_bridge_fn._nack_on = _sdes_nack_enabled()
|
|
5505
|
+
# Same reason, same place: the RTCP cadence
|
|
5506
|
+
# below sits on this loop, so its switches are
|
|
5507
|
+
# resolved once here rather than read from the
|
|
5508
|
+
# environment ~300x/s. A flip takes effect on
|
|
5509
|
+
# the next stream open, as the NACK switch does.
|
|
5510
|
+
_bridge_fn._tmmbr_bps = _sdes_tmmbr_bps()
|
|
5511
|
+
_bridge_fn._tmmbr_after_s = _sdes_tmmbr_after_s()
|
|
5333
5512
|
# For TUTK cameras (_use_plain_rtp) the ffmpeg SDP uses
|
|
5334
5513
|
# RTP/AVP (no crypto). After LIVING the camera switches
|
|
5335
5514
|
# from TUTK SFrames to standard SRTP, so we decrypt here
|