python-aidot-cameras 1.0.0b33__tar.gz → 1.0.0b34__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 (276) hide show
  1. {python_aidot_cameras-1.0.0b33/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b34}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/client.py +223 -13
  3. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/protocol.py +26 -0
  4. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/sdes.py +14 -0
  5. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/sdes_open.py +250 -29
  6. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/webrtc_open.py +1 -1
  7. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/pyproject.toml +1 -1
  8. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  9. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/python_aidot_cameras.egg-info/SOURCES.txt +6 -0
  10. python_aidot_cameras-1.0.0b34/tests/test_peer_id_client_type.py +231 -0
  11. python_aidot_cameras-1.0.0b34/tests/test_sctp_reassembly.py +102 -0
  12. python_aidot_cameras-1.0.0b34/tests/test_sctp_sack.py +94 -0
  13. python_aidot_cameras-1.0.0b34/tests/test_sdes_ice_teardown.py +73 -0
  14. python_aidot_cameras-1.0.0b34/tests/test_sdes_session_cap.py +118 -0
  15. python_aidot_cameras-1.0.0b34/tests/test_stable_terminal_id.py +64 -0
  16. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/LICENSE +0 -0
  17. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/MANIFEST.in +0 -0
  18. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/README.md +0 -0
  19. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/__init__.py +0 -0
  20. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/__main__.py +0 -0
  21. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_upstream.py +0 -0
  22. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/__init__.py +0 -0
  23. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  24. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  25. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  26. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  27. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  28. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  29. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  30. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  31. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  32. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  33. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  34. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  35. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  36. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  37. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  38. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  39. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  40. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  41. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  42. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  43. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  44. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  45. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  46. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  47. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  48. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  49. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  50. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  51. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  52. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  53. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  54. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  55. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  56. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/ble_gateway.py +0 -0
  57. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/__init__.py +0 -0
  58. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/constants.py +0 -0
  59. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/controls.py +0 -0
  60. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/go2rtc.py +0 -0
  61. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/hwaccel.py +0 -0
  62. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/lan_control.py +0 -0
  63. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/models.py +0 -0
  64. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/playback.py +0 -0
  65. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/sd_events.py +0 -0
  66. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/sd_listing.py +0 -0
  67. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/sd_playback.py +0 -0
  68. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/tutk.py +0 -0
  69. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/camera/webrtc.py +0 -0
  70. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/client.py +0 -0
  71. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/cloud_auth.py +0 -0
  72. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/const.py +0 -0
  73. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/credentials.py +0 -0
  74. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/crypto.py +0 -0
  75. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/device_client.py +0 -0
  76. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/discover.py +0 -0
  77. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/exceptions.py +0 -0
  78. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/g711.py +0 -0
  79. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/lan_retry.py +0 -0
  80. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/aidot_cameras/py.typed +0 -0
  81. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  82. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  83. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/python_aidot_cameras.egg-info/requires.txt +0 -0
  84. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  85. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/setup.cfg +0 -0
  86. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_aioice_compat.py +0 -0
  87. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_alarm_event.py +0 -0
  88. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_answer_pt_kinds.py +0 -0
  89. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_answer_sdp_video_pt.py +0 -0
  90. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_answer_section_selection.py +0 -0
  91. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_audio_mux_offline.py +0 -0
  92. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  93. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_avio_request.py +0 -0
  94. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_avio_response_parse.py +0 -0
  95. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_avio_response_router.py +0 -0
  96. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_backoff.py +0 -0
  97. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_battery_detection.py +0 -0
  98. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_battery_evidence_corroboration.py +0 -0
  99. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_battery_skip_turn.py +0 -0
  100. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_bitrate_campaign_arms.py +0 -0
  101. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_ble_gateway.py +0 -0
  102. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_bridge_state_sentinels.py +0 -0
  103. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_busy_backoff.py +0 -0
  104. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_canary_log_names_the_camera.py +0 -0
  105. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_cloud_count.py +0 -0
  106. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_cloud_plan.py +0 -0
  107. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_constants_imports_resolve.py +0 -0
  108. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_control_verdicts.py +0 -0
  109. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_deferred_hardening.py +0 -0
  110. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_device_cmd_seq_match.py +0 -0
  111. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_device_login_guard.py +0 -0
  112. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_device_user_info_cache.py +0 -0
  113. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_direct_ts_serve.py +0 -0
  114. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dormant_camera_cleanup.py +0 -0
  115. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_drain_is_cancellable.py +0 -0
  116. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_mux_terminal_write.py +0 -0
  117. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_not_ready_burst.py +0 -0
  118. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_pinning.py +0 -0
  119. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_resolution_ack.py +0 -0
  120. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_serve_decode_skip.py +0 -0
  121. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_serve_ice_wait.py +0 -0
  122. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_serve_open_timeout.py +0 -0
  123. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_skip_signaling_wait.py +0 -0
  124. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_slow_probe.py +0 -0
  125. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_stdout_producer.py +0 -0
  126. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_dtls_video_presence.py +0 -0
  127. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_egress_guard.py +0 -0
  128. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_env_seams.py +0 -0
  129. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  130. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  131. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_futile_keepalive.py +0 -0
  132. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  133. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_go2rtc.py +0 -0
  134. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_go2rtc_cli.py +0 -0
  135. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_h264_decode_rate_limit.py +0 -0
  136. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_highport_nomination.py +0 -0
  137. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_hwaccel_probe.py +0 -0
  138. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_ice_config_cache.py +0 -0
  139. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_ice_peer_reflexive.py +0 -0
  140. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_idle_release_both_loops.py +0 -0
  141. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_keepalive_renew.py +0 -0
  142. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_key_restart_sdp.py +0 -0
  143. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_keyframe_prompter.py +0 -0
  144. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_lan_control.py +0 -0
  145. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  146. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_late_repeats_are_dropped.py +0 -0
  147. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_light_active_color_mode.py +0 -0
  148. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_live_session_carries_talk.py +0 -0
  149. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_live_stream_param.py +0 -0
  150. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_live_validate_policy.py +0 -0
  151. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_live_video_canary.py +0 -0
  152. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_local_only_controls.py +0 -0
  153. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_logging_caps.py +0 -0
  154. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_login_connect_deadline.py +0 -0
  155. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_login_reconnect_backoff.py +0 -0
  156. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_media_counted_only_when_plaintext.py +0 -0
  157. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_media_path_is_reported.py +0 -0
  158. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_media_socket_rcvbuf.py +0 -0
  159. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_media_wait_floor.py +0 -0
  160. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_motion_poll.py +0 -0
  161. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_mqtt_client_id_is_exact.py +0 -0
  162. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_mqtt_credential_refetch.py +0 -0
  163. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_mqtt_credential_self_heal.py +0 -0
  164. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_mqtt_password_not_persisted.py +0 -0
  165. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_mqtt_session_reconnect.py +0 -0
  166. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  167. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_nack.py +0 -0
  168. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  169. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_narrow_pc_ice.py +0 -0
  170. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_no_key_material_in_logs.py +0 -0
  171. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_no_self_referential_go2rtc.py +0 -0
  172. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_no_undefined_names.py +0 -0
  173. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_offer_bandwidth_line.py +0 -0
  174. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  175. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_offline_keepalive_pause.py +0 -0
  176. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_on_frame_is_an_av_frame.py +0 -0
  177. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_open_fail_logger.py +0 -0
  178. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_open_gate_delay.py +0 -0
  179. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_peerid_reuse.py +0 -0
  180. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_persistent_mqtt.py +0 -0
  181. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_persistent_mqtt_robustness.py +0 -0
  182. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_playback_tls.py +0 -0
  183. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_post_merge_hardening.py +0 -0
  184. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  185. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_public_signature_matches_impl.py +0 -0
  186. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_quality_campaign_in_session.py +0 -0
  187. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_recent_recordings.py +0 -0
  188. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_remb.py +0 -0
  189. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  190. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_resent_frames_are_dropped.py +0 -0
  191. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_resolution_persists.py +0 -0
  192. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_restart_cycle.py +0 -0
  193. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_retry_policy.py +0 -0
  194. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_scripts_importable.py +0 -0
  195. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sctp_init_ack_tsn.py +0 -0
  196. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sd_card_presence.py +0 -0
  197. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sd_event_decode.py +0 -0
  198. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sd_event_probe_payloads.py +0 -0
  199. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sd_listing.py +0 -0
  200. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sd_playback.py +0 -0
  201. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_adaptive.py +0 -0
  202. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_avio_dispatch.py +0 -0
  203. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_bridge_break_gate.py +0 -0
  204. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_connection_mode.py +0 -0
  205. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_echo_wait_timeout.py +0 -0
  206. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_fast_liveplay.py +0 -0
  207. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  208. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_first_media_stall_report.py +0 -0
  209. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_idle_release.py +0 -0
  210. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_nack_bridge.py +0 -0
  211. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_offer_video_order.py +0 -0
  212. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_offer_video_pin.py +0 -0
  213. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_open_cleanup.py +0 -0
  214. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_sctp_abort.py +0 -0
  215. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_serve_audio.py +0 -0
  216. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_serve_cmd.py +0 -0
  217. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_serve_stderr_drain.py +0 -0
  218. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_sprop.py +0 -0
  219. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_talk.py +0 -0
  220. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_teardown_exit_log.py +0 -0
  221. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_terminal_ack.py +0 -0
  222. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_terminal_ack_latency.py +0 -0
  223. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_tmmbr_bridge.py +0 -0
  224. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdes_watchdog.py +0 -0
  225. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdp_narrowing_properties.py +0 -0
  226. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sdp_payload_type_narrowing.py +0 -0
  227. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_security_hardening.py +0 -0
  228. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  229. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_serializable_login_info.py +0 -0
  230. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_serve_input_timeout.py +0 -0
  231. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_serve_loss_tolerance.py +0 -0
  232. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_serve_pt_respects_the_pin.py +0 -0
  233. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_serve_relay.py +0 -0
  234. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  235. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_session_stats.py +0 -0
  236. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_shared_house_seam.py +0 -0
  237. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_snapshot_cancellation.py +0 -0
  238. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_speak.py +0 -0
  239. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sprop_capture_gate.py +0 -0
  240. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sprop_staleness.py +0 -0
  241. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sprop_unstable_public.py +0 -0
  242. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  243. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_srtp_rx_rekey.py +0 -0
  244. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_srtp_tx_key_note.py +0 -0
  245. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stall_nudge_policy.py +0 -0
  246. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  247. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  248. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  249. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  250. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  251. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stream_cap.py +0 -0
  252. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stream_drain_ownership.py +0 -0
  253. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stream_idle.py +0 -0
  254. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stream_slot_cap.py +0 -0
  255. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_stream_teardown.py +0 -0
  256. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_talk.py +0 -0
  257. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  258. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_tap_to_mux_end_to_end.py +0 -0
  259. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_terminal_ack.py +0 -0
  260. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_tmmbr.py +0 -0
  261. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_token_file_robustness.py +0 -0
  262. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_token_refresh.py +0 -0
  263. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_turn_create_permission.py +0 -0
  264. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  265. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_upstream_compat.py +0 -0
  266. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_validator_shares_one_token.py +0 -0
  267. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_video_profile_log.py +0 -0
  268. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_video_pts_dts.py +0 -0
  269. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_video_ts_unwrap.py +0 -0
  270. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_viewer_check_throttle.py +0 -0
  271. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_viewer_presence.py +0 -0
  272. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_viewer_present_push_mode.py +0 -0
  273. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_wait_or_event.py +0 -0
  274. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_wake_gate_camera_evidence.py +0 -0
  275. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_wake_readiness_retry.py +0 -0
  276. {python_aidot_cameras-1.0.0b33 → python_aidot_cameras-1.0.0b34}/tests/test_webrtc_answer_race.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b33
