python-aidot-cameras 0.12.16__tar.gz → 0.12.17__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.
Files changed (153) hide show
  1. {python_aidot_cameras-0.12.16/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.17}/PKG-INFO +3 -3
  2. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/README.md +2 -2
  3. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/client.py +253 -20
  4. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/constants.py +8 -0
  5. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/sdes_open.py +509 -24
  6. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/webrtc_open.py +64 -29
  7. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/discover.py +2 -2
  8. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/pyproject.toml +3 -2
  9. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17/python_aidot_cameras.egg-info}/PKG-INFO +3 -3
  10. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/SOURCES.txt +4 -1
  11. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_aioice_compat.py +2 -2
  12. python_aidot_cameras-0.12.17/tests/test_battery_detection.py +206 -0
  13. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_device_login_guard.py +3 -3
  14. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_mux_terminal_write.py +2 -1
  15. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_egress_guard.py +3 -3
  16. python_aidot_cameras-0.12.17/tests/test_live_stream_param.py +145 -0
  17. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_no_self_referential_go2rtc.py +2 -2
  18. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_session_stats.py +3 -3
  19. python_aidot_cameras-0.12.17/tests/test_turn_create_permission.py +291 -0
  20. python_aidot_cameras-0.12.17/tests/test_wake_readiness_retry.py +167 -0
  21. python_aidot_cameras-0.12.16/tests/test_live_stream_param.py +0 -123
  22. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/LICENSE +0 -0
  23. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/MANIFEST.in +0 -0
  24. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/__init__.py +0 -0
  25. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/__main__.py +0 -0
  26. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/__init__.py +0 -0
  27. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  28. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  29. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  30. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  31. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  32. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  33. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  34. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  35. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  36. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  37. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  38. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  39. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  40. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  41. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  42. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  43. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  44. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  45. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  46. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  47. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  48. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  49. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  50. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  51. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  52. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  53. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  54. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  55. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  56. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  57. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  58. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  59. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/__init__.py +0 -0
  60. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/controls.py +0 -0
  61. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/go2rtc.py +0 -0
  62. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/lan_control.py +0 -0
  63. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/models.py +0 -0
  64. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/playback.py +0 -0
  65. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/protocol.py +0 -0
  66. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/sdes.py +0 -0
  67. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/tutk.py +0 -0
  68. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/camera/webrtc.py +0 -0
  69. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/client.py +0 -0
  70. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/const.py +0 -0
  71. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/credentials.py +0 -0
  72. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/crypto.py +0 -0
  73. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/device_client.py +0 -0
  74. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/exceptions.py +0 -0
  75. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/g711.py +0 -0
  76. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/aidot_cameras/py.typed +0 -0
  77. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  78. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  79. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/requires.txt +0 -0
  80. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  81. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/setup.cfg +0 -0
  82. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_alarm_event.py +0 -0
  83. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_backoff.py +0 -0
  84. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_battery_skip_turn.py +0 -0
  85. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_deferred_hardening.py +0 -0
  86. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_device_user_info_cache.py +0 -0
  87. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dormant_camera_cleanup.py +0 -0
  88. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_not_ready_burst.py +0 -0
  89. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_pinning.py +0 -0
  90. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_serve_decode_skip.py +0 -0
  91. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_serve_open_timeout.py +0 -0
  92. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_skip_signaling_wait.py +0 -0
  93. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_dtls_slow_probe.py +0 -0
  94. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_go2rtc.py +0 -0
  95. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_go2rtc_cli.py +0 -0
  96. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_h264_decode_rate_limit.py +0 -0
  97. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_highport_nomination.py +0 -0
  98. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_ice_config_cache.py +0 -0
  99. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_idle_release_both_loops.py +0 -0
  100. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_keepalive_renew.py +0 -0
  101. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_key_restart_sdp.py +0 -0
  102. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_keyframe_prompter.py +0 -0
  103. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_lan_control.py +0 -0
  104. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_light_active_color_mode.py +0 -0
  105. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_logging_caps.py +0 -0
  106. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_motion_poll.py +0 -0
  107. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_credential_refetch.py +0 -0
  108. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_credential_self_heal.py +0 -0
  109. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_password_not_persisted.py +0 -0
  110. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_mqtt_session_reconnect.py +0 -0
  111. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_narrow_pc_ice.py +0 -0
  112. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_no_undefined_names.py +0 -0
  113. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_offline_keepalive_pause.py +0 -0
  114. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_open_fail_logger.py +0 -0
  115. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_open_gate_delay.py +0 -0
  116. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_peerid_reuse.py +0 -0
  117. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_persistent_mqtt.py +0 -0
  118. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_persistent_mqtt_robustness.py +0 -0
  119. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_playback_tls.py +0 -0
  120. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_post_merge_hardening.py +0 -0
  121. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_retry_policy.py +0 -0
  122. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_adaptive.py +0 -0
  123. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_bridge_break_gate.py +0 -0
  124. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_echo_wait_timeout.py +0 -0
  125. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_fast_liveplay.py +0 -0
  126. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  127. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_idle_release.py +0 -0
  128. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_open_cleanup.py +0 -0
  129. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_serve_audio.py +0 -0
  130. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_serve_cmd.py +0 -0
  131. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_sprop.py +0 -0
  132. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_talk.py +0 -0
  133. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_teardown_exit_log.py +0 -0
  134. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_terminal_ack.py +0 -0
  135. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdes_watchdog.py +0 -0
  136. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_sdp_payload_type_narrowing.py +0 -0
  137. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_security_hardening.py +0 -0
  138. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_serializable_login_info.py +0 -0
  139. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_serve_relay.py +0 -0
  140. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_speak.py +0 -0
  141. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_cap.py +0 -0
  142. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_drain_ownership.py +0 -0
  143. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_idle.py +0 -0
  144. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_slot_cap.py +0 -0
  145. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_stream_teardown.py +0 -0
  146. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_talk.py +0 -0
  147. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_terminal_ack.py +0 -0
  148. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_token_file_robustness.py +0 -0
  149. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_token_refresh.py +0 -0
  150. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_upstream_compat.py +0 -0
  151. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_viewer_check_throttle.py +0 -0
  152. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_viewer_presence.py +0 -0
  153. {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.12.17}/tests/test_wait_or_event.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.12.16
