python-aidot-cameras 1.0.0rc67__tar.gz → 1.0.0rc68__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 (365) hide show
  1. {python_aidot_cameras-1.0.0rc67/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc68}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/protocol.py +62 -0
  3. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/sd_listing.py +141 -29
  4. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/pyproject.toml +1 -1
  5. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_avio_request.py +81 -0
  7. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_listing.py +121 -0
  8. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/LICENSE +0 -0
  9. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/MANIFEST.in +0 -0
  10. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/README.md +0 -0
  11. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/__init__.py +0 -0
  12. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/__main__.py +0 -0
  13. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_log_redaction.py +0 -0
  14. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_upstream.py +0 -0
  15. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/__init__.py +0 -0
  16. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  17. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  18. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  19. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  20. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  21. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  22. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  23. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  24. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  25. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  26. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  27. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  28. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  29. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  30. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  31. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  32. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  33. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  34. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  35. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  36. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  37. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  38. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  39. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  40. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  41. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  42. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  43. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  44. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  45. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  46. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  47. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  48. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  49. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/__init__.py +0 -0
  50. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/aac_track.py +0 -0
  51. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/agc.py +0 -0
  52. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/client.py +0 -0
  53. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/constants.py +0 -0
  54. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/controls.py +0 -0
  55. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/go2rtc.py +0 -0
  56. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/h264_sps.py +0 -0
  57. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/health.py +0 -0
  58. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/hls_ts.py +0 -0
  59. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/hwaccel.py +0 -0
  60. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/lan_control.py +0 -0
  61. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/models.py +0 -0
  62. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/recording.py +0 -0
  63. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/rtp_h264.py +0 -0
  64. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/rtsp_publish.py +0 -0
  65. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/sd_events.py +0 -0
  66. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/sd_playback.py +0 -0
  67. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/sdes.py +0 -0
  68. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/sdes_open.py +0 -0
  69. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/ts_fanout.py +0 -0
  70. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/ts_tee.py +0 -0
  71. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/webrtc.py +0 -0
  72. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/camera/webrtc_open.py +0 -0
  73. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/client.py +0 -0
  74. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/cloud_auth.py +0 -0
  75. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/const.py +0 -0
  76. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/credentials.py +0 -0
  77. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/crypto.py +0 -0
  78. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/device_client.py +0 -0
  79. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/discover.py +0 -0
  80. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/exceptions.py +0 -0
  81. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/g711.py +0 -0
  82. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/lan_retry.py +0 -0
  83. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/py.typed +0 -0
  84. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/aidot_cameras/settings.py +0 -0
  85. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  86. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  87. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  88. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/python_aidot_cameras.egg-info/requires.txt +0 -0
  89. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  90. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/setup.cfg +0 -0
  91. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_aac_track.py +0 -0
  92. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
  93. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
  94. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_aioice_compat.py +0 -0
  95. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_alarm_event.py +0 -0
  96. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_answer_pt_kinds.py +0 -0
  97. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_answer_sdp_video_pt.py +0 -0
  98. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_answer_section_selection.py +0 -0
  99. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_arrival_timestamps.py +0 -0
  100. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_audio_mux_offline.py +0 -0
  101. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_auth_error_routing.py +0 -0
  102. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_auto_alarm.py +0 -0
  103. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_auto_light_settings.py +0 -0
  104. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  105. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_avio_header_single_layout.py +0 -0
  106. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_avio_response_parse.py +0 -0
  107. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_avio_response_router.py +0 -0
  108. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_backoff.py +0 -0
  109. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_battery_cap_spans_sessions.py +0 -0
  110. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_battery_detection.py +0 -0
  111. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_battery_evidence_corroboration.py +0 -0
  112. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  113. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_battery_wake_settles_before_the_command.py +0 -0
  114. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ble_gateway_is_gone.py +0 -0
  115. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_bridge_state_sentinels.py +0 -0
  116. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_busy_backoff.py +0 -0
  117. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_camera_properties.py +0 -0
  118. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_canary_log_names_the_camera.py +0 -0
  119. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
  120. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_cloud_count.py +0 -0
  121. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_cloud_error_log_names_the_exception.py +0 -0
  122. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_cloud_error_severity.py +0 -0
  123. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_cloud_plan.py +0 -0
  124. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_constants_imports_resolve.py +0 -0
  125. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_control_verdicts.py +0 -0
  126. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_declared_properties.py +0 -0
  127. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_deferred_hardening.py +0 -0
  128. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_detection_types.py +0 -0
  129. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_device_cmd_seq_match.py +0 -0
  130. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_device_login_guard.py +0 -0
  131. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_device_user_info_cache.py +0 -0
  132. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
  133. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_direct_ts_keyframe_signal.py +0 -0
  134. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_direct_ts_serve.py +0 -0
  135. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_direct_ts_serve_only_for_http.py +0 -0
  136. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dormant_camera_cleanup.py +0 -0
  137. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_drain_is_cancellable.py +0 -0
  138. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_mux_terminal_write.py +0 -0
  139. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_not_ready_burst.py +0 -0
  140. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
  141. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_pinning.py +0 -0
  142. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_recorder_wiring.py +0 -0
  143. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_refusal_after_the_echo.py +0 -0
  144. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_resolution_ack.py +0 -0
  145. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_serve_decode_skip.py +0 -0
  146. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_serve_ice_wait.py +0 -0
  147. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_serve_open_timeout.py +0 -0
  148. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_skip_signaling_wait.py +0 -0
  149. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_slow_probe.py +0 -0
  150. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_stdout_producer.py +0 -0
  151. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_tap_states_no_reorder.py +0 -0
  152. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_ts_copy_recording.py +0 -0
  153. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_dtls_video_presence.py +0 -0
  154. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_egress_guard.py +0 -0
  155. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_env_seams.py +0 -0
  156. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_every_exception_is_exported.py +0 -0
  157. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  158. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
  159. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  160. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_first_media_abandon_rules.py +0 -0
  161. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_futile_keepalive.py +0 -0
  162. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  163. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_go2rtc.py +0 -0
  164. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_go2rtc_cli.py +0 -0
  165. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_h264_decode_rate_limit.py +0 -0
  166. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_h264_sps.py +0 -0
  167. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_health_line.py +0 -0
  168. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_health_line_counts_pictures.py +0 -0
  169. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_highport_nomination.py +0 -0
  170. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_hls_ts_end_to_end.py +0 -0
  171. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_hls_ts_wiring.py +0 -0
  172. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_hwaccel_probe.py +0 -0
  173. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ice_config_cache.py +0 -0
  174. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ice_credentials_match_the_app.py +0 -0
  175. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ice_peer_reflexive.py +0 -0
  176. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_idle_release_both_loops.py +0 -0
  177. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ipc_envelope_single_builder.py +0 -0
  178. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_keepalive_renew.py +0 -0
  179. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_key_restart_sdp.py +0 -0
  180. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_keyframe_prompter.py +0 -0
  181. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_control.py +0 -0
  182. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_login_accessors.py +0 -0
  183. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  184. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_retry_short_sessions.py +0 -0
  185. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
  186. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_retry_stops_on_close.py +0 -0
  187. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_lan_sweep_only_with_lights.py +0 -0
  188. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_late_repeats_are_dropped.py +0 -0
  189. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_light_active_color_mode.py +0 -0
  190. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_live_session_carries_talk.py +0 -0
  191. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_live_stream_param.py +0 -0
  192. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_live_validate_policy.py +0 -0
  193. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_live_validate_token_refresh.py +0 -0
  194. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_live_video_canary.py +0 -0
  195. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_liveplay_echo_timeout.py +0 -0
  196. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_local_only_controls.py +0 -0
  197. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_log_hygiene.py +0 -0
  198. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_logging_caps.py +0 -0
  199. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_login_connect_deadline.py +0 -0
  200. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_login_reconnect_backoff.py +0 -0
  201. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_loop_break_ends_the_loop.py +0 -0
  202. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_media_counted_only_when_plaintext.py +0 -0
  203. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_media_path_is_reported.py +0 -0
  204. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_media_socket_rcvbuf.py +0 -0
  205. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_media_wait_floor.py +0 -0
  206. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_motion_poll.py +0 -0
  207. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mqtt_client_id_is_exact.py +0 -0
  208. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mqtt_credential_refetch.py +0 -0
  209. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mqtt_credential_self_heal.py +0 -0
  210. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mqtt_password_not_persisted.py +0 -0
  211. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mqtt_session_reconnect.py +0 -0
  212. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  213. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_mypy_new_findings.py +0 -0
  214. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_nack.py +0 -0
  215. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  216. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_narrow_pc_ice.py +0 -0
  217. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_no_key_material_in_logs.py +0 -0
  218. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
  219. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_no_self_referential_go2rtc.py +0 -0
  220. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_no_unbounded_hex_dumps.py +0 -0
  221. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_no_undefined_names.py +0 -0
  222. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
  223. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_off_subnet_candidate.py +0 -0
  224. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  225. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_offline_keepalive_pause.py +0 -0
  226. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_on_frame_is_an_av_frame.py +0 -0
  227. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_open_errors_are_aidot_errors.py +0 -0
  228. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_open_fail_logger.py +0 -0
  229. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_open_gate_delay.py +0 -0
  230. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_peer_id_client_type.py +0 -0
  231. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_peerid_reuse.py +0 -0
  232. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_persistent_mqtt.py +0 -0
  233. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_persistent_mqtt_robustness.py +0 -0
  234. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_phase_returns_reach_the_caller.py +0 -0
  235. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_phase_state_is_complete.py +0 -0
  236. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_plaintext_mqtt_refused.py +0 -0
  237. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_post_merge_hardening.py +0 -0
  238. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  239. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_public_signature_matches_impl.py +0 -0
  240. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_publish_target_readiness.py +0 -0
  241. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_quality_campaign_in_session.py +0 -0
  242. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_reboot_action.py +0 -0
  243. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_recent_recordings.py +0 -0
  244. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_region_allowlist.py +0 -0
  245. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_remb.py +0 -0
  246. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  247. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
  248. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_resent_frames_are_dropped.py +0 -0
  249. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_resolution_persists.py +0 -0
  250. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_restart_cycle.py +0 -0
  251. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_retry_policy.py +0 -0
  252. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_review_lan_retry_and_graces.py +0 -0
  253. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_rtp_h264.py +0 -0
  254. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_rtsp_publish.py +0 -0
  255. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_rtsp_publish_go2rtc.py +0 -0
  256. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_scripts_importable.py +0 -0
  257. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sctp_init_ack_tsn.py +0 -0
  258. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sctp_reassembly.py +0 -0
  259. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sctp_sack.py +0 -0
  260. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_card_presence.py +0 -0
  261. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_event_decode.py +0 -0
  262. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_event_probe_payloads.py +0 -0
  263. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback.py +0 -0
  264. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback_clock.py +0 -0
  265. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback_controls.py +0 -0
  266. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback_dtls.py +0 -0
  267. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback_entry.py +0 -0
  268. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback_media_hold.py +0 -0
  269. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sd_playback_open.py +0 -0
  270. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_avio_dispatch.py +0 -0
  271. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_bridge_break_gate.py +0 -0
  272. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_connection_mode.py +0 -0
  273. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
  274. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_fast_liveplay.py +0 -0
  275. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  276. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_first_media_stall_report.py +0 -0
  277. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_ice_teardown.py +0 -0
  278. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_idle_release.py +0 -0
  279. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_nack_bridge.py +0 -0
  280. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_offer_video_pin.py +0 -0
  281. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_open_cleanup.py +0 -0
  282. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_open_yields_the_loop.py +0 -0
  283. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_pinned_codec_retry.py +0 -0
  284. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_sctp_abort.py +0 -0
  285. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_serve_audio.py +0 -0
  286. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_serve_cmd.py +0 -0
  287. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_serve_stderr_drain.py +0 -0
  288. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_sprop.py +0 -0
  289. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_talk.py +0 -0
  290. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_teardown_exit_log.py +0 -0
  291. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_terminal_ack.py +0 -0
  292. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_terminal_ack_latency.py +0 -0
  293. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdes_watchdog.py +0 -0
  294. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdp_narrowing_properties.py +0 -0
  295. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sdp_payload_type_narrowing.py +0 -0
  296. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_security_hardening.py +0 -0
  297. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  298. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serializable_login_info.py +0 -0
  299. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
  300. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_input_timeout.py +0 -0
  301. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_loop_releases_on_error.py +0 -0
  302. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_loss_tolerance.py +0 -0
  303. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_pt_respects_the_pin.py +0 -0
  304. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_relay.py +0 -0
  305. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  306. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_session_mode_seq.py +0 -0
  307. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_session_stats.py +0 -0
  308. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_session_stop_twice.py +0 -0
  309. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_settings_registry.py +0 -0
  310. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_shared_house_seam.py +0 -0
  311. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  312. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_skip_guard.py +0 -0
  313. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_snapshot_cancellation.py +0 -0
  314. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_speak.py +0 -0
  315. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sprop_capture_gate.py +0 -0
  316. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sprop_staleness.py +0 -0
  317. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sprop_unstable_public.py +0 -0
  318. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  319. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_srtp_rx_rekey.py +0 -0
  320. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_srtp_tx_key_note.py +0 -0
  321. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stable_terminal_id.py +0 -0
  322. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stall_nudge_policy.py +0 -0
  323. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  324. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  325. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  326. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  327. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  328. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_status_and_trace_channels.py +0 -0
  329. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stream_cap.py +0 -0
  330. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stream_drain_ownership.py +0 -0
  331. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stream_idle.py +0 -0
  332. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stream_slot_cap.py +0 -0
  333. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stream_teardown.py +0 -0
  334. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_stun_attr_single_builder.py +0 -0
  335. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_swallowed_exception_names.py +0 -0
  336. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_talk.py +0 -0
  337. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  338. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_tap_to_mux_end_to_end.py +0 -0
  339. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_terminal_ack.py +0 -0
  340. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_tmmbr.py +0 -0
  341. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_token_file_robustness.py +0 -0
  342. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_token_refresh.py +0 -0
  343. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_transport_failure_reporting.py +0 -0
  344. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ts_fanout.py +0 -0
  345. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_ts_tee.py +0 -0
  346. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_turn_create_permission.py +0 -0
  347. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_turn_target_shared.py +0 -0
  348. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
  349. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
  350. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_upstream_compat.py +0 -0
  351. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_upstream_seam_adapter.py +0 -0
  352. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_upstream_secrets_redacted.py +0 -0
  353. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_validator_shares_one_token.py +0 -0
  354. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_vendored_aiortc_integrity.py +0 -0
  355. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_video_presence_needs_a_keyframe.py +0 -0
  356. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_video_profile_log.py +0 -0
  357. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_video_pts_dts.py +0 -0
  358. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_video_ts_unwrap.py +0 -0
  359. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_viewer_check_throttle.py +0 -0
  360. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_viewer_presence.py +0 -0
  361. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_viewer_present_push_mode.py +0 -0
  362. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_wait_or_event.py +0 -0
  363. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_wake_gate_camera_evidence.py +0 -0
  364. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/tests/test_wake_readiness_retry.py +0 -0
  365. {python_aidot_cameras-1.0.0rc67 → python_aidot_cameras-1.0.0rc68}/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.0rc67