3
+ Version: 1.0.0b34
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
@@ -52,7 +52,11 @@ from .sdes import SdesSession # re-exported (split into sdes.py); also the SDES
52
52
  from .controls import _CameraControlsMixin
53
53
  from .sd_listing import _CameraSdMixin
54
54
  from .webrtc_open import _WebRTCOpenMixin
55
- from .sdes_open import _SdesOpenMixin
55
+ from .sdes_open import (
56
+ _SdesOpenMixin,
57
+ _sdes_max_session_s,
58
+ _session_cap_reached,
59
+ )
56
60
  from .protocol import ( # noqa: F401 - used here and/or by the webrtc_open mixin; kept re-exported for back-compat
57
61
  _mqtt_timestamp,
58
62
  ReconnectPacer,
@@ -71,6 +75,7 @@ from .protocol import ( # noqa: F401 - used here and/or by the webrtc_open mixi
71
75
  _save_sprop,
72
76
  _inject_sprop,
73
77
  _sdes_serve_port,
78
+ sdes_ice_teardown,
74
79
  _serve_host,
75
80
  _warn_lan_serve,
76
81
  _ServeRelay,
@@ -1120,6 +1125,118 @@ def _ack_matches_seq(msg, seq):
1120
1125
  return code if isinstance(code, int) else False
1121
1126
 
1122
1127
 
1128
+ _TERMINAL_ID: "Optional[str]" = None
1129
+
1130
+
1131
+ def _stable_terminal_id(seed: "Optional[str]" = None, width: int = 6) -> str:
1132
+ """The six-hex terminal field of our peer ids. Stable, not per-open random.
1133
+
1134
+ The camera parses the peer id as ``<session>_<terminal_id>_%d_%d_%d`` and
1135
+ compares the terminal id against the sessions it already has -
1136
+ ``rtc_session_check_same_terminal_id`` / "rtc session terminal id is same",
1137
+ with paired ``new_terminal_id`` / ``old_terminal_id`` logs. So the terminal
1138
+ id is how it recognises a client it has seen before.
1139
+
1140
+ We were minting six fresh random hex digits per open: 329 distinct terminal
1141
+ ids across 836 opens in 19 hours, so every reconnect looked like a brand-new
1142
+ client and the camera's "same terminal" path could never fire for us. A
1143
+ client that reconnects every couple of minutes should look like one client
1144
+ reconnecting.
1145
+
1146
+ Stable for the life of the process. ``seed`` makes it deterministic for
1147
+ tests and would allow deriving it from a durable identity later; without one
1148
+ it is random per process, which is already the property that matters.
1149
+ """
1150
+ global _TERMINAL_ID
1151
+ if seed is not None:
1152
+ import hashlib
1153
+ return hashlib.sha256(seed.encode()).hexdigest()[:width]
1154
+ if _TERMINAL_ID is None:
1155
+ import os
1156
+ _TERMINAL_ID = os.urandom(16).hex()
1157
+ return _TERMINAL_ID[:width]
1158
+
1159
+
1160
+ #: Measurement scaffolding, OFF unless an operator names a file - see the note
1161
+ #: on EXPT_CAP_FILE. A published library must not open a Home Assistant config
1162
+ #: path on the event loop, and with no env var set this knob does no I/O at all.
1163
+ EXPT_PEERID_FILE = os.environ.get("AIDOT_EXPT_PEERID_FILE")
1164
+
1165
+
1166
+ def _expt_peer_id_fields(device_id=None, path=None):
1167
+ """Experiment override for one device's peer-id fields, or None.
1168
+
1169
+ The camera parses the peer id with ``%[^_]_%[^_]_%d_%d_%d`` (`parse_peer_id`
1170
+ at 0x298ef8 in the A001064 image) and takes its client class from the FIRST
1171
+ CHARACTER of field 2: ``client_type = field2[0] - '0'``, against
1172
+ ``EN_WEBRTC_CLIENT_TYPE_{APP_ANDROID,APP_IOS,WEB,ALEXA,GOOGLE_HOME}`` = 0..4.
1173
+ The vendor app hard-codes a leading ``'0'`` there; we generate six random hex
1174
+ digits, so we announce a uniformly random class and an out-of-range one 11
1175
+ times in 16. This exists to pin that character on the live camera.
1176
+
1177
+ Scoped to ONE device, like ``_sdes_max_session_s``. The scoping is not
1178
+ cosmetic: this override also sets the three trailing integers, and an
1179
+ unscoped file would push an SDES ``_2_0_1`` tail at the DTLS cameras, which
1180
+ silently discard a peer id with the wrong transport digit. An earlier
1181
+ unscoped experiment knob hit a battery camera and forced it to ~30x its
1182
+ normal wake rate.
1183
+
1184
+ File format, one line::
1185
+
1186
+ <device_id>:[<terminal>_]<live_type>_<stream_id>_<version>
1187
+
1188
+ Three integers set the integers only. A leading terminal also pins field 2,
1189
+ the field that carries the client class, and comes in two widths:
1190
+
1191
+ * **one hex character** - pins only field 2's FIRST character (the class)
1192
+ and leaves the other five random. This is the arm you almost always
1193
+ want: it reproduces the observed condition, "class is 4, tail varies",
1194
+ without also making every session's peer id identical.
1195
+ * **six hex characters** - pins the whole of field 2. Field 1 is already
1196
+ stable per process, so this makes every peer id byte-identical across
1197
+ opens, which is cross-session peer-id REUSE. The camera has a
1198
+ session-dedup path keyed on the peer id, so a result obtained this way
1199
+ cannot separate the client class from the reuse. Use it only when reuse
1200
+ is the thing under test.
1201
+
1202
+ FAILS CLOSED on anything it cannot parse, on a device mismatch, and on a
1203
+ caller that passes no device id. A malformed peer id is rejected by the
1204
+ camera outright, so a half-written override file must never reach the wire.
1205
+ """
1206
+ if not device_id:
1207
+ return None
1208
+ path = path if path is not None else EXPT_PEERID_FILE
1209
+ if not path:
1210
+ return None
1211
+ try:
1212
+ with open(path) as _fh:
1213
+ raw = _fh.read().strip()
1214
+ except OSError:
1215
+ return None
1216
+ head, sep, tail = raw.partition(":")
1217
+ if not sep or head.strip() != device_id:
1218
+ return None
1219
+ parts = tail.strip().split("_")
1220
+ # Three fields sets the integers only; four also pins the terminal id, so
1221
+ # that field can be screened as well.
1222
+ terminal = None
1223
+ if len(parts) == 4:
1224
+ terminal, parts = parts[0], parts[1:]
1225
+ # One hex character (pin the class, keep the tail random) or exactly six
1226
+ # (pin all of field 2). Any other width would change the peer id's shape,
1227
+ # and the camera rejects a malformed peer id outright.
1228
+ if len(terminal) not in (1, 6) or any(c not in "0123456789abcdef" for c in terminal):
1229
+ return None
1230
+ elif len(parts) != 3:
1231
+ return None
1232
+ out = []
1233
+ for q in parts:
1234
+ if not q.isdigit():
1235
+ return None
1236
+ out.append(int(q))
1237
+ return (terminal, out[0], out[1], out[2])
1238
+
1239
+
1123
1240
  class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesOpenMixin):
1124
1241
  """All camera/streaming methods, mixed into DeviceClient via inheritance."""
1125
1242
 
@@ -4029,7 +4146,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4029
4146
  # loop re-offers within one open attempt on one peer connection, so a
4030
4147
  # late answer still belongs to the offer in flight.
4031
4148
  _loop_peer_id = self.generate_webrtc_peer_id(live_type=2, stream_id=0,
4032
- sdes=True)
4149
+ sdes=True,
4150
+ device_id=self.device_id)
4033
4151
  _peer_reuses = 0