3
+ Version: 0.12.17
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
@@ -171,12 +171,12 @@ audio, idle release, the sprop cache path) are documented in
171
171
  | `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
172
172
  | `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
173
173
  | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
174
- | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
174
+ | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
175
175
  | `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) |
176
176
  | `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) |
177
177
  | `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) |
178
178
  | `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) |
179
- | `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
179
+ | `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 |
180
180
 
181
181
  ### Security hardening
182
182
 
@@ -140,12 +140,12 @@ audio, idle release, the sprop cache path) are documented in
140
140
  | `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
141
141
  | `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
142
142
  | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
143
- | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
143
+ | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
144
144
  | `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) |
145
145
  | `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) |
146
146
  | `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) |
147
147
  | `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) |
148
- | `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
148
+ | `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 |
149
149
 
150
150
  ### Security hardening
151
151
 
@@ -21,6 +21,7 @@ from ..const import (
21
21
  # in-module references (and any external importers) keep resolving.
22
22
  from .constants import (
23
23
  _LEEDARSON_APP_ID,
24
+ _LIVE_PLAY_NOT_READY,
24
25
  _SMARTHOME_URL_TEMPLATE,
25
26
  GETSTREAMCTRL_CMD, # noqa: F401 - re-exported for back-compat (public pair; unused in-module)
26
27
  SDES_SPEAKERSTART_DELAY, # noqa: F401 - used by the sdes_open mixin; kept re-exported
@@ -33,6 +34,7 @@ from .models import ( # re-exported: data models split into models.py
33
34
  CameraDeviceInformation,
34
35
  CameraStatusData,
35
36
  VideoFrame,
37
+ _as_int,
36
38
  )
37
39
  from .tutk import TutkStreamSession # re-exported (split into tutk.py)
38
40
  from .playback import ( # re-exported (split into playback.py)
@@ -906,6 +908,15 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
906
908
  # A001513 battery cameras). Exclude role-reversal models from the flag.
907
909
  _NO_FAST_LIVEPLAY_MODELS: frozenset = frozenset({"LK.IPC.A001064"})
908
910
 
911
+ # Models confirmed (2026-05-02) to send TUTK-framed data instead of standard
912
+ # SRTP: the camera announces an ASCII-printable fake SDES key and sends
913
+ # packets with a TUTK SFrame header (0xC8 audio / 0xC9 video) and an SSRC
914
+ # that differs from the one advertised in the SDP. For these the ffmpeg SDP
915
+ # uses plain RTP/AVP (no SRTP decryption attempt) and the bridge strips the
916
+ # TUTK header before forwarding. Matched by SUBSTRING (see _use_plain_rtp in
917
+ # sdes_open) so a revision suffix is covered.
918
+ _PLAIN_RTP_MODELS: frozenset = frozenset({"A001064", "A001513"})
919
+
909
920
  @property
910
921
  def _offer_should_include_datachannel(self) -> bool:
911
922
  model = getattr(getattr(self, "info", None), "model_id", None)
@@ -917,13 +928,111 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
917
928
  props = getattr(self, "_raw_device", {}).get("properties") or {}
918
929
  return str(props.get("enableSdes", "0")) == "1"
919
930
 
931
+ # Battery / low-power camera models. ONE list, consumed by
932
+ # is_battery_camera and (through it) by every battery guard and by the
933
+ # livePlayReq/webrtcReq ``powerType`` field - it used to be re-typed inline in
934
+ # three files, which is how a guard drifts out of step with the wire payload.
935
+ # Source: IpcServiceImpl.java B() - powerType=2 for these models, 1 for wired.
936
+ # Substring-matched, so a firmware/hardware revision suffix
937
+ # ("LK.IPC.A001513-1") is covered the way A000088-1 already is on the DTLS
938
+ # side rather than silently reading as a different, mains-powered model.
939
+ _BATTERY_MODELS: frozenset = frozenset({"A001513", "A001108", "A001360"})
940
+
941
+ def _battery_evidence(self) -> "Optional[bool]":
942
+ """Does this camera's OWN cloud data say it runs on a battery?
943
+
944
+ True when the cloud reports a battery for it, else None ("no evidence" -
945
+ NOT "mains", since the device-list payload may simply not carry the field
946
+ yet). Same signal ``lan_control.is_mains_powered`` inverts: a camera that
947
+ reports a battery level has a battery. Never raises - it is read from
948
+ hot paths - and never returns False, so it can only ever ADD a camera to
949
+ the battery set, never take a known model out of it.
950
+ """
951
+ _raw = getattr(self, "_raw_device", None)
952
+ _props = _raw.get("properties") if isinstance(_raw, dict) else None
953
+ if not isinstance(_props, dict):
954
+ _props = {}
955
+ # A reported battery level means there is a battery to report.
956
+ for _key in ("Battery_remaining", "batteryRemaining", "batteryLevel"):
957
+ if _as_int(_props.get(_key)) is not None:
958
+ return True
959
+ # The low-power flag: batteryMode=2 is what a camera that sleeps between
960
+ # sessions reports (see _async_send_camera_command). Deliberately NOT
961
+ # keyed on ``powerType``: that name is also the value we SEND in
962
+ # livePlayReq, we have never observed the cloud returning it as a device
963
+ # property, and p2pCache - the field right next to it - reads 2 on every
964
+ # camera including mains ones. Reading it here would risk classifying a
965
+ # mains camera as battery, which would put powerType=2 on ITS wire payload
966
+ # and can leave an A000088 un-armed. Evidence has to be unambiguous.
967
+ if _as_int(_props.get("batteryMode")) == 2:
968
+ return True
969
+ # Already-parsed status, for a camera refreshed from an attribute push
970
+ # rather than from a device-list dict.
971
+ _status = getattr(self, "status", None)
972
+ if _as_int(getattr(_status, "battery_remaining", None)) is not None:
973
+ return True
974
+ return None
975
+
920
976
  @property
921
977
  def is_battery_camera(self) -> bool:
922
- """True for battery/low-power models (A001513, A001108, A001360).
923
- Source: IpcServiceImpl.java B() - powerType=2 for these models.
978
+ """True for battery/low-power cameras (powerType=2 in the app's B()).
979
+
980
+ Resolved from the camera's own cloud data FIRST (see
981
+ [[_battery_evidence]]), then from the known-model list
982
+ ([[_BATTERY_MODELS]]). Evidence leads because a hardcoded list can only
983
+ ever name the revisions we have already seen, and a battery camera that
984
+ is not recognized as one loses every battery protection at once: the SDES
985
+ TURN relay pre-allocation (its ONLY return path - a camera woken through
986
+ the cloud has no LAN host candidate), the cloud keep-alive renew, the
987
+ HTTP wake, and the powerType=2 the camera is told to expect.
988
+
989
+ That misdetection presents as "streams from a standalone run but never
990
+ under Home Assistant", because HA is where the LAN-direct options that
991
+ strip the relay actually get set - a one-off script leaves them at their
992
+ (relay-keeping) defaults, so it never trips the guard it is missing.
924
993
  """
994
+ if self._battery_evidence():
995
+ return True
925
996
  model = getattr(getattr(self, "info", None), "model_id", None) or ""
926
- return any(m in model for m in ("A001513", "A001108", "A001360"))
997
+ return any(m in model for m in self._BATTERY_MODELS)
998
+
999
+ @property
1000
+ def live_power_type(self) -> int:
1001
+ """The ``powerType`` field for livePlayReq / webrtcReq: 2 battery, 1 wired.
1002
+
1003
+ Source: IpcServiceImpl.java B(). Derived from [[is_battery_camera]] so
1004
+ the value on the wire and the battery guards can never disagree.
1005
+ """
1006
+ return 2 if self.is_battery_camera else 1
1007
+
1008
+ def _note_live_play_resp(self, payload) -> None:
1009
+ """Record the camera's livePlayResp code for this open (loop thread).
1010
+
1011
+ Called from the signaling dispatcher for every livePlayResp matched to
1012
+ this open, whether or not anything is waiting on the future - which
1013
+ matters because ``sdes_fast_liveplay`` is on by default and skips that
1014
+ wait, so this is the only place the code is seen on the SDES path.
1015
+ Best-effort: a malformed payload leaves the record untouched rather than
1016
+ raising into the dispatcher.
1017
+ """
1018
+ try:
1019
+ self._last_live_play_code = _as_int(payload.get("code"))
1020
+ except (AttributeError, TypeError):
1021
+ _LOGGER.debug("camera %s: unparseable livePlayResp payload",
1022
+ getattr(self, "device_id", "?"), exc_info=True)
1023
+
1024
+ def _live_play_not_ready(self) -> bool:
1025
+ """Did this open's livePlayResp say the camera is not ready to stream?
1026
+
1027
+ ``-50019`` is NOT a refusal on its own - a camera emits it and then
1028
+ recovers via ICE, which is why 0.12.15 stopped treating it as fatal and
1029
+ why nothing here aborts an open on it. Its value is post-hoc: pair it
1030
+ with "the session then delivered no media" and you have a waking battery
1031
+ camera that was not ready yet, as distinct from a generic failure. Only
1032
+ the retry delay keys on it (see the SDES keepalive loop) - never the
1033
+ decision to proceed.
1034
+ """
1035
+ return getattr(self, "_last_live_play_code", None) == _LIVE_PLAY_NOT_READY
927
1036
 
928
1037
  def _next_dseq(self) -> int:
929
1038
  """Next livePlayReq dseq (app parity: Q0() starts at 100, increments)."""
@@ -1204,15 +1313,19 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
1204
1313
  async def _async_fetch_live_stream_param(self) -> bool:
1205
1314
  """Provision the camera's live-stream session via the cloud (app parity).
1206
1315
 
1207
- Battery cameras waking from deep sleep reject the MQTT ``livePlayReq``
1208
- with code ``-50019`` ("not ready") and then never run ICE - so no media
1209
- ever flows - unless the app's pre-connect HTTP call has armed the session
1210
- first. The official app (``KVSPreConnectStrategy.fetchKvsParams``) POSTs
1211
- the device id to ``/api/ipc/liveStream/liveStreamParam``; the cloud
1212
- provisions the session (and brings the camera online) and returns AWS KVS
1213
- credentials. We only need the provisioning side effect - the library
1214
- streams over the proprietary MQTT/SDES path, not AWS KVS - so the returned
1215
- credentials are ignored. Best-effort: returns True on HTTP 200.
1316
+ **Not called** - [[_resolve_live_stream_param]] gates this and always says
1317
+ no; see there for why. Kept, with its request shape locked down by
1318
+ tests/test_live_stream_param.py, so re-enabling it for a camera that
1319
+ demonstrably needs it is a one-line change rather than a rediscovery.
1320
+
1321
+ The official app (``KVSPreConnectStrategy.fetchKvsParams``) POSTs the
1322
+ device id to ``/api/ipc/liveStream/liveStreamParam``; the cloud provisions
1323
+ the session (and brings the camera online) and returns AWS KVS
1324
+ credentials. Only the provisioning side effect was ever wanted here - the
1325
+ library streams over the proprietary MQTT/SDES path, not AWS KVS - but
1326
+ that side effect is precisely the problem: the camera then sends its media
1327
+ toward KVS and our bridge receives no video RTP. Best-effort: returns True
1328
+ on HTTP 200.
1216
1329
  """
1217
1330
  url, headers, body = self._live_stream_param_request()
1218
1331
  return await self._async_post_ok(url, headers, body, label="liveStreamParam")
@@ -2700,12 +2813,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
2700
2813
  to the ``AIDOT_FAST_CONNECT`` env var; ``True``/``False`` override it (e.g.
2701
2814
  from a Home Assistant config-entry option, since HA OS can't set env vars).
2702
2815
 
2703
- ``live_stream_param`` toggles the battery-camera cloud pre-connect
2704
- (liveStreamParam), which is **off by default**: it provisions the camera
2705
- toward AWS KVS and diverts an A001513's media away from the SDES bridge
2706
- (no video RTP arrives). Like ``fast_connect`` it overrides the
2707
- ``AIDOT_LIVESTREAM_PARAM`` env var; set True only to re-enable the
2708
- pre-connect for a camera that needs it.
2816
+ ``live_stream_param`` is **accepted but ignored** (a no-op kept so an
2817
+ existing caller doesn't break; it logs one warning if set). It used to
2818
+ request the battery-camera cloud pre-connect, which provisions the camera
2819
+ toward AWS KVS so its media goes there instead of to the SDES bridge - the
2820
+ session negotiates and then serves no video at all. Battery cameras were
2821
+ the only ones it applied to, i.e. exactly the ones it breaks, so the
2822
+ decision is now made once in ``_resolve_live_stream_param`` and is "no".
2823
+ Stop passing it; live video does not depend on it.
2709
2824
 
2710
2825
  ``serve_relay`` toggles the cold-start serve-port relay (holds the public
2711
2826
  serve port connectable through the WebRTC handshake so an eager go2rtc
@@ -3071,6 +3186,19 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3071
3186
  sdes=True)
3072
3187
  _peer_reuses = 0
3073
3188
 
3189
+ # Wake-readiness retries. A battery camera that answers livePlayResp with
3190
+ # -50019 and then sends no media was not refusing - it had not finished
3191
+ # waking. The pacer cannot tell that apart from a degraded camera, so it
3192
+ # escalated the backoff (10s -> 300s) on a camera that would have been
3193
+ # ready seconds later; under a consumer that re-opens per view, that is the
3194
+ # difference between a slow first frame and a live view that never fills
3195
+ # in. Give it the same bounded fast burst the DTLS serve loop already has
3196
+ # (_retry_policy), then fall back to the pacer so a persistently-cold
3197
+ # camera is still not hammered. Bounded to _PEERID_MAX_REUSE so the whole
3198
+ # burst stays on ONE peerid: a fresh peerid registers another camera-side
3199
+ # session, which is what wedged the L2 in the first place (0.12.16).
3200
+ _not_ready_burst = 0
3201
+
3074
3202
  while self._streaming_active:
3075
3203
  if self._serve_relay is not None:
3076
3204
  # Clear any stale backend from a prior session; the open below
@@ -3257,8 +3385,23 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3257
3385
  # (camera refused / degraded on a rapid reconnect); a session
3258
3386
  # that streamed fine and then ended (battery teardown, consumer
3259
3387
  # gone) is a normal lifecycle event and resets to the base interval.
3388
+ # Exception: a battery camera that answered -50019 and then sent
3389
+ # nothing was still waking, not degraded - retry it fast instead.
3390
+ _not_ready_burst = self._next_not_ready_burst(
3391
+ _healthy, _not_ready_burst)
3392
+ _delay, _fast_retry = self._not_ready_retry_delay(
3393
+ _not_ready_burst, burst_max=_PEERID_MAX_REUSE)
3394
+ if _fast_retry:
3395
+ _LOGGER.info(
3396
+ "SDES %s: camera not ready (waking, livePlayResp %d) and "
3397
+ "sent no media - fast retry in %.0fs [%d/%d]",
3398
+ self.device_id, _LIVE_PLAY_NOT_READY, _delay,
3399
+ _not_ready_burst, _PEERID_MAX_REUSE,
3400
+ )
3401
+ else:
3402
+ _delay = _pacer.session_end_delay(healthy=_healthy)
3260
3403
  try:
3261
- await asyncio.sleep(_pacer.session_end_delay(healthy=_healthy))
3404
+ await asyncio.sleep(_delay)
3262
3405
  except asyncio.CancelledError:
3263
3406
  return
3264
3407
 
@@ -3609,6 +3752,54 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3609
3752
  except (ValueError, TypeError):
3610
3753
  return -8.0
3611
3754
 
3755
+ def _resolve_live_stream_param(self) -> bool:
3756
+ """Whether to make the cloud ``liveStreamParam`` KVS pre-connect before
3757
+ signaling. **Always False** - see below; the flag survives only so an
3758
+ existing caller doesn't break.
3759
+
3760
+ The call provisions the camera's live session toward AWS KVS, and the
3761
+ camera then sends its media THERE instead of to our SDES bridge: the
3762
+ session negotiates, the bridge binds, and no video RTP ever arrives. It
3763
+ is also made for battery cameras ONLY, so the set of cameras it can
3764
+ affect is exactly the set it is known to break. It was added to cure a
3765
+ ``-50019`` ("not ready") livePlayResp, which 0.12.15 established is
3766
+ benign - mains cameras emit it too and recover via ICE.
3767
+
3768
+ 0.12.15 turned it off by default but left the option/env able to turn it
3769
+ back on, which is a foot-gun aimed at one foot: a consumer that surfaces
3770
+ it as a setting (the Home Assistant integration surfaces these options
3771
+ because HA OS cannot set env vars) can re-break every battery camera on
3772
+ the account, and the symptom - a live view that negotiates and then shows
3773
+ nothing, forever - looks nothing like a settings problem. A standalone
3774
+ run never passes the option, which is exactly why the same library
3775
+ streams an L2 from the CLI and not under HA.
3776
+
3777
+ So the decision now lives here, in one place, and is "no". Re-enabling
3778
+ it needs a code change plus fresh on-hardware evidence that some camera
3779
+ actually requires it - not a flag flip. A caller that still asks gets
3780
+ one warning naming the camera, so a stuck install says why in the log
3781
+ instead of silently serving nothing.
3782
+ """
3783
+ if not self.is_battery_camera:
3784
+ # The pre-connect was never made for a mains camera in the first
3785
+ # place; nothing to decide.
3786
+ return False
3787
+ asked = getattr(self, "_live_stream_param_opt", None)
3788
+ if asked is None:
3789
+ asked = os.environ.get("AIDOT_LIVESTREAM_PARAM", "0") != "0"
3790
+ if asked and not getattr(self, "_live_stream_param_warned", False):
3791
+ self._live_stream_param_warned = True
3792
+ _LOGGER.warning(
3793
+ "camera %s: ignoring the liveStreamParam pre-connect request "
3794
+ "(live_stream_param / AIDOT_LIVESTREAM_PARAM). On a battery "
3795
+ "camera it provisions the session toward AWS KVS and the camera "
3796
+ "sends its media there instead of to this library, so the live "
3797
+ "view negotiates and then serves no video at all. The setting "
3798
+ "is a no-op - remove it; live video does not depend on it.",
3799
+ self.device_id,
3800
+ )
3801
+ return False
3802
+
3612
3803
  def _resolve_sdes_skip_turn(self) -> bool:
3613
3804
  """EXPERIMENTAL (opt-in, default off): skip the blocking SDES TURN relay
3614
3805
  pre-allocation, for cameras reachable LAN-direct.
@@ -3665,7 +3856,20 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3665
3856
  ``AIDOT_SDES_ADAPTIVE`` env (truthy = 1/true/yes/on), default off. When
3666
3857
  off, the per-attempt override is never set, so the explicit
3667
3858
  ``sdes_fast_liveplay`` / ``sdes_skip_turn`` opts (or their envs) decide -
3668
- exactly the pre-adaptive behaviour."""
3859
+ exactly the pre-adaptive behaviour.
3860
+
3861
+ NEVER on for a battery camera, whatever the opt/env say - for those it is
3862
+ all cost and no benefit. The saving adaptive chases is the TURN relay
3863
+ pre-allocation, and [[_resolve_sdes_skip_turn]] force-KEEPS that for a
3864
+ battery camera (it is the only return path to a cloud-woken camera), while
3865
+ fast-liveplay is already on by default. So a battery "fast" attempt runs
3866
+ the very same handshake as the patient one and differs only in being given
3867
+ 45 s to open and a 40 s media grace - inside the documented 25-70 s battery
3868
+ cold-start window. A slow-but-healthy wake is then scored as a fast-path
3869
+ failure: it latches ``_fast_path_unavailable``, escalates the backoff, and
3870
+ burns a camera-side session on a device that frees them slowly."""
3871
+ if getattr(self, "is_battery_camera", False):
3872
+ return False
3669
3873
  opt = getattr(self, "_sdes_adaptive_opt", None)
3670
3874
  if opt is not None:
3671
3875
  return bool(opt)
@@ -3789,6 +3993,35 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3789
3993
  adaptive mode is on and the fast path has not already failed this loop."""
3790
3994
  return bool(adaptive) and not bool(fast_failed)
3791
3995
 
3996
+ def _next_not_ready_burst(self, healthy: bool, burst: int) -> int:
3997
+ """Updated wake-readiness burst counter after a session ended.
3998
+
3999
+ Counts up only while a BATTERY camera keeps answering ``-50019`` and then
4000
+ delivering no media; any other outcome resets it to 0. Scoped to battery
4001
+ cameras because they are the only ones with a wake-readiness state - on a
4002
+ mains camera ``-50019`` is transient noise and the pacer's judgement
4003
+ (something is degraded) is the right one.
4004
+ """
4005
+ if healthy or not self.is_battery_camera:
4006
+ return 0
4007
+ return burst + 1 if self._live_play_not_ready() else 0
4008
+
4009
+ @staticmethod
4010
+ def _not_ready_retry_delay(burst: int, *,
4011
+ burst_max: int = 3) -> "tuple[float, bool]":
4012
+ """``(delay, is_fast_retry)`` for a wake-readiness retry.
4013
+
4014
+ Shares [[_retry_policy]] with the DTLS serve loop so both paths agree on
4015
+ what "the camera is still warming up" is worth waiting. Returns
4016
+ ``(0.0, False)`` when the counter is 0 (not a wake-readiness failure) and
4017
+ ``(_, False)`` once the burst is spent, in both cases handing the decision
4018
+ back to the caller's normal pacer.
4019
+ """
4020
+ if burst <= 0:
4021
+ return (0.0, False)
4022
+ delay, fast = _retry_policy("not_ready", burst, burst_max=burst_max)
4023
+ return (delay, bool(fast))
4024
+
3792
4025
  @staticmethod
3793
4026
  def _adaptive_after_attempt(use_fast: bool, healthy: bool, fast_failed: bool) -> bool:
3794
4027
  """Updated ``fast_failed`` after an attempt: latch it once a fast attempt
@@ -88,6 +88,14 @@ GETSTREAMCTRL_CMD = 802
88
88
  _STREAM_QUALITY: dict = {"hd": 1, "sd": 5} # AVIOCTRL_QUALITY_MAX / _MIDDLE
89
89
 
90
90
 
91
+ # livePlayResp code for "not ready": the camera accepted the request but cannot
92
+ # stream yet (a battery camera still waking). Explicitly NOT a refusal - mains
93
+ # cameras emit it too and recover via ICE - so no path aborts an open on it. It
94
+ # is only read after the fact, to tell a waking camera apart from a generically
95
+ # failed session when choosing a retry delay (see _live_play_not_ready).
96
+ _LIVE_PLAY_NOT_READY = -50019
97
+
98
+
91
99
  # Two-way-audio (talk) / SDES timing constants (moved from client.py)
92
100
  TALK_PCM_RATE = 8000
93
101
  TALK_PCM_FRAME_BYTES = 320 # 160 samples (20 ms @ 8 kHz) x 2 bytes (s16)