3
+ Version: 1.0.0rc68
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
@@ -3847,6 +3847,68 @@ class AvioRequestMixin:
3847
3847
  return None
3848
3848
  return await waiter.result(timeout)
3849
3849
 
3850
+ async def async_avio_request_all(
3851
+ self,
3852
+ cmd: int,
3853
+ payload: bytes = b"",
3854
+ *,
3855
+ response_cmd: int,
3856
+ timeout: float = AVIO_RESPONSE_TIMEOUT_S,
3857
+ more_timeout: float = AVIO_RESPONSE_TIMEOUT_S,
3858
+ max_replies: int = 64,
3859
+ budget: "Optional[float]" = None,
3860
+ done=None,
3861
+ ) -> "list[AvioResponse]":
3862
+ """Send ``cmd`` once and return every ``response_cmd`` reply it draws.
3863
+
3864
+ For a command the camera answers in several packets with no further
3865
+ request: the SD listing sends a 7-day window as one 255-record page
3866
+ after another, all unprompted. ``async_avio_request`` keeps the first
3867
+ and the rest arrive with nobody waiting, so a caller saw the oldest
3868
+ 255 recordings and never today's.
3869
+
3870
+ Every waiter is registered before the send, for the same reason as the
3871
+ single request: the pages follow each other faster than a waiter
3872
+ registered after the first reply could catch the second. Collection
3873
+ stops at the first wait that times out (``timeout`` for the first
3874
+ reply, ``more_timeout`` after it), at ``max_replies``, once ``budget``
3875
+ seconds have passed since the send, or as soon as ``done(replies)`` is
3876
+ true. An empty list means nothing came back.
3877
+
3878
+ Replies are matched on ``response_cmd`` alone, so two of these for the
3879
+ same command at once on one session can take each other's replies.
3880
+ """
3881
+ router = self._avio_responses
3882
+ waiters = [router.expect(response_cmd) for _ in range(max(1, max_replies))]
3883
+ replies: "list[AvioResponse]" = []
3884
+ try:
3885
+ try:
3886
+ sent = self._avio_cmd(cmd, payload)
3887
+ except Exception:
3888
+ sent = False
3889
+ if not sent:
3890
+ return replies
3891
+ loop = asyncio.get_running_loop()
3892
+ give_up = None if budget is None else loop.time() + budget
3893
+ for waiter in waiters:
3894
+ wait = more_timeout if replies else timeout
3895
+ if give_up is not None:
3896
+ wait = min(wait, give_up - loop.time())
3897
+ if wait <= 0:
3898
+ break
3899
+ resp = await waiter.result(wait)
3900
+ if resp is None:
3901
+ break
3902
+ replies.append(resp)
3903
+ if done is not None and done(replies):
3904
+ break
3905
+ finally:
3906
+ # The ones never reached are still registered; a stray later reply
3907
+ # must go to nobody rather than to a question long since answered.
3908
+ for waiter in waiters:
3909
+ waiter.cancel()
3910
+ return replies
3911
+
3850
3912
 
