python-aidot-cameras 1.0.0rc65__tar.gz → 1.0.0rc67__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.0rc65/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc67}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/recording.py +15 -4
  3. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sd_playback.py +87 -7
  4. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/webrtc_open.py +98 -21
  5. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/pyproject.toml +1 -1
  6. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  7. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_recorder_wiring.py +34 -0
  8. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_dtls.py +127 -6
  9. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/LICENSE +0 -0
  10. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/README.md +0 -0
  12. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/__init__.py +0 -0
  13. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/__main__.py +0 -0
  14. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_log_redaction.py +0 -0
  15. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_upstream.py +0 -0
  16. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/__init__.py +0 -0
  17. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  18. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  20. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  21. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  22. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  23. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  24. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  25. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  26. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  27. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  28. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  29. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  30. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  31. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  32. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  33. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  34. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  35. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  36. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  37. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  38. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  39. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  40. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  41. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  42. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  43. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  44. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  45. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  46. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  47. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  48. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  49. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  50. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/__init__.py +0 -0
  51. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/aac_track.py +0 -0
  52. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/agc.py +0 -0
  53. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/client.py +0 -0
  54. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/constants.py +0 -0
  55. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/controls.py +0 -0
  56. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/go2rtc.py +0 -0
  57. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/h264_sps.py +0 -0
  58. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/health.py +0 -0
  59. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/hls_ts.py +0 -0
  60. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/hwaccel.py +0 -0
  61. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/lan_control.py +0 -0
  62. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/models.py +0 -0
  63. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/protocol.py +0 -0
  64. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/rtp_h264.py +0 -0
  65. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/rtsp_publish.py +0 -0
  66. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sd_events.py +0 -0
  67. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sd_listing.py +0 -0
  68. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sdes.py +0 -0
  69. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sdes_open.py +0 -0
  70. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/ts_fanout.py +0 -0
  71. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/ts_tee.py +0 -0
  72. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/webrtc.py +0 -0
  73. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/client.py +0 -0
  74. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/cloud_auth.py +0 -0
  75. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/const.py +0 -0
  76. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/credentials.py +0 -0
  77. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/crypto.py +0 -0
  78. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/device_client.py +0 -0
  79. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/discover.py +0 -0
  80. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/exceptions.py +0 -0
  81. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/g711.py +0 -0
  82. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/lan_retry.py +0 -0
  83. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/py.typed +0 -0
  84. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/settings.py +0 -0
  85. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  86. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  87. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  88. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/requires.txt +0 -0
  89. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  90. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/setup.cfg +0 -0
  91. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_aac_track.py +0 -0
  92. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
  93. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
  94. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_aioice_compat.py +0 -0
  95. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_alarm_event.py +0 -0
  96. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_answer_pt_kinds.py +0 -0
  97. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_answer_sdp_video_pt.py +0 -0
  98. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_answer_section_selection.py +0 -0
  99. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_arrival_timestamps.py +0 -0
  100. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_audio_mux_offline.py +0 -0
  101. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_auth_error_routing.py +0 -0
  102. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_auto_alarm.py +0 -0
  103. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_auto_light_settings.py +0 -0
  104. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  105. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_header_single_layout.py +0 -0
  106. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_request.py +0 -0
  107. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_response_parse.py +0 -0
  108. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_response_router.py +0 -0
  109. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_backoff.py +0 -0
  110. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_cap_spans_sessions.py +0 -0
  111. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_detection.py +0 -0
  112. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_evidence_corroboration.py +0 -0
  113. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  114. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_wake_settles_before_the_command.py +0 -0
  115. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ble_gateway_is_gone.py +0 -0
  116. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_bridge_state_sentinels.py +0 -0
  117. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_busy_backoff.py +0 -0
  118. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_camera_properties.py +0 -0
  119. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_canary_log_names_the_camera.py +0 -0
  120. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
  121. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_count.py +0 -0
  122. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_error_log_names_the_exception.py +0 -0
  123. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_error_severity.py +0 -0
  124. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_plan.py +0 -0
  125. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_constants_imports_resolve.py +0 -0
  126. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_control_verdicts.py +0 -0
  127. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_declared_properties.py +0 -0
  128. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_deferred_hardening.py +0 -0
  129. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_detection_types.py +0 -0
  130. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_device_cmd_seq_match.py +0 -0
  131. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_device_login_guard.py +0 -0
  132. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_device_user_info_cache.py +0 -0
  133. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
  134. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_ts_keyframe_signal.py +0 -0
  135. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_ts_serve.py +0 -0
  136. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_ts_serve_only_for_http.py +0 -0
  137. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dormant_camera_cleanup.py +0 -0
  138. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_drain_is_cancellable.py +0 -0
  139. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_mux_terminal_write.py +0 -0
  140. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_not_ready_burst.py +0 -0
  141. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
  142. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_pinning.py +0 -0
  143. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_refusal_after_the_echo.py +0 -0
  144. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_resolution_ack.py +0 -0
  145. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_serve_decode_skip.py +0 -0
  146. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_serve_ice_wait.py +0 -0
  147. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_serve_open_timeout.py +0 -0
  148. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_skip_signaling_wait.py +0 -0
  149. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_slow_probe.py +0 -0
  150. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_stdout_producer.py +0 -0
  151. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_tap_states_no_reorder.py +0 -0
  152. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_ts_copy_recording.py +0 -0
  153. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_video_presence.py +0 -0
  154. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_egress_guard.py +0 -0
  155. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_env_seams.py +0 -0
  156. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_every_exception_is_exported.py +0 -0
  157. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  158. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
  159. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  160. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_first_media_abandon_rules.py +0 -0
  161. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_futile_keepalive.py +0 -0
  162. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  163. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_go2rtc.py +0 -0
  164. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_go2rtc_cli.py +0 -0
  165. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_h264_decode_rate_limit.py +0 -0
  166. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_h264_sps.py +0 -0
  167. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_health_line.py +0 -0
  168. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_health_line_counts_pictures.py +0 -0
  169. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_highport_nomination.py +0 -0
  170. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_hls_ts_end_to_end.py +0 -0
  171. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_hls_ts_wiring.py +0 -0
  172. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_hwaccel_probe.py +0 -0
  173. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ice_config_cache.py +0 -0
  174. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ice_credentials_match_the_app.py +0 -0
  175. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ice_peer_reflexive.py +0 -0
  176. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_idle_release_both_loops.py +0 -0
  177. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ipc_envelope_single_builder.py +0 -0
  178. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_keepalive_renew.py +0 -0
  179. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_key_restart_sdp.py +0 -0
  180. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_keyframe_prompter.py +0 -0
  181. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_control.py +0 -0
  182. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_login_accessors.py +0 -0
  183. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  184. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_short_sessions.py +0 -0
  185. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
  186. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_stops_on_close.py +0 -0
  187. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_sweep_only_with_lights.py +0 -0
  188. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_late_repeats_are_dropped.py +0 -0
  189. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_light_active_color_mode.py +0 -0
  190. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_session_carries_talk.py +0 -0
  191. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_stream_param.py +0 -0
  192. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_validate_policy.py +0 -0
  193. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_validate_token_refresh.py +0 -0
  194. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_video_canary.py +0 -0
  195. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_liveplay_echo_timeout.py +0 -0
  196. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_local_only_controls.py +0 -0
  197. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_log_hygiene.py +0 -0
  198. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_logging_caps.py +0 -0
  199. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_login_connect_deadline.py +0 -0
  200. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_login_reconnect_backoff.py +0 -0
  201. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_loop_break_ends_the_loop.py +0 -0
  202. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_counted_only_when_plaintext.py +0 -0
  203. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_path_is_reported.py +0 -0
  204. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_socket_rcvbuf.py +0 -0
  205. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_wait_floor.py +0 -0
  206. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_motion_poll.py +0 -0
  207. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_client_id_is_exact.py +0 -0
  208. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_credential_refetch.py +0 -0
  209. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_credential_self_heal.py +0 -0
  210. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_password_not_persisted.py +0 -0
  211. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_session_reconnect.py +0 -0
  212. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  213. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mypy_new_findings.py +0 -0
  214. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_nack.py +0 -0
  215. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  216. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_narrow_pc_ice.py +0 -0
  217. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_key_material_in_logs.py +0 -0
  218. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
  219. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_self_referential_go2rtc.py +0 -0
  220. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_unbounded_hex_dumps.py +0 -0
  221. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_undefined_names.py +0 -0
  222. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
  223. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_off_subnet_candidate.py +0 -0
  224. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  225. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_offline_keepalive_pause.py +0 -0
  226. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_on_frame_is_an_av_frame.py +0 -0
  227. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_open_errors_are_aidot_errors.py +0 -0
  228. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_open_fail_logger.py +0 -0
  229. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_open_gate_delay.py +0 -0
  230. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_peer_id_client_type.py +0 -0
  231. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_peerid_reuse.py +0 -0
  232. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_persistent_mqtt.py +0 -0
  233. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_persistent_mqtt_robustness.py +0 -0
  234. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_phase_returns_reach_the_caller.py +0 -0
  235. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_phase_state_is_complete.py +0 -0
  236. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_plaintext_mqtt_refused.py +0 -0
  237. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_post_merge_hardening.py +0 -0
  238. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  239. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_public_signature_matches_impl.py +0 -0
  240. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_publish_target_readiness.py +0 -0
  241. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_quality_campaign_in_session.py +0 -0
  242. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_reboot_action.py +0 -0
  243. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_recent_recordings.py +0 -0
  244. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_region_allowlist.py +0 -0
  245. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_remb.py +0 -0
  246. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  247. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
  248. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_resent_frames_are_dropped.py +0 -0
  249. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_resolution_persists.py +0 -0
  250. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_restart_cycle.py +0 -0
  251. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_retry_policy.py +0 -0
  252. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_review_lan_retry_and_graces.py +0 -0
  253. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_rtp_h264.py +0 -0
  254. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_rtsp_publish.py +0 -0
  255. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_rtsp_publish_go2rtc.py +0 -0
  256. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_scripts_importable.py +0 -0
  257. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sctp_init_ack_tsn.py +0 -0
  258. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sctp_reassembly.py +0 -0
  259. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sctp_sack.py +0 -0
  260. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_card_presence.py +0 -0
  261. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_event_decode.py +0 -0
  262. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_event_probe_payloads.py +0 -0
  263. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_listing.py +0 -0
  264. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback.py +0 -0
  265. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_clock.py +0 -0
  266. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_controls.py +0 -0
  267. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_entry.py +0 -0
  268. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_media_hold.py +0 -0
  269. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_open.py +0 -0
  270. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_avio_dispatch.py +0 -0
  271. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_bridge_break_gate.py +0 -0
  272. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_connection_mode.py +0 -0
  273. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
  274. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_fast_liveplay.py +0 -0
  275. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  276. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_first_media_stall_report.py +0 -0
  277. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_ice_teardown.py +0 -0
  278. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_idle_release.py +0 -0
  279. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_nack_bridge.py +0 -0
  280. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_offer_video_pin.py +0 -0
  281. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_open_cleanup.py +0 -0
  282. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_open_yields_the_loop.py +0 -0
  283. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_pinned_codec_retry.py +0 -0
  284. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_sctp_abort.py +0 -0
  285. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_serve_audio.py +0 -0
  286. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_serve_cmd.py +0 -0
  287. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_serve_stderr_drain.py +0 -0
  288. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_sprop.py +0 -0
  289. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_talk.py +0 -0
  290. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_teardown_exit_log.py +0 -0
  291. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_terminal_ack.py +0 -0
  292. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_terminal_ack_latency.py +0 -0
  293. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_watchdog.py +0 -0
  294. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdp_narrowing_properties.py +0 -0
  295. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdp_payload_type_narrowing.py +0 -0
  296. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_security_hardening.py +0 -0
  297. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  298. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serializable_login_info.py +0 -0
  299. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
  300. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_input_timeout.py +0 -0
  301. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_loop_releases_on_error.py +0 -0
  302. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_loss_tolerance.py +0 -0
  303. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_pt_respects_the_pin.py +0 -0
  304. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_relay.py +0 -0
  305. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  306. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_session_mode_seq.py +0 -0
  307. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_session_stats.py +0 -0
  308. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_session_stop_twice.py +0 -0
  309. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_settings_registry.py +0 -0
  310. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_shared_house_seam.py +0 -0
  311. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  312. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_skip_guard.py +0 -0
  313. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_snapshot_cancellation.py +0 -0
  314. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_speak.py +0 -0
  315. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_capture_gate.py +0 -0
  316. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_staleness.py +0 -0
  317. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_unstable_public.py +0 -0
  318. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  319. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_srtp_rx_rekey.py +0 -0
  320. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_srtp_tx_key_note.py +0 -0
  321. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stable_terminal_id.py +0 -0
  322. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_nudge_policy.py +0 -0
  323. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  324. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  325. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  326. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  327. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  328. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_status_and_trace_channels.py +0 -0
  329. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_cap.py +0 -0
  330. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_drain_ownership.py +0 -0
  331. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_idle.py +0 -0
  332. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_slot_cap.py +0 -0
  333. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_teardown.py +0 -0
  334. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stun_attr_single_builder.py +0 -0
  335. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_swallowed_exception_names.py +0 -0
  336. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_talk.py +0 -0
  337. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  338. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_tap_to_mux_end_to_end.py +0 -0
  339. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_terminal_ack.py +0 -0
  340. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_tmmbr.py +0 -0
  341. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_token_file_robustness.py +0 -0
  342. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_token_refresh.py +0 -0
  343. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_transport_failure_reporting.py +0 -0
  344. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ts_fanout.py +0 -0
  345. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ts_tee.py +0 -0
  346. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_turn_create_permission.py +0 -0
  347. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_turn_target_shared.py +0 -0
  348. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
  349. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
  350. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_upstream_compat.py +0 -0
  351. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_upstream_seam_adapter.py +0 -0
  352. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_upstream_secrets_redacted.py +0 -0
  353. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_validator_shares_one_token.py +0 -0
  354. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_vendored_aiortc_integrity.py +0 -0
  355. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_presence_needs_a_keyframe.py +0 -0
  356. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_profile_log.py +0 -0
  357. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_pts_dts.py +0 -0
  358. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_ts_unwrap.py +0 -0
  359. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_viewer_check_throttle.py +0 -0
  360. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_viewer_presence.py +0 -0
  361. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_viewer_present_push_mode.py +0 -0
  362. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_wait_or_event.py +0 -0
  363. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_wake_gate_camera_evidence.py +0 -0
  364. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_wake_readiness_retry.py +0 -0
  365. {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/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.0rc65
3
+ Version: 1.0.0rc67
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
@@ -55,6 +55,7 @@ class TsCopyRecorder:
55
55
  path: str,
56
56
  device_id: Optional[str] = None,
57
57
  video_codec: Optional[str] = "h264",
58
+ file=None,
58
59
  ) -> None:
59
60
  self._path = path
60
61
  # None: the first keyframe names it (SD playback, H264 or H265).
@@ -67,10 +68,15 @@ class TsCopyRecorder:
67
68
  self._thread: Optional[threading.Thread] = None
68
69
  self._state: dict = {}
69
70
  self._stopped = False
70
- # Opened here, before the session connects, so an unwritable path fails
71
- # while there is nothing to clean up. Held open until the mux thread is
72
- # done with it - no single block encloses that, so no context manager.
73
- self._file = open(self._path, "wb") # noqa: SIM115
71
+ # Called first thing in stop(), for whoever still holds packets for
72
+ # this recording (the SD playback tap's reorder buffer).
73
+ self.on_stop: "list" = []
74
+ # Opened before the session connects, so an unwritable path fails
75
+ # while there is nothing to clean up: by the caller in an executor
76
+ # (``file``; an open() on an event loop blocks it), or here. Held open
77
+ # until the mux thread is done with it - no single block encloses
78
+ # that, so no context manager.
79
+ self._file = file if file is not None else open(self._path, "wb") # noqa: SIM115
74
80
 
75
81
  async def start(self) -> None:
76
82
  if self._thread is not None or self._stopped:
@@ -93,6 +99,11 @@ class TsCopyRecorder:
93
99
  if self._stopped:
94
100
  return
95
101
  self._stopped = True
102
+ for _cb in self.on_stop:
103
+ try:
104
+ _cb()
105
+ except Exception:
106
+ _LOGGER.debug("recording stop callback failed", exc_info=True)
96
107
  if self._thread is None:
97
108
  self._close()
98
109
  return
@@ -597,37 +597,117 @@ def answer_h265_parameter_sets(sdp: str, pt: Optional[int]) -> "list[bytes]":
597
597
  return out
598
598
 
599
599
 
600
+ class SeqReorder:
601
+ """Puts one RTP stream back in sequence order, waiting a little for gaps.
602
+
603
+ A packet the camera resends after a NACK arrives after the ones that
604
+ followed it; the access-unit assembler takes packets in order, so they
605
+ are held here until the gap fills or ``max_wait_s`` passes, then handed
606
+ on in order (a gap given up on reaches the assembler as a gap, and its
607
+ unit is handed on damaged). ``push`` returns what is ready; ``flush``
608
+ returns the rest at the end.
609
+ """
610
+
611
+ #: Most packets held; past this the oldest gap is given up at once.
612
+ MAX_HELD = 1500
613
+
614
+ def __init__(self, max_wait_s: float = 0.3) -> None:
615
+ self.max_wait_s = max_wait_s
616
+ self._next: "Optional[int]" = None
617
+ self._held: "dict[int, tuple[float, Any]]" = {}
618
+ self.late = 0 # arrived after its gap was given up
619
+
620
+ def push(self, seq: int, item: Any, now: float) -> "list":
621
+ if self._next is None:
622
+ self._next = seq
623
+ if (seq - self._next) & 0x8000: # before the next expected one
624
+ self.late += 1
625
+ return []
626
+ self._held.setdefault(seq, (now, item))
627
+ return self._release(now)
628
+
629
+ def _release(self, now: float) -> "list":
630
+ out = []
631
+ while True:
632
+ while self._next in self._held:
633
+ out.append(self._held.pop(self._next)[1])
634
+ self._next = (self._next + 1) & 0xFFFF
635
+ if not self._held:
636
+ return out
637
+ oldest = min(t for t, _ in self._held.values())
638
+ if now - oldest < self.max_wait_s and len(self._held) < self.MAX_HELD:
639
+ return out
640
+ # Give up on the gap: carry on from the earliest packet held.
641
+ self._next = min(self._held, key=lambda s: (s - self._next) & 0xFFFF)
642
+
643
+ def flush(self) -> "list":
644
+ out = []
645
+ while self._held:
646
+ if self._next not in self._held:
647
+ self._next = min(self._held, key=lambda s: (s - self._next) & 0xFFFF)
648
+ out.append(self._held.pop(self._next)[1])
649
+ self._next = (self._next + 1) & 0xFFFF
650
+ return out
651
+
652
+
600
653
  class H265AccessUnits:
601
654
  """RTP payloads (RFC 7798) in, Annex B access units out.
602
655
 
603
656
  ``feed`` takes one packet's sequence number, RTP timestamp, marker bit and
604
657
  payload, and returns the access units it completed as
605
658
  ``(bytes, rtp_timestamp, is_keyframe)`` - the shape the DTLS recorder's
606
- video queue takes. A unit with a sequence gap in it is dropped rather than
607
- handed on damaged. ``parameter_sets`` (from the answer's fmtp) go in front
608
- of a keyframe that does not carry its own VPS.
659
+ video queue takes. A unit with a sequence gap in it is handed on with
660
+ what arrived (``damaged`` counts them): every later frame up to the next
661
+ keyframe refers to it, so dropping it made all of those undecodable - an
662
+ A000088 playback lost 122 frames to one lost packet (2026-10-29), where
663
+ the decoder conceals a damaged frame and carries on. ``keep_damaged=False``
664
+ drops them instead (``dropped``). ``parameter_sets`` (from the answer's
665
+ fmtp) go in front of a keyframe that does not carry its own VPS.
609
666
  """
610
667
 
611
- def __init__(self, parameter_sets: "Optional[list[bytes]]" = None) -> None:
668
+ def __init__(
669
+ self,
670
+ parameter_sets: "Optional[list[bytes]]" = None,
671
+ *,
672
+ keep_damaged: bool = True,
673
+ ) -> None:
612
674
  self.parameter_sets = list(parameter_sets or [])
675
+ self.keep_damaged = keep_damaged
613
676
  self._ts: "Optional[int]" = None
614
677
  self._nals: "list[bytes]" = []
615
678
  self._fu: "Optional[bytearray]" = None
616
679
  self._last_seq: "Optional[int]" = None