4034
4152
 
4035
4153
  # Wake-readiness retries. A battery camera that answers livePlayResp with
@@ -4052,7 +4170,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4052
4170
  self._serve_relay.set_backend(None)
4053
4171
  if _peer_reuses >= _PEERID_MAX_REUSE:
4054
4172
  _loop_peer_id = self.generate_webrtc_peer_id(
4055
- live_type=2, stream_id=0, sdes=True)
4173
+ live_type=2, stream_id=0, sdes=True,
4174
+ device_id=self.device_id)
4056
4175
  _peer_reuses = 0
4057
4176
  _peer_reuses += 1
4058
4177
  _use_fast = self._adaptive_next_fast(_adaptive, _fast_failed)
@@ -4138,6 +4257,14 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4138
4257
  _started_at = time.monotonic()
4139
4258
  _done = asyncio.ensure_future(session.wait_done())
4140
4259
  _stalled = False
4260
+ _ice_dead = False
4261
+ # Measurement scaffolding, off by default and device-scoped: end a
4262
+ # session after its allotted media time so a pass costs the same
4263
+ # wall clock as a stall and a RATE can be sampled. Anchored on
4264
+ # media, never on the open.
4265
+ _cap_s = _sdes_max_session_s(self.device_id)
4266
+ _first_media_at = None
4267
+ _capped = False
4141
4268
  _idle_release = False