3851
3913
  _SDP_SECRET_ATTRS = ("a=ice-pwd:", "a=psk:")
3852
3914
 
@@ -14,7 +14,7 @@ list rather than as a traceback.
14
14
 
15
15
  import logging
16
16
  import time
17
- from typing import Any, Optional
17
+ from typing import Any, List, Optional
18
18
 
19
19
  from .sd_events import (
20
20
  HASLISTEVENT_REQ_CMD,
@@ -34,6 +34,94 @@ _LOGGER = logging.getLogger(__name__)
34
34
  #: requests cannot hold a session for more than a viewer would tolerate.
35
35
  _SD_LIST_TIMEOUT_S = 8.0
36
36
 
37
+ #: The pages of one listing follow each other back to back; measured on a PTZ,
38
+ #: four pages of a 7-day window arrived together. A gap this long after a page
39
+ #: means no more are coming.
40
+ _SD_MORE_PAGES_TIMEOUT_S = 3.0
41
+
42
+ #: 255 records a page, so 64 pages is over 16,000 recordings - far more than a
43
+ #: card holds in the 7 days a caller asks about.
44
+ _SD_MAX_PAGES = 64
45
+
46
+ #: However the pages trickle in, a listing gives up collecting them after this
47
+ #: long and reports what it has as incomplete. Without it each page would
48
+ #: restart the gap timer and a slow camera could hold the session for minutes.
49
+ _SD_LIST_PAGES_BUDGET_S = 30.0
50
+
51
+
52
+ class _ListPages:
53
+ """The pages of one listing reply, each decoded once, as it arrives."""
54
+
55
+ def __init__(self) -> None:
56
+ self.pages: dict = {}
57
+ self.undecodable = 0
58
+ self._total: Optional[int] = None
59
+ self._seen = 0
60
+
61
+ def take(self, replies: List[Any]) -> None:
62
+ """Decode whatever in ``replies`` has not been decoded yet."""
63
+ for reply in replies[self._seen :]:
64
+ self._add(reply)
65
+ self._seen = len(replies)
66
+
67
+ def done(self, replies: List[Any]) -> bool:
68
+ """The ``done`` hook: stop collecting once the reply is whole."""
69
+ self.take(replies)
70
+ return self.covered()
71
+
72
+ def _add(self, reply: Any) -> None:
73
+ page = decode_list_event_response(
74
+ bytes(reply.payload or b""), command=LISTEVENT_RESP_CMD
75
+ )
76
+ if page is None:
77
+ # The decode is measured from a handful of live replies and a
78
+ # reply with a different shape is entirely possible. It is an
79
+ # empty list with a logged reason, never an exception - but not
80
+ # a COMPLETE empty list: something came back and could not be
81
+ # read, so there may well be recordings behind it.
82
+ self.undecodable += 1
83
+ _LOGGER.debug(
84
+ "SD list reply did not decode (%d bytes)",
85
+ len(bytes(reply.payload or b"")),
86
+ )
87
+ return
88
+ if self._total is None:
89
+ self._total = page.total
90
+ elif page.total != self._total:
91
+ # Every page of one reply declares the same page count. One that
92
+ # does not is a late page of some earlier listing; mixing it in
93
+ # would file another window's records under this one.
94
+ _LOGGER.debug(
95
+ "SD list page declares %d pages, this reply %d: not ours",
96
+ page.total,
97
+ self._total,
98
+ )
99
+ return
100
+ # A page the camera sent twice is one page.
101
+ self.pages.setdefault(page.page_index, page)
102
+
103
+ def covered(self) -> bool:
104
+ """The end flag has arrived and no page before it is missing.
105
+
106
+ ``total`` is the page count the camera declared. A camera that declares
107
+ none (0) is held to the pages up to its end-flag page instead.
108
+ """
109
+ ends = [i for i, p in self.pages.items() if p.end_flag == 1]
110
+ if not ends:
111
+ return False
112
+ want = range(self._total) if self._total else range(max(ends) + 1)
113
+ return set(self.pages) >= set(want)
114
+
115
+ @property
116
+ def records(self) -> list:
117
+ return [e for i in sorted(self.pages) for e in self.pages[i].events]
118
+
119
+ @property
120
+ def complete(self) -> bool:
121
+ if self.undecodable:
122
+ return False
123
+ return not self.pages or self.covered()
124
+
37
125
 
38
126
  def _session_is_live(session: Any) -> bool:
39
127
  """Is this session object one that could still carry a request?
@@ -88,6 +176,48 @@ class _CameraSdMixin:
88
176
  )
89
177
  return None
90
178
 
179
+ async def _sd_ask_pages(
180
+ self, session: Any, cmd: int, payload: bytes, response_cmd: int, timeout: float
181
+ ) -> "tuple[_ListPages, bool]":
182
+ """Every reply page to one request, and whether any reply came back.
183
+
184
+ A session without ``async_avio_request_all`` (an older or a stand-in
185
+ session) is asked the single-reply way, which is the first page only.
186
+
187
+ Not safe to run two at once on one session: replies carry no id this
188
+ layer can match on SDES, so concurrent listings could take each other's
189
+ pages. Callers serialise listings per camera.
190
+ """
191
+ pages = _ListPages()
192
+ ask_all = getattr(session, "async_avio_request_all", None)
193
+ if ask_all is None:
194
+ reply = await self._sd_ask(session, cmd, payload, response_cmd, timeout)
195
+ replies = [] if reply is None else [reply]
196
+ else:
197
+ try:
198
+ replies = list(
199
+ await ask_all(
200
+ cmd,
201
+ payload,
202
+ response_cmd=response_cmd,
203
+ timeout=timeout,
204
+ more_timeout=_SD_MORE_PAGES_TIMEOUT_S,
205
+ max_replies=_SD_MAX_PAGES,
206
+ budget=_SD_LIST_PAGES_BUDGET_S,
207
+ done=pages.done,
208
+ )
209
+ )
210
+ except Exception as exc:
211
+ _LOGGER.debug(
212
+ "SD request %#x failed for %s: %s",
213
+ cmd,
214
+ getattr(self, "device_id", "?"),
215
+ exc,
216
+ )
217
+ replies = []
218
+ pages.take(replies)
219
+ return pages, bool(replies)
220
+
91
221
  async def async_get_sd_recordings(
92
222
  self,
93
223
  *,
@@ -115,11 +245,13 @@ class _CameraSdMixin:
115
245
  card-bearing A000088 that returns an EMPTY page while 0 returns the
116
246
  records - measured 2026-08-11, same session, same window.
117
247
 
118
- One request each for the list and the occupancy map. The reply is paged
119
- and there is no known continuation request: the vendor's handler matches
120
- a reply to the sequence number of the single request it sent. So a reply
121
- whose end flag is not set comes back with ``complete=False`` rather than
122
- with a second request this code would have to invent.
248
+ One request each for the list and the occupancy map. The list reply is
249
+ paged, 255 records a page, oldest first, and the camera sends every page
250
+ unprompted: there is no continuation request and none is needed. All of
251
+ them are collected (measured 2026-11-01: a PTZ's 7-day window was four
252
+ pages, 824 records, and reading only the first stopped six days short
253
+ of today). ``complete=False`` when the end flag never arrived or a page
254
+ before it is missing.
123
255
  """
124
256
  session = (
125
257
  session if session is not None else getattr(self, "_stream_session", None)
@@ -134,34 +266,14 @@ class _CameraSdMixin:
134
266
  end_ts = time.time()
135
267
  start_ts = end_ts - days * 86400
136
268
 
137
- records = []
138
- complete = True
139
- list_reply = await self._sd_ask(
269
+ pages, list_answered = await self._sd_ask_pages(
140
270
  session,
141
271
  LISTEVENT_REQ_CMD,
142
272
  listevent_payload(start_ts, end_ts, channel=channel),
143
273
  LISTEVENT_RESP_CMD,
144
274
  timeout,
145
275
  )
146
- if list_reply is not None:
147
- page = decode_list_event_response(
148
- bytes(list_reply.payload or b""), command=LISTEVENT_RESP_CMD
149
- )
150
- if page is None:
151
- # The decode is measured from a handful of live replies and a
152
- # reply with a different shape is entirely possible. It is an
153
- # empty list with a logged reason, never an exception - but not
154
- # a COMPLETE empty list: something came back and could not be
155
- # read, so there may well be recordings behind it.
156
- complete = False
157
- _LOGGER.debug(
158
- "SD list reply for %s did not decode (%d bytes)",
159
- getattr(self, "device_id", "?"),
160
- len(bytes(list_reply.payload or b"")),
161
- )
162
- else:
163
- records = page.events
164
- complete = page.end_flag == 1
276
+ records, complete = pages.records, pages.complete
165
277
 
166
278
  hours = None
167
279
  map_reply = await self._sd_ask(
@@ -181,7 +293,7 @@ class _CameraSdMixin:
181
293
  hours=hours,
182
294
  # Either reply is enough to prove the camera is listening. Both
183
295
  # silent is the case that must never read as an empty card.
184
- answered=list_reply is not None or map_reply is not None,
296
+ answered=list_answered or map_reply is not None,
185
297
  complete=complete,
186
298
  start_ts=start_ts,
187
299
  end_ts=end_ts,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0rc67"
7
+ version = "1.0.0rc68"
8
8
  description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0rc67
3
+ Version: 1.0.0rc68
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
@@ -218,3 +218,84 @@ async def test_dispatching_a_frame_nobody_asked_for_is_harmless():
218
218
  session = _dtls_session(SimpleNamespace(send=lambda data: None))
219
219
  assert session.dispatch_avio_frame(_reply()) is False
220
220
  assert session.dispatch_avio_frame(b"junk") is False
221
+
222
+
223
+ async def test_a_request_answered_in_several_replies_returns_them_all():
224
+ """The SD listing sends its pages back to back with no further request.
225
+
226
+ The second reply follows the first immediately; a waiter registered only
227
+ after the first reply came back would miss it.
228
+ """
229
+ session = None
230
+
231
+ def _send_and_answer(data):
232
+ for body in (b"\x00\x01", b"\x00\x02", b"\x00\x03"):
233
+ session.dispatch_avio_frame(_reply(payload=body))
234
+
235
+ session = _dtls_session(SimpleNamespace(send=_send_and_answer))
236
+
237
+ got = await session.async_avio_request_all(
238
+ SETSTREAMCTRL,
239
+ response_cmd=SETSTREAMCTRL_RESP,
240
+ timeout=2.0,
241
+ more_timeout=0.05,
242
+ )
243
+ assert [r.payload for r in got] == [b"\x00\x01", b"\x00\x02", b"\x00\x03"]
244
+ # Nothing left registered: a reply that turns up later belongs to nobody.
245
+ assert session.dispatch_avio_frame(_reply()) is False
246
+
247
+
248
+ async def test_collecting_replies_stops_when_the_caller_has_enough():
249
+ session = None
250
+
251
+ def _send_and_answer(data):
252
+ for body in (b"\x00\x01", b"\x00\x02", b"\x00\x03"):
253
+ session.dispatch_avio_frame(_reply(payload=body))
254
+
255
+ session = _dtls_session(SimpleNamespace(send=_send_and_answer))
256
+
257
+ got = await session.async_avio_request_all(
258
+ SETSTREAMCTRL,
259
+ response_cmd=SETSTREAMCTRL_RESP,
260
+ timeout=2.0,
261
+ done=lambda replies: len(replies) == 2,
262
+ )
263
+ assert [r.payload for r in got] == [b"\x00\x01", b"\x00\x02"]
264
+ assert session.dispatch_avio_frame(_reply()) is False
265
+
266
+
267
+ async def test_collecting_replies_to_a_command_that_could_not_be_sent():
268
+ session = _dtls_session(None) # DataChannel not open
269
+
270
+ loop = asyncio.get_running_loop()
271
+ started = loop.time()
272
+ got = await session.async_avio_request_all(
273
+ SETSTREAMCTRL, response_cmd=SETSTREAMCTRL_RESP, timeout=30.0
274
+ )
275
+ assert got == []
276
+ assert loop.time() - started < 1.0
277
+ assert session.dispatch_avio_frame(_reply()) is False
278
+
279
+
280
+ async def test_collecting_replies_gives_up_once_its_budget_is_spent():
281
+ """Each reply restarts the gap timer; the budget bounds the whole wait."""
282
+ session = _dtls_session(SimpleNamespace(send=lambda data: None))
283
+
284
+ async def _trickle():
285
+ for _ in range(20):
286
+ await asyncio.sleep(0.03)
287
+ session.dispatch_avio_frame(_reply())
288
+
289
+ feeder = asyncio.create_task(_trickle())
290
+ loop = asyncio.get_running_loop()
291
+ started = loop.time()
292
+ got = await session.async_avio_request_all(
293
+ SETSTREAMCTRL,
294
+ response_cmd=SETSTREAMCTRL_RESP,
295
+ timeout=1.0,
296
+ more_timeout=1.0,
297
+ budget=0.2,
298
+ )
299
+ feeder.cancel()
300
+ assert 1 <= len(got) < 20
301
+ assert loop.time() - started < 0.5
@@ -243,3 +243,124 @@ async def test_a_session_that_raises_does_not_reach_the_caller():
243
243
 
244
244
  out = await _Camera(_Angry({})).async_get_sd_recordings()
245
245
  assert out is not None and out.records == []
246
+
247
+
248
+ class _PagedSession(_Session):
249
+ """A session that answers the list with every page, like a real camera."""
250
+
251
+ def __init__(self, pages, answers=None):
252
+ super().__init__(answers or {})
253
+ self._pages = pages
254
+
255
+ async def async_avio_request_all(
256
+ self, cmd, payload, *, response_cmd, max_replies, done, **_timeouts
257
+ ):
258
+ self.asked.append((cmd, payload, response_cmd))
259
+ got = []
260
+ for body in self._pages[:max_replies]:
261
+ got.append(_Reply(response_cmd, body))
262
+ if done(got):
263
+ break
264
+ return got
265
+
266
+
267
+ @pytest.mark.asyncio
268
+ async def test_every_page_of_a_long_listing_is_read():
269
+ """Measured 2026-11-01: four pages for one PTZ week, and only the first was read.
270
+
271
+ The first page holds the OLDEST records, so stopping there hid every
272
+ recording newer than the first 255.
273
+ """
274
+ session = _PagedSession(
275
+ [
276
+ _page([_record(day=5)], total=3, index=0, end_flag=0),
277
+ _page([_record(day=6)], total=3, index=1, end_flag=0),
278
+ _page([_record(day=7)], total=3, index=2, end_flag=1),
279
+ ]
280
+ )
281
+ out = await _Camera(session).async_get_sd_recordings()
282
+ assert [r.day for r in out.records] == [5, 6, 7]
283
+ assert out.complete is True
284
+
285
+
286
+ @pytest.mark.asyncio
287
+ async def test_pages_that_arrive_out_of_order_are_put_back_in_order():
288
+ session = _PagedSession(
289
+ [
290
+ _page([_record(day=6)], total=2, index=1, end_flag=1),
291
+ _page([_record(day=5)], total=2, index=0, end_flag=0),
292
+ ]
293
+ )
294
+ out = await _Camera(session).async_get_sd_recordings()
295
+ assert [r.day for r in out.records] == [5, 6]
296
+ assert out.complete is True
297
+
298
+
299
+ @pytest.mark.asyncio
300
+ async def test_a_page_sent_twice_is_counted_once():
301
+ session = _PagedSession(
302
+ [
303
+ _page([_record(day=5)], total=2, index=0, end_flag=0),
304
+ _page([_record(day=5)], total=2, index=0, end_flag=0),
305
+ _page([_record(day=6)], total=2, index=1, end_flag=1),
306
+ ]
307
+ )
308
+ out = await _Camera(session).async_get_sd_recordings()
309
+ assert [r.day for r in out.records] == [5, 6]
310
+ assert out.complete is True
311
+
312
+
313
+ @pytest.mark.asyncio
314
+ async def test_a_missing_page_before_the_end_is_reported_incomplete():
315
+ session = _PagedSession(
316
+ [
317
+ _page([_record(day=5)], total=3, index=0, end_flag=0),
318
+ _page([_record(day=7)], total=3, index=2, end_flag=1),
319
+ ]
320
+ )
321
+ out = await _Camera(session).async_get_sd_recordings()
322
+ assert [r.day for r in out.records] == [5, 7]
323
+ assert out.complete is False
324
+
325
+
326
+ @pytest.mark.asyncio
327
+ async def test_pages_that_stop_before_the_end_are_reported_incomplete():
328
+ session = _PagedSession(
329
+ [
330
+ _page([_record(day=5)], total=3, index=0, end_flag=0),
331
+ _page([_record(day=6)], total=3, index=1, end_flag=0),
332
+ ]
333
+ )
334
+ out = await _Camera(session).async_get_sd_recordings()
335
+ assert [r.day for r in out.records] == [5, 6]
336
+ assert out.complete is False
337
+ assert out.answered is True
338
+
339
+
340
+ @pytest.mark.asyncio
341
+ async def test_a_late_page_of_another_listing_is_not_mixed_in():
342
+ """Every page of one reply declares the same page count; a stray does not."""
343
+ session = _PagedSession(
344
+ [
345
+ _page([_record(day=5)], total=2, index=0, end_flag=0),
346
+ _page([_record(day=1)], total=4, index=1, end_flag=0),
347
+ _page([_record(day=6)], total=2, index=1, end_flag=1),
348
+ ]
349
+ )
350
+ out = await _Camera(session).async_get_sd_recordings()
351
+ assert [r.day for r in out.records] == [5, 6]
352
+ assert out.complete is True
353
+
354
+
355
+ @pytest.mark.asyncio
356
+ async def test_an_end_page_first_with_no_page_count_does_not_end_the_listing():
357
+ """With no declared count, the pages before the end-flag page are still owed."""
358
+ session = _PagedSession(
359
+ [
360
+ _page([_record(day=6)], total=0, index=1, end_flag=1),
361
+ _page([_record(day=5)], total=0, index=0, end_flag=0),
362
+ ]
363
+ )
364
+ out = await _Camera(session).async_get_sd_recordings()
365
+ assert [r.day for r in out.records] == [5, 6]
366
+ assert out.complete is True