python-aidot-cameras 0.17.0__tar.gz → 0.17.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-0.17.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.17.2}/PKG-INFO +4 -1
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/README.md +3 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/client.py +56 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/constants.py +7 -3
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/controls.py +11 -3
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/protocol.py +127 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/sdes_open.py +184 -2
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/webrtc_open.py +41 -11
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/client.py +26 -6
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/device_client.py +61 -11
- python_aidot_cameras-0.17.2/aidot_cameras/lan_retry.py +198 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2/python_aidot_cameras.egg-info}/PKG-INFO +4 -1
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/python_aidot_cameras.egg-info/SOURCES.txt +11 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dormant_camera_cleanup.py +52 -0
- python_aidot_cameras-0.17.2/tests/test_futile_keepalive.py +59 -0
- python_aidot_cameras-0.17.2/tests/test_lan_retry_reaches_the_storming_devices.py +252 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_light_active_color_mode.py +15 -6
- python_aidot_cameras-0.17.2/tests/test_login_connect_deadline.py +113 -0
- python_aidot_cameras-0.17.2/tests/test_login_reconnect_backoff.py +83 -0
- python_aidot_cameras-0.17.2/tests/test_offer_keeps_rtcp_feedback.py +137 -0
- python_aidot_cameras-0.17.2/tests/test_remb.py +96 -0
- python_aidot_cameras-0.17.2/tests/test_sdes_offer_video_pin.py +107 -0
- python_aidot_cameras-0.17.2/tests/test_sprop_capture_gate.py +63 -0
- python_aidot_cameras-0.17.2/tests/test_video_profile_log.py +61 -0
- python_aidot_cameras-0.17.2/tests/test_wake_gate_camera_evidence.py +120 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/LICENSE +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.17.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.17.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.2
|
|
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
|
|
@@ -178,6 +178,9 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
178
178
|
| `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) |
|
|
179
179
|
| `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) |
|
|
180
180
|
| `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) |
|
|
181
|
+
| `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone (it is retried again the next time something asks for it). Applies to every device, camera or not - the devices that hit this are lights. | `6` |
|
|
182
|
+
| `AIDOT_LOGIN_RETRY_CAP_S` | Ceiling on the exponential delay between those retries. | `60` |
|
|
183
|
+
| `AIDOT_LOGIN_CONNECT_TIMEOUT_S` | Ceiling on one LAN connect+login attempt. A device that completes the TCP handshake and then stops answering is abandoned and its socket closed rather than parking the attempt forever. | `20` |
|
|
181
184
|
| `AIDOT_LIVESTREAM_PARAM` | **No-op (ignored).** Formerly requested the cloud `liveStreamParam` pre-connect for battery cameras. That call provisions the camera toward AWS KVS, so it sends its media there instead of to this library and the live view negotiates and then shows nothing - and battery cameras were the only ones it applied to. Setting it (or `start_keepalive(live_stream_param=True)`) logs one warning and changes nothing. | ignored |
|
|
182
185
|
|
|
183
186
|
### Security hardening
|
|
@@ -147,6 +147,9 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
147
147
|
| `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) |
|
|
148
148
|
| `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) |
|
|
149
149
|
| `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) |
|
|
150
|
+
| `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone (it is retried again the next time something asks for it). Applies to every device, camera or not - the devices that hit this are lights. | `6` |
|
|
151
|
+
| `AIDOT_LOGIN_RETRY_CAP_S` | Ceiling on the exponential delay between those retries. | `60` |
|
|
152
|
+
| `AIDOT_LOGIN_CONNECT_TIMEOUT_S` | Ceiling on one LAN connect+login attempt. A device that completes the TCP handshake and then stops answering is abandoned and its socket closed rather than parking the attempt forever. | `20` |
|
|
150
153
|
| `AIDOT_LIVESTREAM_PARAM` | **No-op (ignored).** Formerly requested the cloud `liveStreamParam` pre-connect for battery cameras. That call provisions the camera toward AWS KVS, so it sends its media there instead of to this library and the live view negotiates and then shows nothing - and battery cameras were the only ones it applied to. Setting it (or `start_keepalive(live_stream_param=True)`) logs one warning and changes nothing. | ignored |
|
|
151
154
|
|
|
152
155
|
### Security hardening
|
|
@@ -388,6 +388,32 @@ _SERVE_MAX_DELAY_US = int(os.environ.get("AIDOT_SERVE_MAX_DELAY_US", "500000"))
|
|
|
388
388
|
#: has none free, and on a battery model risks a wake-then-sleep loop.
|
|
389
389
|
_BUSY_BACKOFF_S = float(os.environ.get("AIDOT_BUSY_BACKOFF_S", "20"))
|
|
390
390
|
|
|
391
|
+
#: Consecutive background sessions that deliver NO media before the keepalive
|
|
392
|
+
#: stops reopening a battery camera. Observed 2026-08-07 on an A001513: 22 opens
|
|
393
|
+
#: across ~8 hours, none of them delivering media, because the loop escalated
|
|
394
|
+
#: its backoff but had no condition under which it ever stopped. On a mains
|
|
395
|
+
#: camera that is wasteful; on a battery one it spends charge, and a unit has
|
|
396
|
+
#: already been drained to 5% that way.
|
|
397
|
+
#: 0 disables the ceiling entirely.
|
|
398
|
+
_FUTILE_KEEPALIVE_LIMIT = int(os.environ.get("AIDOT_FUTILE_KEEPALIVE_LIMIT", "5"))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _should_abandon_keepalive(no_media_streak: int, *, is_battery: bool,
|
|
402
|
+
limit: int = _FUTILE_KEEPALIVE_LIMIT) -> bool:
|
|
403
|
+
"""Should the background keepalive stop reopening this camera?
|
|
404
|
+
|
|
405
|
+
Only for battery cameras. A mains camera has no charge to protect, and the
|
|
406
|
+
same persistence is what recovers a stream after a router reboot or a
|
|
407
|
+
power-cycle - there the loop is the feature.
|
|
408
|
+
|
|
409
|
+
Scoped to the BACKGROUND loop. A view still opens a session on demand: the
|
|
410
|
+
user asking to see the camera is new information, and a retry ceiling should
|
|
411
|
+
not decide on their behalf that it cannot work.
|
|
412
|
+
"""
|
|
413
|
+
if not is_battery or limit <= 0:
|
|
414
|
+
return False
|
|
415
|
+
return no_media_streak >= limit
|
|
416
|
+
|
|
391
417
|
|
|
392
418
|
def _build_sdes_serve_cmd(
|
|
393
419
|
*,
|
|
@@ -3357,6 +3383,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3357
3383
|
# camera (or a fleet reconnecting at once) isn't hammered into further
|
|
3358
3384
|
# degradation / cloud rate-limiting; resets after a session that delivered
|
|
3359
3385
|
# media (see end of loop).
|
|
3386
|
+
_no_media_streak = 0
|
|
3360
3387
|
_pacer = ReconnectPacer(_MIN_DELAY, _MAX_DELAY)
|
|
3361
3388
|
|
|
3362
3389
|
# Adaptive fast-with-fallback (default on): the first open tries the fast
|
|
@@ -3579,6 +3606,35 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3579
3606
|
_loop_peer_id = self.generate_webrtc_peer_id(
|
|
3580
3607
|
live_type=2, stream_id=0, sdes=True)
|
|
3581
3608
|
_peer_reuses = 0
|
|
3609
|
+
_no_media_streak = 0
|
|
3610
|
+
else:
|
|
3611
|
+
_no_media_streak += 1
|
|
3612
|
+
if _should_abandon_keepalive(
|
|
3613
|
+
_no_media_streak, is_battery=self.is_battery_camera):
|
|
3614
|
+
# Every open so far has cost the camera a wake and returned
|
|
3615
|
+
# nothing. Continuing spends charge to learn the same thing
|
|
3616
|
+
# again; a view will still open a session on demand.
|
|
3617
|
+
_LOGGER.warning(
|
|
3618
|
+
"camera %s: %d consecutive keepalive sessions delivered no "
|
|
3619
|
+
"media - stopping the background keepalive to stop waking "
|
|
3620
|
+
"it. A live view will still open a session; set "
|
|
3621
|
+
"AIDOT_FUTILE_KEEPALIVE_LIMIT=0 to keep retrying.",
|
|
3622
|
+
self.device_id, _no_media_streak,
|
|
3623
|
+
)
|
|
3624
|
+
self._streaming_active = False
|
|
3625
|
+
# Same teardown the idle-release exit does. Leaving the
|
|
3626
|
+
# go2rtc stream registered points it at a serve port with
|
|
3627
|
+
# nothing listening, so a viewer attaching to a dormant
|
|
3628
|
+
# camera gets "connection refused" instead of a clean miss -
|
|
3629
|
+
# exactly what the idle-release path exists to avoid.
|
|
3630
|
+
self._cancel_keepalive_renew()
|
|
3631
|
+
try:
|
|
3632
|
+
await self._deregister_go2rtc()
|
|
3633
|
+
except Exception:
|
|
3634
|
+
_LOGGER.debug("camera %s: go2rtc deregister after "
|
|
3635
|
+
"abandon failed", self.device_id,
|
|
3636
|
+
exc_info=True)
|
|
3637
|
+
return
|
|
3582
3638
|
if _use_fast and not _healthy and not _fast_failed:
|
|
3583
3639
|
_LOGGER.info(
|
|
3584
3640
|
"SDES adaptive[%s]: fast attempt delivered no media - "
|
{python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/constants.py
RENAMED
|
@@ -131,9 +131,13 @@ _PTZ_DIR_CODES: dict = {
|
|
|
131
131
|
"zoom_in": 23, "zoom_out": 24,
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
# Live-stream resolution (AVIOCTRL_QUALITY_*). The official app's
|
|
135
|
-
#
|
|
136
|
-
#
|
|
134
|
+
# Live-stream resolution (AVIOCTRL_QUALITY_*). The official app's picker has
|
|
135
|
+
# THREE arms, not two: MAX(1), MIDDLE(5), and 0x10 = 16, whose label is
|
|
136
|
+
# _auto_resulution (HorLiveController.smali:629-631). 16 is deliberately absent
|
|
137
|
+
# here: it is outside AVIOCTRL_QUALITY, and measured 2026-08-07 it behaves like
|
|
138
|
+
# every other value - acked, and the encode unchanged in that session and the
|
|
139
|
+
# next. Adding it would offer a control that does nothing. The two arms below
|
|
140
|
+
# are the ones with enum names, kept for parity with the command the app sends. Sent as SETSTREAMCTRL (cmd 800) over the active session,
|
|
137
141
|
# payload <IB3x> = channel(0) + quality byte (SMsgAVIoctrlSetStreamCtrlReq).
|
|
138
142
|
SETSTREAMCTRL_CMD = 800
|
|
139
143
|
GETSTREAMCTRL_CMD = 802
|
{python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/controls.py
RENAMED
|
@@ -172,9 +172,17 @@ class _CameraControlsMixin:
|
|
|
172
172
|
"will be applied when a session next starts", quality)
|
|
173
173
|
return True
|
|
174
174
|
# Ask, and read what comes back. The camera acks with 801 in
|
|
175
|
-
# 0.01-0.
|
|
176
|
-
#
|
|
177
|
-
#
|
|
175
|
+
# 0.01-0.19s and reports the new value through GETSTREAMCTRL afterwards
|
|
176
|
+
# (where it implements 802), so the command is genuinely accepted - it
|
|
177
|
+
# simply does not change the encode.
|
|
178
|
+
#
|
|
179
|
+
# Settled 2026-08-07 across both transports and every value the enum
|
|
180
|
+
# defines, not just hd/sd: an A001064 was swept over all six
|
|
181
|
+
# AVIOCTRL_QUALITY values, each followed by a session that sent nothing
|
|
182
|
+
# to catch a setting that applies only to the NEXT session. Twelve
|
|
183
|
+
# sessions, twelve times h264 1280x720, no dimension change in any of
|
|
184
|
+
# them. Four of those values had never been sent to any camera here.
|
|
185
|
+
# See docs/APP-PARITY-STATUS.md for the table.
|
|
178
186
|
#
|
|
179
187
|
# The RETURN value deliberately still means "the command went out". An
|
|
180
188
|
# A001064 answers SPEAKERSTART but does not implement GETSTREAMCTRL at
|
{python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -235,6 +235,14 @@ def _parse_alarm_event(msg: dict):
|
|
|
235
235
|
return {"device_id": dev, "type": kind, "alarm_type": code}
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
def _section_of(lines: list) -> str:
|
|
239
|
+
"""The m-section the given emitted lines currently end inside, or ""."""
|
|
240
|
+
for ln in reversed(lines):
|
|
241
|
+
if ln.startswith("m="):
|
|
242
|
+
return ln.split(" ")[0]
|
|
243
|
+
return ""
|
|
244
|
+
|
|
245
|
+
|
|
238
246
|
def _compress_sdp_for_camera(sdp: str) -> str:
|
|
239
247
|
"""Selective SDP filter matching official Java client's g.b() behaviour.
|
|
240
248
|
|
|
@@ -258,6 +266,14 @@ def _compress_sdp_for_camera(sdp: str) -> str:
|
|
|
258
266
|
seen: dict = {}
|
|
259
267
|
media_type = ""
|
|
260
268
|
before_m = True
|
|
269
|
+
# Payload types that survived narrowing. rtcp-fb is kept for these and only
|
|
270
|
+
# these: feedback describing a codec we dropped is bytes spent on something
|
|
271
|
+
# that will never be sent, in an offer whose size is why this function
|
|
272
|
+
# exists.
|
|
273
|
+
_kept_pts: set = set()
|
|
274
|
+
# rtcp-fb lines are buffered because they can appear before the rtpmap that
|
|
275
|
+
# decides whether their payload type survives.
|
|
276
|
+
_fb_lines: list = []
|
|
261
277
|
|
|
262
278
|
def keep(ln: str, key: str = "") -> None:
|
|
263
279
|
out.append(ln + "\r\n")
|
|
@@ -283,6 +299,11 @@ def _compress_sdp_for_camera(sdp: str) -> str:
|
|
|
283
299
|
if ln.startswith("a=mid:"):
|
|
284
300
|
keep(ln)
|
|
285
301
|
continue
|
|
302
|
+
if ln.startswith("a=rtcp-fb:"):
|
|
303
|
+
# Buffered, not decided here: an rtcp-fb line can precede the
|
|
304
|
+
# rtpmap that determines whether its payload type survives.
|
|
305
|
+
_fb_lines.append((media_type, ln))
|
|
306
|
+
continue
|
|
286
307
|
if ln.startswith("a=ssrc") and "cname" in ln:
|
|
287
308
|
if seen.get(f"ssrc-cname-{media_type}") is None:
|
|
288
309
|
keep(ln, f"ssrc-cname-{media_type}")
|
|
@@ -303,17 +324,26 @@ def _compress_sdp_for_camera(sdp: str) -> str:
|
|
|
303
324
|
if media_type == "m=audio":
|
|
304
325
|
if any(c in ln for c in ("opus", "PCMU", "PCMA", "AAC")):
|
|
305
326
|
keep(ln)
|
|
327
|
+
# Remember the payload type so its rtcp-fb can be kept too.
|
|
328
|
+
if ln.startswith("a=rtpmap:"):
|
|
329
|
+
try:
|
|
330
|
+
_kept_pts.add(ln.split(":")[1].split(" ")[0])
|
|
331
|
+
except Exception:
|
|
332
|
+
_LOGGER.debug("swallowed exception in %s", 'keep',
|
|
333
|
+
exc_info=True)
|
|
306
334
|
elif media_type == "m=video":
|
|
307
335
|
if "H264/90000" in ln and seen.get("H264/90000") is None:
|
|
308
336
|
keep(ln, "H264/90000")
|
|
309
337
|
try:
|
|
310
338
|
seen["H264/90000_pt"] = ln.split(":")[1].split(" ")[0]
|
|
339
|
+
_kept_pts.add(seen["H264/90000_pt"])
|
|
311
340
|
except Exception:
|
|
312
341
|
_LOGGER.debug("swallowed exception in %s", 'keep', exc_info=True)
|
|
313
342
|
elif "H265/90000" in ln and seen.get("H265/90000") is None:
|
|
314
343
|
keep(ln, "H265/90000")
|
|
315
344
|
try:
|
|
316
345
|
seen["H265/90000_pt"] = ln.split(":")[1].split(" ")[0]
|
|
346
|
+
_kept_pts.add(seen["H265/90000_pt"])
|
|
317
347
|
except Exception:
|
|
318
348
|
_LOGGER.debug("swallowed exception in %s", 'keep', exc_info=True)
|
|
319
349
|
elif "apt=" in ln:
|
|
@@ -332,6 +362,22 @@ def _compress_sdp_for_camera(sdp: str) -> str:
|
|
|
332
362
|
elif media_type == "m=application":
|
|
333
363
|
if "sctp-port" in ln:
|
|
334
364
|
keep(ln)
|
|
365
|
+
# Re-insert the surviving feedback at the end of its own media section, so
|
|
366
|
+
# the offer stays well-formed. Kept only for payload types that survived
|
|
367
|
+
# narrowing - see _kept_pts.
|
|
368
|
+
if _fb_lines:
|
|
369
|
+
kept_fb = [(mt, ln) for mt, ln in _fb_lines
|
|
370
|
+
if ln.split(":", 1)[1].split(" ")[0] in _kept_pts]
|
|
371
|
+
if kept_fb:
|
|
372
|
+
rebuilt: list = []
|
|
373
|
+
for line in out:
|
|
374
|
+
if line.startswith("m=") and rebuilt:
|
|
375
|
+
prev = _section_of(rebuilt)
|
|
376
|
+
rebuilt.extend(l + "\r\n" for mt, l in kept_fb if mt == prev)
|
|
377
|
+
rebuilt.append(line)
|
|
378
|
+
prev = _section_of(rebuilt)
|
|
379
|
+
rebuilt.extend(l + "\r\n" for mt, l in kept_fb if mt == prev)
|
|
380
|
+
out = rebuilt
|
|
335
381
|
return "".join(out)
|
|
336
382
|
|
|
337
383
|
|
|
@@ -2337,6 +2383,87 @@ class AvioResponseRouter:
|
|
|
2337
2383
|
self._waiting.pop(waiter._command, None)
|
|
2338
2384
|
|
|
2339
2385
|
|
|
2386
|
+
#: What we ask the camera to send, in bits per second. Zero disables sending
|
|
2387
|
+
#: REMB at all, which is the default.
|
|
2388
|
+
#:
|
|
2389
|
+
#: OFF by default because it was measured and did not work. An A001064 was A/B'd
|
|
2390
|
+
#: with REMB transmitting at 500 Kbps and correctly naming the video SSRC -
|
|
2391
|
+
#: receipts per session, interleaved arms against a control - and its bitrate
|
|
2392
|
+
#: did not fall. The one clean like-for-like pair had the REMB arm HIGHER than
|
|
2393
|
+
#: its control (3859 vs 3355 Kbps). No effect was demonstrated on the only model
|
|
2394
|
+
#: tested.
|
|
2395
|
+
#:
|
|
2396
|
+
#: That alone would argue for opt-in. The stronger argument is the models NOT
|
|
2397
|
+
#: tested: an A000088 or A001513 that does honour REMB would have had its
|
|
2398
|
+
#: picture capped at 500 Kbps fleet-wide, by default, on the strength of a
|
|
2399
|
+
#: measurement taken elsewhere that showed no benefit. Shipping a bitrate cap
|
|
2400
|
+
#: enabled by default, validated on one camera that ignores it, risks degrading
|
|
2401
|
+
#: precisely the cameras nobody measured.
|
|
2402
|
+
#:
|
|
2403
|
+
#: The negotiation, the encoder and the decoder are all kept and tested: the
|
|
2404
|
+
#: camera does advertise goog-remb in its answer, so this is a working
|
|
2405
|
+
#: instrument for anyone who wants to measure their own hardware. Set
|
|
2406
|
+
#: AIDOT_REMB_TARGET_BPS to a bitrate to turn it on.
|
|
2407
|
+
REMB_TARGET_BPS = int(os.environ.get("AIDOT_REMB_TARGET_BPS", "0"))
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
def build_remb(sender_ssrc: int, media_ssrcs: "list", bitrate_bps: int) -> bytes:
|
|
2411
|
+
"""An RTCP REMB packet asking the sender for at most ``bitrate_bps``.
|
|
2412
|
+
|
|
2413
|
+
Format is the Google draft (draft-alvestrand-rmcat-remb) carried as a
|
|
2414
|
+
Payload-Specific Feedback message: FMT 15, PT 206, the ASCII identifier
|
|
2415
|
+
``REMB``, then a count of SSRCs and the bitrate as a 6-bit exponent with an
|
|
2416
|
+
18-bit mantissa, then the SSRCs themselves.
|
|
2417
|
+
|
|
2418
|
+
The camera negotiates ``goog-remb`` in its answer (confirmed live on an
|
|
2419
|
+
A001064 over SDES), which means it will accept this. It has no effect until
|
|
2420
|
+
one is actually sent - negotiation alone changes nothing, which is why this
|
|
2421
|
+
is a separate step from keeping the attribute in the offer.
|
|
2422
|
+
"""
|
|
2423
|
+
if bitrate_bps <= 0:
|
|
2424
|
+
raise ValueError("REMB bitrate must be positive - zero would ask the "
|
|
2425
|
+
"camera to stop sending entirely")
|
|
2426
|
+
if not media_ssrcs:
|
|
2427
|
+
raise ValueError("REMB must name at least one stream")
|
|
2428
|
+
|
|
2429
|
+
# 18-bit mantissa, 6-bit exponent: shift right until the value fits.
|
|
2430
|
+
exp = 0
|
|
2431
|
+
mantissa = int(bitrate_bps)
|
|
2432
|
+
while mantissa > 0x3FFFF:
|
|
2433
|
+
mantissa >>= 1
|
|
2434
|
+
exp += 1
|
|
2435
|
+
if exp > 63:
|
|
2436
|
+
raise ValueError("REMB bitrate too large to encode")
|
|
2437
|
+
|
|
2438
|
+
header = struct.pack(
|
|
2439
|
+
"!BBH",
|
|
2440
|
+
0x80 | 15, # V=2, P=0, FMT=15
|
|
2441
|
+
206, # PT: payload-specific feedback
|
|
2442
|
+
0, # length, filled in below
|
|
2443
|
+
)
|
|
2444
|
+
body = (struct.pack("!II", sender_ssrc & 0xFFFFFFFF, 0)
|
|
2445
|
+
+ b"REMB"
|
|
2446
|
+
+ struct.pack("!BBBB",
|
|
2447
|
+
len(media_ssrcs) & 0xFF,
|
|
2448
|
+
((exp << 2) | (mantissa >> 16)) & 0xFF,
|
|
2449
|
+
(mantissa >> 8) & 0xFF,
|
|
2450
|
+
mantissa & 0xFF)
|
|
2451
|
+
+ b"".join(struct.pack("!I", s & 0xFFFFFFFF) for s in media_ssrcs))
|
|
2452
|
+
pkt = header + body
|
|
2453
|
+
# RTCP length is in 32-bit words, minus one.
|
|
2454
|
+
return pkt[:2] + struct.pack("!H", len(pkt) // 4 - 1) + pkt[4:]
|
|
2455
|
+
|
|
2456
|
+
|
|
2457
|
+
def decode_remb_bitrate(pkt: bytes) -> int:
|
|
2458
|
+
"""The bitrate a REMB packet asks for. For tests and diagnostics."""
|
|
2459
|
+
# Layout: 0-3 header, 4-7 sender SSRC, 8-11 media SSRC, 12-15 "REMB",
|
|
2460
|
+
# 16 num-SSRC, 17-19 exponent+mantissa, 20+ the SSRCs.
|
|
2461
|
+
b = pkt[17]
|
|
2462
|
+
exp = b >> 2
|
|
2463
|
+
mantissa = ((b & 0x03) << 16) | (pkt[18] << 8) | pkt[19]
|
|
2464
|
+
return mantissa << exp
|
|
2465
|
+
|
|
2466
|
+
|
|
2340
2467
|
def _avio_cmd_id(frame: bytes) -> "Optional[int]":
|
|
2341
2468
|
"""The command id of an AVIO frame, or None if it does not look like one.
|
|
2342
2469
|
|
{python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -25,7 +25,9 @@ from .models import VideoFrame # noqa: F401 - forward-ref annotation
|
|
|
25
25
|
from .sdes import SdesSession
|
|
26
26
|
from .protocol import (
|
|
27
27
|
AVIO_HDR_LEN,
|
|
28
|
+
REMB_TARGET_BPS,
|
|
28
29
|
AvioResponseRouter,
|
|
30
|
+
build_remb,
|
|
29
31
|
_build_sprop,
|
|
30
32
|
parse_avio_response,
|
|
31
33
|
_build_stun_binding_success_response,
|
|
@@ -174,6 +176,33 @@ _SDP_AUDIO_PTS = (0, 8)
|
|
|
174
176
|
# even when the camera numbers that codec differently on the wire - it is our own
|
|
175
177
|
# SDP being rewritten, not the camera's.
|
|
176
178
|
_SDP_VIDEO_PT_BY_CODEC = {"H264": 96, "H265": 97}
|
|
179
|
+
_SDP_CODEC_BY_VIDEO_PT = {pt: codec for codec, pt in _SDP_VIDEO_PT_BY_CODEC.items()}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def describe_video_profile(pt) -> str:
|
|
183
|
+
"""Name the video profile a session negotiated, for the log.
|
|
184
|
+
|
|
185
|
+
An A001064 was measured serving two profiles for identical requests - H264
|
|
186
|
+
1280x720 at 2.5-4.0 Mbps and H265 2560x1440 at ~1.1 Mbps - varying per
|
|
187
|
+
session with nothing on our side asking for either. What selects it is
|
|
188
|
+
unknown, and it is unknown because nothing recorded it: bitrate figures
|
|
189
|
+
gathered before 2026-08-07 cannot be compared, since the codec that produced
|
|
190
|
+
each one was never written down.
|
|
191
|
+
|
|
192
|
+
Instrumentation, not a fix. It asserts no cause.
|
|
193
|
+
|
|
194
|
+
Deliberately limited to the payload type and its codec name, which is what
|
|
195
|
+
the bridge knows when the first video packet lands. Frame dimensions would
|
|
196
|
+
mean parsing H264/H265 parameter sets out of the stream - far more than the
|
|
197
|
+
question needs, and on the camera measured codec and resolution moved
|
|
198
|
+
together in 11 of 11 sessions, so the codec stratifies it today.
|
|
199
|
+
|
|
200
|
+
Never raises: this runs on the media path, and nothing here may be able to
|
|
201
|
+
break an otherwise healthy stream. An unmapped payload type is reported as
|
|
202
|
+
``unknown``, which is a finding worth having rather than a reason to skip
|
|
203
|
+
the record.
|
|
204
|
+
"""
|
|
205
|
+
return f"pt={pt} codec={_SDP_CODEC_BY_VIDEO_PT.get(pt) or 'unknown'}"
|
|
177
206
|
|
|
178
207
|
# How long to wait for the FIRST media of a session before launching the serve.
|
|
179
208
|
# Nothing useful can happen before then: the payload types are unknown, so the SDP
|
|
@@ -245,6 +274,77 @@ def video_pt_from_answer_sdp(sdp_text: str) -> Optional[int]:
|
|
|
245
274
|
return None
|
|
246
275
|
|
|
247
276
|
|
|
277
|
+
#: Video payload types the SDES answer template advertises (H264, H265).
|
|
278
|
+
_SDES_ANSWER_VIDEO_PTS = (96, 97)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _should_capture_sprop(kind, decrypted: bool, sprop_done: bool) -> bool:
|
|
282
|
+
"""May this packet's payload be mined for SPS/PPS and cached to disk?
|
|
283
|
+
|
|
284
|
+
``decrypted`` is the load-bearing term and it used to be missing. The
|
|
285
|
+
bridge's ``_fwd_pkt`` starts as the raw inbound packet and is rebound only
|
|
286
|
+
when ``unprotect`` succeeds, so it stays ciphertext whenever there is no
|
|
287
|
+
SRTP receive session - pylibsrtp is in the optional ``webrtc`` extra, so a
|
|
288
|
+
base install never has one - or when ``unprotect`` raised, since that
|
|
289
|
+
handler logs a few lines and falls through.
|
|
290
|
+
|
|
291
|
+
SRTP leaves the RTP header in clear, so the payload offset comes out right
|
|
292
|
+
and random ciphertext reaches the NAL demux; ``payload[0] & 0x1F`` draws 7
|
|
293
|
+
and 8 at about 1/32 each, and on a 30 fps stream both appear within seconds.
|
|
294
|
+
|
|
295
|
+
The result is written to ``~/.config/aidot/sprop/<devid>.sprop`` and injected
|
|
296
|
+
as ``sprop-parameter-sets=`` by every later session, so a poisoned cache
|
|
297
|
+
outlives the session that created it: installing pylibsrtp afterwards does
|
|
298
|
+
not clear the file, and the first correct capture then differs from it and
|
|
299
|
+
trips the ``.unstable`` marker, disabling injection for that camera for good.
|
|
300
|
+
A bad frame is cheap; a bad file is not.
|
|
301
|
+
|
|
302
|
+
Returning False for a packet nobody decrypted is right even where ffmpeg
|
|
303
|
+
does its own decryption: what we hold is still ciphertext, so anything mined
|
|
304
|
+
from it would be noise either way.
|
|
305
|
+
"""
|
|
306
|
+
return bool(kind == "video" and decrypted and not sprop_done)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _resolve_sdes_video_pt() -> Optional[int]:
|
|
310
|
+
"""EXPERIMENTAL (opt-in, default off): pin the OFFER to one video codec.
|
|
311
|
+
|
|
312
|
+
The offer sent in webrtcReq advertises BOTH 96 (H264) and 97 (H265) and
|
|
313
|
+
expresses no preference, so the camera chooses which to send in its answer.
|
|
314
|
+
Measured on an A001064 across eleven sessions in one afternoon it chose H264
|
|
315
|
+
nine times and H265 twice for an otherwise identical request, and the codec
|
|
316
|
+
it chose determined the resolution (H264 -> 1280x720, H265 -> 2560x1440, 11
|
|
317
|
+
of 11). A consumer that cannot decode a sudden 2560x1440 H265 stream, or
|
|
318
|
+
cannot absorb the bitrate change either way, has no means today of
|
|
319
|
+
preventing the flip. Pinning removes the choice: measured 2026-08-07, an
|
|
320
|
+
offer pinned to 96 produced h264 1280x720 in 4 of 4 sessions.
|
|
321
|
+
|
|
322
|
+
It is the OFFER that matters, not the answer. Traced live with every status
|
|
323
|
+
line printed: this path sends webrtcReq carrying our offer and then reports
|
|
324
|
+
"Using camera's video SRTP key from answer" - the camera answers, we do not.
|
|
325
|
+
An earlier version of this pinned the answer builder instead and changed
|
|
326
|
+
nothing at all, while the arms still came out looking like it had worked.
|
|
327
|
+
|
|
328
|
+
**Do not set this to 97.** An H265-only offer returned NO VIDEO - audio
|
|
329
|
+
only, no video stream in the recording - in 3 of 3 interleaved rounds
|
|
330
|
+
against 3 of 3 successes for 96 in the same run. The efficient H265 profile
|
|
331
|
+
is real and reproducible, but only when BOTH codecs are offered; narrowing
|
|
332
|
+
to it removes the option rather than selecting it.
|
|
333
|
+
|
|
334
|
+
Left unset this returns None and the offer is byte-identical to today. The
|
|
335
|
+
SDES offer path is shared by every SDES camera, and this project's CHANGELOG
|
|
336
|
+
records fleet-wide blackouts caused by changes to shared paths, so the
|
|
337
|
+
default has to be inert. Anything unparseable, or a payload type the
|
|
338
|
+
template does not advertise, also returns None: narrowing to a payload type
|
|
339
|
+
the camera was never offered would leave it nothing to send.
|
|
340
|
+
"""
|
|
341
|
+
raw = (os.environ.get("AIDOT_SDES_VIDEO_PT", "") or "").strip()
|
|
342
|
+
if not raw.isdigit():
|
|
343
|
+
return None
|
|
344
|
+
pt = int(raw)
|
|
345
|
+
return pt if pt in _SDES_ANSWER_VIDEO_PTS else None
|
|
346
|
+
|
|
347
|
+
|
|
248
348
|
def narrow_sdp_payload_types(sdp_text: str, keep_video=None, keep_audio=None) -> str:
|
|
249
349
|
"""Rewrite an SDP to advertise a single payload type per media line.
|
|
250
350
|
|
|
@@ -1086,6 +1186,23 @@ class _SdesOpenMixin:
|
|
|
1086
1186
|
)
|
|
1087
1187
|
)
|
|
1088
1188
|
|
|
1189
|
+
# Opt-in: express a video-codec preference in the OFFER rather than
|
|
1190
|
+
# advertising both 96/97 and letting the camera decide in its answer.
|
|
1191
|
+
#
|
|
1192
|
+
# The offer is the SDP that matters here. Traced live 2026-08-07 with
|
|
1193
|
+
# every status line printed: this path sends webrtcReq carrying OUR
|
|
1194
|
+
# offer and then reports "Using camera's video SRTP key from answer" -
|
|
1195
|
+
# the camera answers, we do not. The answer builder further down runs
|
|
1196
|
+
# only on the branch where the camera offers first, which this camera
|
|
1197
|
+
# did not take, so pinning there changed nothing at all while the
|
|
1198
|
+
# arms still came out looking like the pin had worked. Inert unless set.
|
|
1199
|
+
_pin_video_pt = _resolve_sdes_video_pt()
|
|
1200
|
+
if _pin_video_pt is not None:
|
|
1201
|
+
sdes_offer_sdp = narrow_sdp_payload_types(
|
|
1202
|
+
sdes_offer_sdp, keep_video=_pin_video_pt)
|
|
1203
|
+
if _status:
|
|
1204
|
+
_status(f"SDES: offer pinned to video pt={_pin_video_pt}")
|
|
1205
|
+
|
|
1089
1206
|
_relay_str = (
|
|
1090
1207
|
f" relay-audio={_relay_addrs[_audio_sock][0]}:{_relay_addrs[_audio_sock][1]}"
|
|
1091
1208
|
if _audio_sock in _relay_addrs else ""
|
|
@@ -1351,6 +1468,18 @@ class _SdesOpenMixin:
|
|
|
1351
1468
|
"dstAddr": device_id,
|
|
1352
1469
|
"encOffer": 1,
|
|
1353
1470
|
"liveMqtt": 1,
|
|
1471
|
+
# powerType / p2pCache ride on the webrtcReq payload, alongside
|
|
1472
|
+
# encOffer and liveMqtt - the same object the reference client
|
|
1473
|
+
# puts them on (LDSMQTTClient.sendSdpOffer, smali :2967-2969,
|
|
1474
|
+
# put at :3017/:3022). They are STRINGS on the wire there: the
|
|
1475
|
+
# app stringifies the ints it reads from the IPC device info,
|
|
1476
|
+
# and its no-device-info fallback puts the literals "1" / "0".
|
|
1477
|
+
# The DTLS webrtcReq has carried both for a long time
|
|
1478
|
+
# (webrtc_open.py, per docs/official_camera_network_calls.md
|
|
1479
|
+
# section 5.2) and sends them as ints; that path is fleet-proven,
|
|
1480
|
+
# so it is deliberately left alone rather than churned to match.
|
|
1481
|
+
"powerType": str(_live_power_type),
|
|
1482
|
+
"p2pCache": str(_live_p2p_cache),
|
|
1354
1483
|
# wPayload: newer firmware parses wPayload for ICE credentials
|
|
1355
1484
|
# and PSK. Fields match reference app o.java (signaling/tyrus).
|
|
1356
1485
|
"wPayload": {
|
|
@@ -3061,6 +3190,43 @@ class _SdesOpenMixin:
|
|
|
3061
3190
|
except ValueError:
|
|
3062
3191
|
_pli_gaps = (0.0, 1.5, 2.0, 3.0)
|
|
3063
3192
|
_bridge_fn._pli_gaps = _pli_gaps
|
|
3193
|
+
# REMB names the VIDEO stream, which is the one to
|
|
3194
|
+
# limit. An earlier version rode the audio Receiver Report
|
|
3195
|
+
# and so named the audio SSRC - it asked the camera to keep
|
|
3196
|
+
# PCMA under the target, which it already was, and left the
|
|
3197
|
+
# video untouched. Measured no change, correctly.
|
|
3198
|
+
#
|
|
3199
|
+
# Own cadence: the PLI timer backs off to 30s once the
|
|
3200
|
+
# stream is up, which is too slow to hold a rate.
|
|
3201
|
+
if (REMB_TARGET_BPS > 0
|
|
3202
|
+
and hasattr(_bridge_fn, '_cam_video_ssrc')
|
|
3203
|
+
and hasattr(_bridge_fn, '_cam_srtp_sock')
|
|
3204
|
+
and _time_br.time() - getattr(
|
|
3205
|
+
_bridge_fn, '_last_remb_ts', 0.0) >= 1.0):
|
|
3206
|
+
_bridge_fn._last_remb_ts = _time_br.time()
|
|
3207
|
+
try:
|
|
3208
|
+
_remb_raw = build_remb(
|
|
3209
|
+
0xAB12CD34,
|
|
3210
|
+
[_bridge_fn._cam_video_ssrc],
|
|
3211
|
+
REMB_TARGET_BPS)
|
|
3212
|
+
_remb_sess = getattr(_bridge_fn, '_pli_tx_sess', None)
|
|
3213
|
+
_bridge_fn._cam_srtp_sock.sendto(
|
|
3214
|
+
_remb_sess.protect_rtcp(_remb_raw)
|
|
3215
|
+
if _remb_sess is not None else _remb_raw,
|
|
3216
|
+
_bridge_fn._cam_srtp_src,
|
|
3217
|
+
)
|
|
3218
|
+
if not getattr(_bridge_fn, '_remb_logged', False):
|
|
3219
|
+
_bridge_fn._remb_logged = True
|
|
3220
|
+
_status(
|
|
3221
|
+
f"SDES: sent REMB {REMB_TARGET_BPS // 1000} kbps"
|
|
3222
|
+
f" for video SSRC"
|
|
3223
|
+
f" 0x{_bridge_fn._cam_video_ssrc:08x}")
|
|
3224
|
+
except Exception:
|
|
3225
|
+
_LOGGER.debug(
|
|
3226
|
+
"camera %s: swallowed exception in %s",
|
|
3227
|
+
getattr(self, "device_id", "?"), '_remb',
|
|
3228
|
+
exc_info=True)
|
|
3229
|
+
|
|
3064
3230
|
_pli_done = getattr(_bridge_fn, '_pli_count', 0)
|
|
3065
3231
|
_pli_interval = (_pli_gaps[_pli_done]
|
|
3066
3232
|
if _pli_done < len(_pli_gaps) else 30.0)
|
|
@@ -4123,6 +4289,14 @@ class _SdesOpenMixin:
|
|
|
4123
4289
|
_br_first_video_logged = True
|
|
4124
4290
|
_first_video_pt[0] = _pt
|
|
4125
4291
|
_status(f"bridge: first video RTP pt={_pt}")
|
|
4292
|
+
# At INFO, not DEBUG: this is the one line that
|
|
4293
|
+
# makes a bitrate figure comparable to another
|
|
4294
|
+
# one, and it is emitted once per session.
|
|
4295
|
+
_LOGGER.info(
|
|
4296
|
+
"camera %s: video profile %s",
|
|
4297
|
+
getattr(self, "device_id", "?"),
|
|
4298
|
+
describe_video_profile(_pt),
|
|
4299
|
+
)
|
|
4126
4300
|
# Camera answers BUNDLE (all media on one 5-tuple),
|
|
4127
4301
|
# so the talk destination is the same address as
|
|
4128
4302
|
# video. Capture it here too - the camera may send
|
|
@@ -4149,6 +4323,12 @@ class _SdesOpenMixin:
|
|
|
4149
4323
|
# from TUTK SFrames to standard SRTP, so we decrypt here
|
|
4150
4324
|
# before forwarding plain RTP to ffmpeg.
|
|
4151
4325
|
_fwd_pkt = _bpkt
|
|
4326
|
+
# Whether _fwd_pkt is plaintext by the time anything
|
|
4327
|
+
# below reads its PAYLOAD. False until something has
|
|
4328
|
+
# actually decrypted it: forwarding ciphertext is
|
|
4329
|
+
# inert (ffmpeg discards it), but MINING it is not -
|
|
4330
|
+
# see _should_capture_sprop.
|
|
4331
|
+
_decrypted = False
|
|
4152
4332
|
if _use_plain_rtp:
|
|
4153
4333
|
if not hasattr(_bridge_fn, '_srtp_rx_sess'):
|
|
4154
4334
|
_bridge_fn._srtp_rx_sess = None
|
|
@@ -4173,6 +4353,7 @@ class _SdesOpenMixin:
|
|
|
4173
4353
|
if _bridge_fn._srtp_rx_sess is not None:
|
|
4174
4354
|
try:
|
|
4175
4355
|
_fwd_pkt = _bridge_fn._srtp_rx_sess.unprotect(_bpkt)
|
|
4356
|
+
_decrypted = True
|
|
4176
4357
|
except Exception as _srx_dec_e:
|
|
4177
4358
|
_ec = getattr(
|
|
4178
4359
|
_bridge_fn, '_decrypt_err_n', 0)
|
|
@@ -4195,8 +4376,9 @@ class _SdesOpenMixin:
|
|
|
4195
4376
|
# can inject sprop-parameter-sets (out-of-band decoder
|
|
4196
4377
|
# init, robust to in-band SPS loss). Parses only until
|
|
4197
4378
|
# both are seen; then _sprop_done short-circuits.
|
|
4198
|
-
if (
|
|
4199
|
-
|
|
4379
|
+
if _should_capture_sprop(
|
|
4380
|
+
_kind, _decrypted,
|
|
4381
|
+
getattr(_bridge_fn, "_sprop_done", False)):
|
|
4200
4382
|
_ps = _extract_param_sets_from_rtp(_fwd_pkt)
|
|
4201
4383
|
if _ps:
|
|
4202
4384
|
_psc = getattr(_bridge_fn, "_ps_cache", None)
|
{python_aidot_cameras-0.17.0 → python_aidot_cameras-0.17.2}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
@@ -145,6 +145,43 @@ async def _wait_or_event(ev: "asyncio.Event", timeout: float) -> bool:
|
|
|
145
145
|
return False
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def _is_camera_present_signal(topic, msg, device_id):
|
|
149
|
+
"""Whether this inbound MQTT message releases the pre-offer wake wait.
|
|
150
|
+
|
|
151
|
+
Two of these clauses recognise the camera's own device-channel traffic and
|
|
152
|
+
were missing. The client subscribes to iot/v1/cb/{device_id}/# but only
|
|
153
|
+
ever checked iot/v1/c/{device_id}/ ("c", no "b"), so no device-channel
|
|
154
|
+
topic matched; and wakeupStatus - the camera announcing it is awake -
|
|
155
|
+
carries no devId at top level or in payload, identifying itself only via
|
|
156
|
+
srcAddr ("2.{device_id}"), which nothing read. Both additions are strictly
|
|
157
|
+
additive: they can only recognise more real camera traffic.
|
|
158
|
+
|
|
159
|
+
lowPowerActiveStateResp stays sufficient on its own, deliberately. It is a
|
|
160
|
+
cloud ack, not the camera: published by the server (clientId "server-..."),
|
|
161
|
+
addressed to the user channel, carrying no devId, and arriving in ~60 ms
|
|
162
|
+
whatever the camera is doing. Waiting for the camera instead was tried and
|
|
163
|
+
rejected - the official app does not do it. In the decompiled client,
|
|
164
|
+
DeviceWakeUpRepos.wakeUpOrSleep fires the MQTT wake fire-and-forget (its
|
|
165
|
+
handler has no emitter at all) and completes the wake step from the HTTP
|
|
166
|
+
lowPowerActiveState response: DeviceWakeUpRepos$1.onSuccess calls
|
|
167
|
+
onNext + onComplete on BOTH the valid and invalid branches. The string
|
|
168
|
+
"wakeupStatus" does not appear anywhere in the app. So publishing the
|
|
169
|
+
offer on a cloud ack is app behaviour, and holding a battery camera back
|
|
170
|
+
for device evidence would be a new, unvalidated divergence on the shared
|
|
171
|
+
open path.
|
|
172
|
+
"""
|
|
173
|
+
inner = msg.get("payload") or {}
|
|
174
|
+
return bool(
|
|
175
|
+
topic.startswith(f"iot/v1/cb/{device_id}/")
|
|
176
|
+
or topic.startswith(f"iot/v1/c/{device_id}/")
|
|
177
|
+
or topic.startswith(f"lds/v1/cb/{device_id}/")
|
|
178
|
+
or inner.get("devId") == device_id
|
|
179
|
+
or msg.get("devId") == device_id
|
|
180
|
+
or str(msg.get("srcAddr") or "").endswith(device_id)
|
|
181
|
+
or (msg.get("method") or "") == "lowPowerActiveStateResp"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
148
185
|
class _WebRTCOpenMixin:
|
|
149
186
|
async def _async_open_webrtc_stream_impl(
|
|
150
187
|
self,
|
|
@@ -668,17 +705,10 @@ class _WebRTCOpenMixin:
|
|
|
668
705
|
return
|
|
669
706
|
method = msg.get("method") or ""
|
|
670
707
|
inner = msg.get("payload") or {}
|
|
671
|
-
# Fire camera_ready_ev the moment the camera appears on MQTT
|
|
672
|
-
#
|
|
673
|
-
#
|
|
674
|
-
|
|
675
|
-
# rather than the device channel, so the old topic-prefix check never fired -
|
|
676
|
-
# causing the 17-second getIceConfigReq timeout overhead every session.
|
|
677
|
-
if (method == "lowPowerActiveStateResp"
|
|
678
|
-
or topic.startswith(f"iot/v1/c/{device_id}/")
|
|
679
|
-
or topic.startswith(f"lds/v1/cb/{device_id}/")
|
|
680
|
-
or inner.get("devId") == device_id
|
|
681
|
-
or msg.get("devId") == device_id):
|
|
708
|
+
# Fire camera_ready_ev the moment the camera appears on MQTT. See
|
|
709
|
+
# _is_camera_present_signal for what counts as evidence and why the
|
|
710
|
+
# server's wake-ACK is not enough for a battery camera.
|
|
711
|
+
if _is_camera_present_signal(topic, msg, device_id):
|
|
682
712
|
loop.call_soon_threadsafe(camera_ready_ev.set)
|
|
683
713
|
# livePlayResp: explicit camera ack/nack for start-play command.
|
|
684
714
|
# The camera echoes our peer_id (verified live); its payload has NO
|