4142
4269
  # No-viewer release. Unlike the DTLS serve (which idle-releases when
4143
4270
  # its mux pipe goes stale), an SDES keepalive otherwise reconnects
@@ -4167,6 +4294,26 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4167
4294
  ):
4168
4295
  _stalled = True
4169
4296
  break
4297
+ if _cap_s:
4298
+ if _first_media_at is None:
4299
+ _first_media_at = session.last_media_monotonic
4300
+ if _session_cap_reached(_first_media_at,
4301
+ time.monotonic(), _cap_s):
4302
+ _capped = True
4303
+ _stalled = True
4304
+ break
4305
+ if sdes_ice_teardown(
4306
+ session.last_media_monotonic,
4307
+ session.last_ice_answer_monotonic,
4308
+ time.monotonic(),
4309
+ ):
4310
+ # Media gone AND the camera's STUN answers gone: its ICE
4311
+ # agent has dropped the transport. Nudging cannot recover
4312
+ # that, so reopen now instead of serving ~25 s more of
4313
+ # nothing while the media watchdog runs down.
4314
+ _ice_dead = True
4315
+ _stalled = True
4316
+ break
4170
4317
  if _idle_on and _serve_port is not None:
4171
4318
  _present = await self._viewer_present(_serve_port)
4172
4319
  if _present: # True -> a viewer is pulling; stay alive