617
680
  self._broken = False
618
681
  self.dropped = 0
682
+ self.damaged = 0
683
+ # The camera sends a recording's last frame twice, under a new
684
+ # timestamp (A000088, 2026-10-30): a decoder then sees the same picture
685
+ # order count twice. A unit identical to the one before is not handed
686
+ # on (``repeats`` counts them).
687
+ self.repeats = 0
688
+ self._last_unit: "Optional[bytes]" = None
619
689
 
620
690
  def _emit(self) -> "list[tuple[bytes, int, bool]]":
621
691
  out = []
622
692
  if self._fu is not None:
623
693
  self._broken = True # a fragment never finished
624
- if self._nals and not self._broken and self._ts is not None:
694
+ if self.keep_damaged and len(self._fu) > 2:
695
+ self._nals.append(bytes(self._fu)) # what arrived of it
696
+ keep = not self._broken or self.keep_damaged
697
+ if self._nals and keep and self._ts is not None:
625
698
  types = [(n[0] >> 1) & 0x3F for n in self._nals if n]
626
699
  kf = any(16 <= t <= 21 for t in types)
627
700
  nals = self._nals
628
701
  if kf and 32 not in types and self.parameter_sets:
629
702
  nals = self.parameter_sets + nals
630
- out.append((b"".join(_NAL_SC + n for n in nals), self._ts, kf))
703
+ unit = b"".join(_NAL_SC + n for n in nals)
704
+ if unit == self._last_unit:
705
+ self.repeats += 1
706
+ else:
707
+ out.append((unit, self._ts, kf))
708
+ self._last_unit = unit
709
+ if self._broken:
710
+ self.damaged += 1
631
711
  elif self._nals or self._broken:
632
712
  self.dropped += 1
633
713
  self._nals, self._fu, self._broken = [], None, False
@@ -665,7 +745,7 @@ class H265AccessUnits:
665
745
  while i + 2 <= len(payload):
666
746
  n = struct.unpack_from("!H", payload, i)[0]
667
747
  if n == 0 or i + 2 + n > len(payload):
668
- self._broken = True # truncated or corrupt
748
+ self._broken = True # truncated or corrupt: the rest is lost
669
749
  break
670
750
  self._nals.append(bytes(payload[i + 2 : i + 2 + n]))
671
751
  i += 2 + n
@@ -295,7 +295,8 @@ def _install_sd_h265_tap(pc, h265, status) -> int:
295
295
  PCMU by number, but its answer's audio was PCMA alone, so nothing else
296
296
  claims it. Returns the routers tapped.