@@ -4217,6 +4364,16 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4217
4364
  )
4218
4365
  return
4219
4366
 
4367
+ if _capped:
4368
+ _LOGGER.info(
4369
+ "SDES %s: session cap %.0fs reached - ending session"
4370
+ " (measurement scaffolding; this session PASSED the"
4371
+ " 80.2s gate)", self.device_id, _cap_s)
4372
+ if _ice_dead:
4373
+ _LOGGER.info(
4374
+ "SDES %s: camera ICE transport gone (media and STUN answers"
4375
+ " both stopped) - reopening now rather than waiting out the"
4376
+ " media watchdog", self.device_id)
4220
4377
  if _stalled:
4221
4378
  # Say whether this will repeat forever. With idle-release off
4222
4379
  # (<=0 idle window) the loop never asks whether a viewer is
@@ -4252,7 +4409,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4252
4409
  # the next open should be a fresh one rather than re-offering on a
4253
4410
  # peerid the camera has already finished with.
4254
4411
  _loop_peer_id = self.generate_webrtc_peer_id(
4255
- live_type=2, stream_id=0, sdes=True)
4412
+ live_type=2, stream_id=0, sdes=True,
4413
+ device_id=self.device_id)
4256
4414
  _peer_reuses = 0
4257
4415
  else:
4258
4416
  if _should_abandon_keepalive(
@@ -6052,24 +6210,76 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
6052
6210
 
6053
6211
  @staticmethod
6054
6212
  def generate_webrtc_peer_id(
6055
- live_type: int = 2, stream_id: int = 0, *, sdes: bool = False
6213
+ live_type: int = 2, stream_id: int = 0, *, sdes: bool = False,
6214
+ device_id: "Optional[str]" = None
6056
6215
  ) -> str:
6057
6216
  """Generate a peerId for a WebRTC connection.
6058
6217
 
6059
6218
  Format observed in iOS app telemetry:
6060
6219
  ``{32-hex-session}_{6-hex-random}_{liveType}_{streamId}_{version}``
6061
6220
 
6062
- The trailing version digit encodes the signalling transport:
6063
- ``1`` for SDES-SRTP cameras, ``2`` for DTLS-SRTP cameras.
6064
- iOS app telemetry (2025-03-23) confirms: LK.IPC.A001513 (SDES,
6065
- ``enableSdes: "1"``) uses ``_1`` and responds; LK.IPC.A000088 /
6066
- LK.IPC.A001064 (DTLS, ``enableSdes: "0"``) use ``_2`` and respond.
6067
- SDES cameras appear to silently discard webrtcReq with ``_2``.
6221
+ The trailing digit is set per transport - ``1`` for SDES-SRTP cameras,
6222
+ ``2`` for DTLS-SRTP ones - and empirically the cameras want it that way:
6223
+ an SDES camera silently discards a webrtcReq ending ``_2``.
6224
+
6225
+ The NAME is wrong and the 2025-03 telemetry reading behind it does not
6226
+ survive the decompiled app. ``KVSWebRTCChannel`` builds this field as
6227
+ ``streamID = !isDefaultHD``, i.e. a stream index in {0, 1} derived from
6228
+ the quality setting - HD is 0, SD is 1. It is not a transport version
6229
+ and the app can never emit a ``2`` here. Field 3 is ``playCmd``
6230
+ (LIVE=0, SDCARD=1, PRE_LINK=2) and we send 2 = PRE_LINK for a live view;
6231
+ field 4 is a literal 0 in the app. All three are left alone for now:
6232
+ they are demonstrably not what the camera classifies a client by (that
6233
+ is field 2's first character - see ``_expt_peer_id_fields``), and
6234
+ changing a digit the cameras currently accept, on a guess, is how the
6235
+ stream stops working.
6068
6236
  """
6069
6237
  import os
6070
- session = os.urandom(16).hex() # 32 hex chars
6071
- rand6 = os.urandom(3).hex() # 6 hex chars
6238
+ # Field 1 is the install identity - the vendor app puts
6239
+ # DeviceIdUtils.getDeviceId(app) here and it is stable for the life of
6240
+ # the install. Field 2 is per-open random ("0" + 5 random chars in the
6241
+ # app), so it must NOT be pinned.
6242
+ session = _stable_terminal_id(width=32)
6243
+ # Field 2's first character is the client class on the A001064:
6244
+ # `parse_peer_id` (0x298ef8 in that image) does
6245
+ # `client_type = field2[0] - '0'` against
6246
+ # EN_WEBRTC_CLIENT_TYPE_{APP_ANDROID,APP_IOS,WEB,ALEXA,GOOGLE_HOME}=0..4,
6247
+ # the vendor Android app hard-codes '0' there and the vendor web app
6248
+ # sends '2'. That is all verified.
6249
+ #
6250
+ # **It is NOT verified for the rest of the fleet, and pinning it breaks
6251
+ # the A000088.** Setting this character to '0' for every camera was
6252
+ # tried and reverted on 2026-08-31. Against a drained fleet with Home
6253
+ # Assistant stopped, three A000088 cameras went 0 for 3 over nine
6254
+ # attempts - each one completing its DTLS handshake in under two seconds
6255
+ # and then receiving no media at all - while the same build streamed the
6256
+ # A001064 on the first attempt. Reverting THIS LINE ALONE, with every
6257
+ # other change of that release left in place, restored 3 of 3.
6258
+ #
6259
+ # The A000088 is a different SoC (Realtek Ameba, FreeRTOS/KM4) whose
6260
+ # image has never been disassembled. A finding from the A001064's
6261
+ # firmware does not transfer to it, and this camera does not merely
6262
+ # ignore the byte - announcing APP_ANDROID makes it accept the session
6263
+ # and send nothing, which is worse than the random value it replaced.
6264
+ #
6265
+ # So the character stays random. Nothing needs it: the 80.2 s cliff this
6266
+ # was found while chasing is fixed in the SCTP receiver (see
6267
+ # `_sctp_sack_chunk`), not here. Screen it per model with
6268
+ # `_expt_peer_id_fields` before ever setting it again, and get evidence
6269
+ # from EACH model's own firmware.
6270
+ rand6 = os.urandom(3).hex() # 6 hex chars, fresh per open
6072
6271
  version = 1 if sdes else 2
6272
+ # Experiment override (off by default, fails closed, scoped to one
6273
+ # device): the camera reads its client class from field 2's first
6274
+ # character - see _expt_peer_id_fields. Without a device id the
6275
+ # override never applies.
6276
+ _fields = _expt_peer_id_fields(device_id)
6277
+ if _fields is not None:
6278
+ _term, live_type, stream_id, version = _fields
6279
+ if _term:
6280
+ # One character pins the class and keeps the tail random; six
6281
+ # pins the whole field. Never let the id change length.
6282
+ rand6 = _term if len(_term) == 6 else _term + rand6[1:]
6073
6283
  return f"{session}_{rand6}_{live_type}_{stream_id}_{version}"
6074
6284
 
6075
6285
  async def async_open_webrtc_stream(
@@ -271,6 +271,32 @@ def answer_inserted_a_section(*, shifted_sections: int,
271
271
  return shifted_sections > 0 and unclaimed_answer_mids > 0
272
272
 
273
273
 
274
+ def sdes_ice_teardown(last_media: float, last_ice_answer: float, now: float,
275
+ media_gap: float = 3.0, ice_gap: float = 6.0) -> bool:
276
+ """Whether the camera's ICE agent has torn the transport down.
277
+
278
+ The A001064 runs Leedarson's fork of the AWS KVS WebRTC SDK C, whose ICE
279
+ agent transitions CONNECTED -> DISCONNECTED -> FAILED and drops the
280
+ transport. Measured over four stock sessions: while the transport is alive
281
+ the camera answers our STUN binding requests roughly every 2.5 s and sends
282
+ its own keepalive indication; at the 80.2 s cliff the answers, the
283
+ indications and the media all stop in the same instant. A session that
284
+ survives the cliff keeps all three going.
285
+
286
+ Media stopping ALONE can be a pause the camera recovers from - that is what
287
+ the LIVING nudges are for. Media stopping AND the STUN answers stopping is
288
+ the transport being gone, and only a reopen can help. Acting on that
289
+ directly saves the ~30 s the media watchdog otherwise spends finding out.
290
+
291
+ Fails safe: either clock unstarted (0.0) returns False, so a session that
292
+ never carried media - or a build where the ICE timestamp is not plumbed
293
+ through - falls back to the ordinary watchdog rather than being torn down.
294
+ """
295
+ if last_media <= 0.0 or last_ice_answer <= 0.0:
296
+ return False
297
+ return (now - last_media) > media_gap and (now - last_ice_answer) > ice_gap
298
+
299
+
274
300
  def ice_wait_timeout(*, shifted_sections: int, default_timeout: float) -> float:
275
301
  """How long to give ICE, given what the camera's answer looked like.
276
302
 
@@ -49,6 +49,8 @@ class SdesSession(AvioRequestMixin):
49
49
  cmd_chan=None,
50
50
  talk_state=None,
51
51
  media_progress=None,
52
+
53
+ ice_progress=None,
52
54
  media_counts=None,
53
55
  teardown_requested=None,
54
56
  first_video_pt=None,
@@ -74,6 +76,13 @@ class SdesSession(AvioRequestMixin):
74
76
  # that stopped sending (battery teardown ~49-72s) and reconnect - ffmpeg
75
77
  # itself never exits on a dead UDP input, so wait_done() alone would hang.
76
78
  self._media_progress = media_progress if media_progress is not None else [0.0]
79
+ # Mutable one-element list shared with the bridge thread: [0] holds the
80
+ # monotonic timestamp of the last STUN binding success or keepalive
81
+ # indication the camera sent us (0.0 until the first one). While the
82
+ # ICE transport is alive the camera answers about every 2.5 s; at the
83
+ # 80.2 s cliff the answers stop in the same instant media does, which is
84
+ # how a transport teardown is told apart from a media pause.
85
+ self._ice_progress = ice_progress if ice_progress is not None else [0.0]
77
86
  # Shared with the bridge thread: [packets, bytes] forwarded to ffmpeg.
78
87
  # See media_stats() - the SDES path never calls on_frame, so this is the
79
88
  # in-process signal that media is actually flowing.
@@ -228,6 +237,11 @@ class SdesSession(AvioRequestMixin):
228
237
  """time.monotonic() of the last media packet forwarded, 0.0 if none yet."""
229
238
  return self._media_progress[0]
230
239
 
240
+ @property
241
+ def last_ice_answer_monotonic(self) -> float:
242
+ """time.monotonic() of the camera's last STUN answer, 0.0 if none yet."""
243
+ return self._ice_progress[0]
244
+
231
245
  def media_stats(self) -> dict:
232
246
  """Snapshot of media actually forwarded to ffmpeg by the bridge.
233
247