297
297
  """
298
- from .sd_playback import starts_h265_stream
298
+ from .._vendor.aiortc.rtcrtpreceiver import NackGenerator
299
+ from .sd_playback import SeqReorder, starts_h265_stream
299
300
 
300
301
  if h265 is None:
301
302
  return 0
@@ -308,19 +309,24 @@ def _install_sd_h265_tap(pc, h265, status) -> int:
308
309
  routers.append(router)
309
310
 
310
311
  def _make(router):
311
- def _tap(packet) -> None:
312
- pt = h265["pt"]
313
- if pt is None:
314
- if packet.payload_type in router.payload_type_table or not (
315
- starts_h265_stream(packet.payload)
316
- ):
317
- return
318
- h265["pt"] = pt = packet.payload_type
319
- status(f"SD playback: taking pt={pt} as the recording's H265")
320
- if packet.payload_type != pt:
321
- return
312
+ # A video receiver on this transport sends the NACKs: aiortc only
313
+ # asks for resends on streams it routes, and this one it does not.
314
+ rx = None
315
+ for t in pc.getTransceivers():
316
+ r = t.receiver
317
+ if getattr(getattr(r, "transport", None), "_rtp_router", None) is router:
318
+ if rx is None or getattr(t, "kind", None) == "video":
319
+ rx = r
320
+ reorder = SeqReorder()
321
+ nack_gen = NackGenerator()
322
+ # The loop holds tasks weakly: a NACK not referenced here can be
323
+ # collected before it is sent.
324
+ nack_tasks: "set" = set()
325
+
326
+ def _feed(packet) -> None:
322
327
  units = h265["units"]
323
328
  lost_before = units.dropped
329
+ damaged_before = units.damaged
324
330
  for data, ts, kf in units.feed(
325
331
  packet.sequence_number,
326
332
  packet.timestamp,
@@ -335,6 +341,48 @@ def _install_sd_h265_tap(pc, h265, status) -> int:
335
341
  status("SD playback: the recorder fell behind - H265 dropped")
336
342
  if units.dropped and not lost_before:
337
343
  status("SD playback: an H265 frame with a lost packet was dropped")
344
+ if units.damaged and not damaged_before:
345
+ status(
346
+ "SD playback: an H265 frame with a lost packet was passed on"
347
+ " damaged (the decoder conceals it)"
348
+ )
349
+
350
+ def _flush() -> None:
351
+ for held in reorder.flush():
352
+ _feed(held)
353
+
354
+ on_stop = getattr(h265["recorder"], "on_stop", None)
355
+ if on_stop is not None:
356
+ on_stop.append(_flush)
357
+
358
+ def _tap(packet) -> None:
359
+ pt = h265["pt"]
360
+ if pt is None:
361
+ if packet.payload_type in router.payload_type_table or not (
362
+ starts_h265_stream(packet.payload)
363
+ ):
364
+ return
365
+ h265["pt"] = pt = packet.payload_type
366
+ status(f"SD playback: taking pt={pt} as the recording's H265")
367
+ if packet.payload_type != pt:
368
+ return
369
+ # Ask for what never arrived; the resend is put back in place
370
+ # by the reorder buffer before the assembler sees it.
371
+ if nack_gen.add(packet) and rx is not None and nack_gen.missing:
372
+ h265["nacks"] = h265.get("nacks", 0) + 1
373
+ if h265["nacks"] == 1:
374
+ status("SD playback: asking the camera to resend lost H265 (NACK)")
375
+ try:
376
+ task = asyncio.get_running_loop().create_task(
377
+ rx._send_rtcp_nack(packet.ssrc, sorted(nack_gen.missing))
378
+ )
379
+ except RuntimeError:
380
+ pass # no loop: nothing to send on
381
+ else:
382
+ nack_tasks.add(task)
383
+ task.add_done_callback(nack_tasks.discard)
384
+ for ready in reorder.push(packet.sequence_number, packet, time.monotonic()):
385
+ _feed(ready)
338
386
 
339
387
  return _tap
340
388
 
@@ -3155,14 +3203,24 @@ async def _dtls_open_peer_connection_setup(self, ps: _DtlsOpenState) -> None:
3155
3203
  track,
3156
3204
  )
3157
3205
 
3158
- ps.recorder = self._attach_file_recorder(
3159
- ps.pc,
3160
- ps.output_path,
3161
- ps.on_frame,
3162
- # SD playback: H264 or H265, whichever the recording is; the first
3163
- # keyframe names it.
3164
- video_codec=None if ps._sd_playback is not None else "h264",
3165
- )
3206
+ # Opened off the event loop: a caller such as Home Assistant flags (and
3207
+ # may be stalled by) a blocking open() on it. The recorder takes the open
3208
+ # file, so an unwritable path still fails before the session connects.
3209
+ _rec_file = await _open_ts_output(ps.output_path)
3210
+ try:
3211
+ ps.recorder = self._attach_file_recorder(
3212
+ ps.pc,
3213
+ ps.output_path,
3214
+ ps.on_frame,
3215
+ # SD playback: H264 or H265, whichever the recording is; the first
3216
+ # keyframe names it.
3217
+ video_codec=None if ps._sd_playback is not None else "h264",
3218
+ file=_rec_file,
3219
+ )
3220
+ finally:
3221
+ # A recorder that took the file closes it; nobody else will.
3222
+ if _rec_file is not None and getattr(ps, "recorder", None) is None:
3223
+ _rec_file.close()
3166
3224
  if ps._sd_playback is not None and ps.recorder is not None:
3167
3225
  from .sd_playback import H265AccessUnits
3168
3226
 
@@ -4693,8 +4751,26 @@ async def _dtls_open_ice_candidates_and_connect(
4693
4751
  return None
4694
4752
 
4695
4753
 
4754
+ async def _open_ts_output(path):
4755
+ """``path`` opened for the TS recorder in an executor, or None: not a .ts
4756
+ output (the recorder is not ours), or the open failed - the recorder then
4757
+ tries itself and reports the error as it always has."""
4758
+ from .recording import is_ts_path
4759
+
4760
+ if not path or not is_ts_path(path):
4761
+ return None
4762
+ try:
4763
+ return await asyncio.get_running_loop().run_in_executor(
4764
+ None, functools.partial(open, path, "wb")
4765
+ )
4766
+ except OSError:
4767
+ return None
4768
+
4769
+
4696
4770
  class _WebRTCOpenMixin:
4697
- def _attach_file_recorder(self, pc, output_path, on_frame, video_codec="h264"):
4771
+ def _attach_file_recorder(
4772
+ self, pc, output_path, on_frame, video_codec="h264", file=None
4773
+ ):
4698
4774
  """The recorder for a DTLS ``output_path``, wired to ``pc``'s tracks.
4699
4775
 
4700
4776
  MPEG-TS paths copy the camera's own streams (``TsCopyRecorder``): the
@@ -4716,6 +4792,7 @@ class _WebRTCOpenMixin:
4716
4792
  output_path,
4717
4793
  device_id=getattr(self, "device_id", None),
4718
4794
  video_codec=video_codec,
4795
+ file=file,
4719
4796
  )
4720
4797
  except OSError as exc:
4721
4798
  _LOGGER.warning(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0rc65"
7
+ version = "1.0.0rc67"
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.0rc65
3
+ Version: 1.0.0rc67
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
@@ -122,3 +122,37 @@ def test_tapping_a_receiver_twice_for_different_queues_is_flagged(caplog):
122
122
  with caplog.at_level("WARNING"):
123
123
  CameraMixin._install_encoded_tap(rcv, second, True, serve=True)
124
124
  assert any("already tapped" in r.getMessage() for r in caplog.records)
125
+
126
+
127
+ def test_the_recording_file_is_opened_off_the_event_loop(tmp_path, monkeypatch):
128
+ # Home Assistant flagged "Detected blocking call to open ... .part.ts"
129
+ # on every DTLS card-recording fetch (2026-10-29): the recorder opened its
130
+ # file in its constructor, on the loop. The open phase now opens it in an
131
+ # executor and hands it over.
132
+ import asyncio
133
+ import threading
134
+
135
+ from aidot_cameras.camera import webrtc_open
136
+ from aidot_cameras.camera.recording import TsCopyRecorder
137
+
138
+ loop_thread, opened_on = [], []
139
+ real_open = open
140
+
141
+ def _spy(*a, **k):
142
+ opened_on.append(threading.current_thread())
143
+ return real_open(*a, **k)
144
+
145
+ async def main():
146
+ loop_thread.append(threading.current_thread())
147
+ monkeypatch.setattr("builtins.open", _spy)
148
+ fh = await webrtc_open._open_ts_output(str(tmp_path / "clip.part.ts"))
149
+ monkeypatch.setattr("builtins.open", real_open)
150
+ assert await webrtc_open._open_ts_output(str(tmp_path / "clip.mp4")) is None
151
+ assert await webrtc_open._open_ts_output(None) is None
152
+ return fh
153
+
154
+ fh = asyncio.run(main())
155
+ assert fh is not None and opened_on and opened_on[0] is not loop_thread[0]
156
+ rec = TsCopyRecorder(str(tmp_path / "clip.part.ts"), file=fh)
157
+ assert rec._file is fh
158
+ fh.close()
@@ -164,9 +164,9 @@ def test_access_units_survive_rtp_and_back():
164
164
  assert [ts for _, ts, _ in got] == [90000 + i * 6000 for i in range(len(aus))]
165
165
 
166
166
 
167
- def test_a_unit_with_a_lost_packet_is_dropped_not_damaged():
167
+ def test_in_drop_mode_a_unit_with_a_lost_packet_is_dropped():
168
168
  aus = _hevc_access_units(4)
169
- units = H265AccessUnits()
169
+ units = H265AccessUnits(keep_damaged=False)
170
170
  got, seq = [], 7
171
171
  for i, au in enumerate(aus):
172
172
  pkts = _packetize(au, i * 6000, seq, mtu=200)
@@ -356,7 +356,7 @@ def test_a_recording_held_as_h264_is_muxed_as_h264():
356
356
 
357
357
  def test_a_lost_last_packet_drops_its_own_unit_not_the_next():
358
358
  aus = _hevc_access_units(5)
359
- units = H265AccessUnits()
359
+ units = H265AccessUnits(keep_damaged=False)
360
360
  got, seq = [], 50
361
361
  for i, au in enumerate(aus):
362
362
  pkts = _packetize(au, i * 6000, seq, mtu=200)
@@ -419,7 +419,7 @@ def test_the_sd_mode_echo_has_one_parser():
419
419
 
420
420
  def test_a_lost_head_drops_the_new_unit_not_the_one_already_emitted():
421
421
  aus = _hevc_access_units(5)
422
- units = H265AccessUnits()
422
+ units = H265AccessUnits(keep_damaged=False)
423
423
  got, seq = [], 300
424
424
  for i, au in enumerate(aus):
425
425
  pkts = _packetize(au, i * 6000, seq, mtu=200)
@@ -434,7 +434,7 @@ def test_a_lost_head_drops_the_new_unit_not_the_one_already_emitted():
434
434
 
435
435
  def test_a_gap_wider_than_the_lost_marker_drops_both_units():
436
436
  aus = _hevc_access_units(5)
437
- units = H265AccessUnits()
437
+ units = H265AccessUnits(keep_damaged=False)
438
438
  got, seq = [], 900
439
439
  for i, au in enumerate(aus):
440
440
  pkts = _packetize(au, i * 6000, seq, mtu=200)
@@ -448,9 +448,130 @@ def test_a_gap_wider_than_the_lost_marker_drops_both_units():
448
448
 
449
449
 
450
450
  def test_a_truncated_aggregation_packet_breaks_its_unit():
451
- units = H265AccessUnits()
451
+ units = H265AccessUnits(keep_damaged=False)
452
452
  vps = bytes([32 << 1, 1, 0x0C])
453
453
  ap = bytes([48 << 1, 1]) + struct.pack("!H", 3) + vps + struct.pack("!H", 1200)
454
454
  ap += bytes([19 << 1, 1]) + b"\x00" * 298
455
455
  assert units.feed(1, 0, True, ap) == []
456
456
  assert units.dropped == 1
457
+
458
+
459
+ def test_by_default_a_damaged_unit_is_handed_on_and_counted():
460
+ # One lost packet used to cost every frame up to the next keyframe
461
+ # (A000088, 2026-10-29: 122 undecodable frames); handed on, the decoder
462
+ # conceals the one damaged frame.
463
+ aus = _hevc_access_units(5)
464
+ units = H265AccessUnits()
465
+ got, seq = [], 600
466
+ for i, au in enumerate(aus):
467
+ pkts = _packetize(au, i * 6000, seq, mtu=200)
468
+ seq += len(pkts)
469
+ for j, p in enumerate(pkts):
470
+ if i == 0 and j == 3:
471
+ continue # one packet inside the first (keyframe) unit
472
+ got += units.feed(*p)
473
+ assert [ts for _, ts, _ in got] == [i * 6000 for i in range(len(aus))]
474
+ assert units.damaged == 1 and units.dropped == 0
475
+ assert got[0][2] is True # still marked a keyframe
476
+ assert got[1:] and [_nals(d) for d, _, _ in got[1:]] == [_nals(a) for a in aus[1:]]
477
+
478
+
479
+ def test_a_unit_sent_twice_is_handed_on_once():
480
+ # The A000088 sent a recording's last frame twice under a new timestamp
481
+ # (2026-10-30); the decoder then reported a duplicate picture order count.
482
+ aus = _hevc_access_units(3)
483
+ units = H265AccessUnits()
484
+ got, seq = [], 10
485
+ for i, au in enumerate([*aus, aus[-1]]):
486
+ pkts = _packetize(au, i * 6000, seq)
487
+ seq += len(pkts)
488
+ for p in pkts:
489
+ got += units.feed(*p)
490
+ assert [ts for _, ts, _ in got] == [0, 6000, 12000]
491
+ assert units.repeats == 1
492
+
493
+
494
+ def test_a_resent_packet_is_put_back_in_place():
495
+ from aidot_cameras.camera.sd_playback import SeqReorder
496
+
497
+ r = SeqReorder(max_wait_s=0.3)
498
+ assert r.push(10, "a", 0.0) == ["a"]
499
+ assert r.push(12, "c", 0.01) == [] # 11 missing: hold 12
500
+ assert r.push(13, "d", 0.02) == []
501
+ assert r.push(11, "b", 0.1) == ["b", "c", "d"] # the resend fills it
502
+
503
+
504
+ def test_a_gap_that_never_fills_is_given_up_in_time():
505
+ from aidot_cameras.camera.sd_playback import SeqReorder
506
+
507
+ r = SeqReorder(max_wait_s=0.3)
508
+ r.push(65534, "a", 0.0)
509
+ assert r.push(1, "d", 0.01) == [] # 65535 and 0 missing, across the wrap
510
+ assert r.push(2, "e", 0.35) == ["d", "e"] # past the wait: carry on
511
+ assert r.push(0, "late", 0.4) == [] and r.late == 1
512
+ r.push(5, "h", 0.5)
513
+ assert r.flush() == ["h"]
514
+
515
+
516
+ def test_the_tap_asks_for_a_lost_packet_and_puts_the_resend_in_place():
517
+ # The A000088 resends what a NACK names (2026-10-30: three plays, no
518
+ # frame damaged or dropped, 0 decode errors, against 147 error lines
519
+ # from one dropped frame before). aiortc does not NACK this stream, as it
520
+ # does not route it; the tap asks, and holds packets until the resend.
521
+ import asyncio
522
+
523
+ from aidot_cameras.camera.webrtc_open import _install_sd_h265_tap
524
+
525
+ sent = []
526
+
527
+ class _Rx:
528
+ def __init__(self, router):
529
+ self.transport = type("T", (), {"_rtp_router": router})()
530
+
531
+ async def _send_rtcp_nack(self, ssrc, lost):
532
+ sent.append((ssrc, list(lost)))
533
+
534
+ class _PcNack:
535
+ def __init__(self, router):
536
+ self._t = [type("Tr", (), {"receiver": _Rx(router), "kind": "video"})()]
537
+
538
+ def getTransceivers(self):
539
+ return self._t
540
+
541
+ def _p(seq, ts, marker, payload):
542
+ p = _pkt(98, seq, ts, marker, payload)
543
+ p.ssrc = 0x1234
544
+ return p
545
+
546
+ rec = type("Rec", (), {"vq": queue.Queue(), "on_stop": []})()
547
+ state = {"pt": 98, "units": H265AccessUnits(), "recorder": rec, "dropped": 0}
548
+ router = _Router()
549
+
550
+ # One IDR in three fragments, and the next unit.
551
+ idr = bytes([19 << 1, 1]) + bytes(range(60))
552
+ fu_ind = bytes([(idr[0] & 0x81) | (49 << 1), idr[1]])
553
+ body = idr[2:]
554
+ frags = [
555
+ fu_ind + bytes([19 | 0x80]) + body[:20],
556
+ fu_ind + bytes([19]) + body[20:40],
557
+ fu_ind + bytes([19 | 0x40]) + body[40:],
558
+ ]
559
+ trail = bytes([1 << 1, 1, 0xBB])
560
+
561
+ async def main():
562
+ assert _install_sd_h265_tap(_PcNack(router), state, lambda m: None) == 1
563
+ router.raw_tap(_p(100, 3000, False, frags[0]))
564
+ router.raw_tap(_p(102, 3000, True, frags[2])) # 101 lost
565
+ router.raw_tap(_p(103, 9000, True, trail))
566
+ await asyncio.sleep(0)
567
+ assert sent == [(0x1234, [101])]
568
+ assert rec.vq.empty() # held for the resend
569
+ router.raw_tap(_p(101, 3000, False, frags[1])) # the resend
570
+ await asyncio.sleep(0)
571
+
572
+ asyncio.run(main())
573
+ first = rec.vq.get_nowait()
574
+ assert first == (b"\x00\x00\x00\x01" + idr, 3000, True, "hevc")
575
+ assert state["units"].damaged == 0 and state["units"].dropped == 0
576
+ assert rec.vq.get_nowait()[1] == 9000
577
+ assert rec.on_stop # the tap flushes what it holds when the recording stops