python-aidot-cameras 1.0.0rc63__tar.gz → 1.0.0rc65__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.0rc63/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc65}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/client.py +11 -1
  3. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_playback.py +204 -15
  4. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes.py +74 -1
  5. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes_open.py +92 -0
  6. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/webrtc_open.py +15 -0
  7. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/pyproject.toml +1 -1
  8. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  9. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  10. python_aidot_cameras-1.0.0rc65/tests/test_sd_playback_clock.py +225 -0
  11. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_dtls.py +39 -0
  12. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_entry.py +45 -1
  13. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_media_hold.py +66 -1
  14. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/LICENSE +0 -0
  15. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/MANIFEST.in +0 -0
  16. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/README.md +0 -0
  17. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/__init__.py +0 -0
  18. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/__main__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_log_redaction.py +0 -0
  20. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_upstream.py +0 -0
  21. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/__init__.py +0 -0
  22. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  23. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  24. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  25. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  26. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  27. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  28. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  29. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  30. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  31. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  32. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  33. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  34. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  35. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  36. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  37. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  38. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  39. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  40. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  41. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  42. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  43. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  44. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  45. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  46. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  47. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  48. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  49. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  50. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  51. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  52. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  53. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  54. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  55. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/__init__.py +0 -0
  56. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/aac_track.py +0 -0
  57. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/agc.py +0 -0
  58. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/constants.py +0 -0
  59. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/controls.py +0 -0
  60. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/go2rtc.py +0 -0
  61. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/h264_sps.py +0 -0
  62. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/health.py +0 -0
  63. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/hls_ts.py +0 -0
  64. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/hwaccel.py +0 -0
  65. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/lan_control.py +0 -0
  66. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/models.py +0 -0
  67. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/protocol.py +0 -0
  68. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/recording.py +0 -0
  69. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/rtp_h264.py +0 -0
  70. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/rtsp_publish.py +0 -0
  71. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_events.py +0 -0
  72. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_listing.py +0 -0
  73. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/ts_fanout.py +0 -0
  74. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/ts_tee.py +0 -0
  75. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/webrtc.py +0 -0
  76. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/client.py +0 -0
  77. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/cloud_auth.py +0 -0
  78. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/const.py +0 -0
  79. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/credentials.py +0 -0
  80. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/crypto.py +0 -0
  81. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/device_client.py +0 -0
  82. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/discover.py +0 -0
  83. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/exceptions.py +0 -0
  84. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/g711.py +0 -0
  85. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/lan_retry.py +0 -0
  86. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/py.typed +0 -0
  87. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/settings.py +0 -0
  88. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  89. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  90. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/requires.txt +0 -0
  91. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  92. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/setup.cfg +0 -0
  93. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_aac_track.py +0 -0
  94. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
  95. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
  96. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_aioice_compat.py +0 -0
  97. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_alarm_event.py +0 -0
  98. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_pt_kinds.py +0 -0
  99. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_sdp_video_pt.py +0 -0
  100. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_section_selection.py +0 -0
  101. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_arrival_timestamps.py +0 -0
  102. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_audio_mux_offline.py +0 -0
  103. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_auth_error_routing.py +0 -0
  104. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_auto_alarm.py +0 -0
  105. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_auto_light_settings.py +0 -0
  106. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  107. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_header_single_layout.py +0 -0
  108. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_request.py +0 -0
  109. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_response_parse.py +0 -0
  110. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_response_router.py +0 -0
  111. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_backoff.py +0 -0
  112. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_cap_spans_sessions.py +0 -0
  113. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_detection.py +0 -0
  114. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_evidence_corroboration.py +0 -0
  115. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  116. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_wake_settles_before_the_command.py +0 -0
  117. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ble_gateway_is_gone.py +0 -0
  118. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_bridge_state_sentinels.py +0 -0
  119. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_busy_backoff.py +0 -0
  120. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_camera_properties.py +0 -0
  121. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_canary_log_names_the_camera.py +0 -0
  122. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
  123. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_count.py +0 -0
  124. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_error_log_names_the_exception.py +0 -0
  125. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_error_severity.py +0 -0
  126. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_plan.py +0 -0
  127. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_constants_imports_resolve.py +0 -0
  128. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_control_verdicts.py +0 -0
  129. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_declared_properties.py +0 -0
  130. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_deferred_hardening.py +0 -0
  131. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_detection_types.py +0 -0
  132. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_device_cmd_seq_match.py +0 -0
  133. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_device_login_guard.py +0 -0
  134. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_device_user_info_cache.py +0 -0
  135. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
  136. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_keyframe_signal.py +0 -0
  137. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_serve.py +0 -0
  138. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_serve_only_for_http.py +0 -0
  139. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dormant_camera_cleanup.py +0 -0
  140. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_drain_is_cancellable.py +0 -0
  141. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_mux_terminal_write.py +0 -0
  142. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_not_ready_burst.py +0 -0
  143. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
  144. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_pinning.py +0 -0
  145. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_recorder_wiring.py +0 -0
  146. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_refusal_after_the_echo.py +0 -0
  147. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_resolution_ack.py +0 -0
  148. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_decode_skip.py +0 -0
  149. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_ice_wait.py +0 -0
  150. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_open_timeout.py +0 -0
  151. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_skip_signaling_wait.py +0 -0
  152. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_slow_probe.py +0 -0
  153. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_stdout_producer.py +0 -0
  154. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_tap_states_no_reorder.py +0 -0
  155. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_ts_copy_recording.py +0 -0
  156. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_video_presence.py +0 -0
  157. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_egress_guard.py +0 -0
  158. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_env_seams.py +0 -0
  159. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_every_exception_is_exported.py +0 -0
  160. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  161. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
  162. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  163. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_first_media_abandon_rules.py +0 -0
  164. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_futile_keepalive.py +0 -0
  165. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  166. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_go2rtc.py +0 -0
  167. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_go2rtc_cli.py +0 -0
  168. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_h264_decode_rate_limit.py +0 -0
  169. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_h264_sps.py +0 -0
  170. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_health_line.py +0 -0
  171. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_health_line_counts_pictures.py +0 -0
  172. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_highport_nomination.py +0 -0
  173. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_hls_ts_end_to_end.py +0 -0
  174. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_hls_ts_wiring.py +0 -0
  175. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_hwaccel_probe.py +0 -0
  176. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_config_cache.py +0 -0
  177. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_credentials_match_the_app.py +0 -0
  178. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_peer_reflexive.py +0 -0
  179. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_idle_release_both_loops.py +0 -0
  180. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ipc_envelope_single_builder.py +0 -0
  181. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_keepalive_renew.py +0 -0
  182. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_key_restart_sdp.py +0 -0
  183. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_keyframe_prompter.py +0 -0
  184. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_control.py +0 -0
  185. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_login_accessors.py +0 -0
  186. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  187. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_short_sessions.py +0 -0
  188. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
  189. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_stops_on_close.py +0 -0
  190. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_sweep_only_with_lights.py +0 -0
  191. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_late_repeats_are_dropped.py +0 -0
  192. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_light_active_color_mode.py +0 -0
  193. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_session_carries_talk.py +0 -0
  194. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_stream_param.py +0 -0
  195. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_validate_policy.py +0 -0
  196. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_validate_token_refresh.py +0 -0
  197. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_video_canary.py +0 -0
  198. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_liveplay_echo_timeout.py +0 -0
  199. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_local_only_controls.py +0 -0
  200. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_log_hygiene.py +0 -0
  201. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_logging_caps.py +0 -0
  202. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_login_connect_deadline.py +0 -0
  203. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_login_reconnect_backoff.py +0 -0
  204. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_loop_break_ends_the_loop.py +0 -0
  205. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_counted_only_when_plaintext.py +0 -0
  206. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_path_is_reported.py +0 -0
  207. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_socket_rcvbuf.py +0 -0
  208. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_wait_floor.py +0 -0
  209. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_motion_poll.py +0 -0
  210. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_client_id_is_exact.py +0 -0
  211. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_credential_refetch.py +0 -0
  212. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_credential_self_heal.py +0 -0
  213. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_password_not_persisted.py +0 -0
  214. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_session_reconnect.py +0 -0
  215. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  216. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mypy_new_findings.py +0 -0
  217. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_nack.py +0 -0
  218. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  219. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_narrow_pc_ice.py +0 -0
  220. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_key_material_in_logs.py +0 -0
  221. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
  222. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_self_referential_go2rtc.py +0 -0
  223. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_unbounded_hex_dumps.py +0 -0
  224. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_undefined_names.py +0 -0
  225. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
  226. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_off_subnet_candidate.py +0 -0
  227. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  228. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_offline_keepalive_pause.py +0 -0
  229. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_on_frame_is_an_av_frame.py +0 -0
  230. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_open_errors_are_aidot_errors.py +0 -0
  231. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_open_fail_logger.py +0 -0
  232. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_open_gate_delay.py +0 -0
  233. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_peer_id_client_type.py +0 -0
  234. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_peerid_reuse.py +0 -0
  235. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_persistent_mqtt.py +0 -0
  236. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_persistent_mqtt_robustness.py +0 -0
  237. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_phase_returns_reach_the_caller.py +0 -0
  238. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_phase_state_is_complete.py +0 -0
  239. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_plaintext_mqtt_refused.py +0 -0
  240. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_post_merge_hardening.py +0 -0
  241. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  242. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_public_signature_matches_impl.py +0 -0
  243. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_publish_target_readiness.py +0 -0
  244. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_quality_campaign_in_session.py +0 -0
  245. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_reboot_action.py +0 -0
  246. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_recent_recordings.py +0 -0
  247. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_region_allowlist.py +0 -0
  248. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_remb.py +0 -0
  249. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  250. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
  251. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_resent_frames_are_dropped.py +0 -0
  252. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_resolution_persists.py +0 -0
  253. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_restart_cycle.py +0 -0
  254. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_retry_policy.py +0 -0
  255. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_review_lan_retry_and_graces.py +0 -0
  256. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_rtp_h264.py +0 -0
  257. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_rtsp_publish.py +0 -0
  258. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_rtsp_publish_go2rtc.py +0 -0
  259. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_scripts_importable.py +0 -0
  260. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_init_ack_tsn.py +0 -0
  261. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_reassembly.py +0 -0
  262. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_sack.py +0 -0
  263. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_card_presence.py +0 -0
  264. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_event_decode.py +0 -0
  265. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_event_probe_payloads.py +0 -0
  266. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_listing.py +0 -0
  267. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback.py +0 -0
  268. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_controls.py +0 -0
  269. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_open.py +0 -0
  270. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_avio_dispatch.py +0 -0
  271. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_bridge_break_gate.py +0 -0
  272. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_connection_mode.py +0 -0
  273. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
  274. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_fast_liveplay.py +0 -0
  275. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  276. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_first_media_stall_report.py +0 -0
  277. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_ice_teardown.py +0 -0
  278. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_idle_release.py +0 -0
  279. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_nack_bridge.py +0 -0
  280. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_offer_video_pin.py +0 -0
  281. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_open_cleanup.py +0 -0
  282. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_open_yields_the_loop.py +0 -0
  283. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_pinned_codec_retry.py +0 -0
  284. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_sctp_abort.py +0 -0
  285. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_audio.py +0 -0
  286. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_cmd.py +0 -0
  287. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_stderr_drain.py +0 -0
  288. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_sprop.py +0 -0
  289. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_talk.py +0 -0
  290. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_teardown_exit_log.py +0 -0
  291. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_terminal_ack.py +0 -0
  292. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_terminal_ack_latency.py +0 -0
  293. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_watchdog.py +0 -0
  294. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdp_narrowing_properties.py +0 -0
  295. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdp_payload_type_narrowing.py +0 -0
  296. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_security_hardening.py +0 -0
  297. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  298. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serializable_login_info.py +0 -0
  299. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
  300. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_input_timeout.py +0 -0
  301. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_loop_releases_on_error.py +0 -0
  302. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_loss_tolerance.py +0 -0
  303. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_pt_respects_the_pin.py +0 -0
  304. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_relay.py +0 -0
  305. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  306. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_session_mode_seq.py +0 -0
  307. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_session_stats.py +0 -0
  308. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_session_stop_twice.py +0 -0
  309. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_settings_registry.py +0 -0
  310. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_shared_house_seam.py +0 -0
  311. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  312. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_skip_guard.py +0 -0
  313. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_snapshot_cancellation.py +0 -0
  314. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_speak.py +0 -0
  315. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_capture_gate.py +0 -0
  316. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_staleness.py +0 -0
  317. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_unstable_public.py +0 -0
  318. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  319. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_srtp_rx_rekey.py +0 -0
  320. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_srtp_tx_key_note.py +0 -0
  321. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stable_terminal_id.py +0 -0
  322. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_nudge_policy.py +0 -0
  323. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  324. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  325. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  326. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  327. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  328. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_status_and_trace_channels.py +0 -0
  329. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_cap.py +0 -0
  330. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_drain_ownership.py +0 -0
  331. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_idle.py +0 -0
  332. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_slot_cap.py +0 -0
  333. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_teardown.py +0 -0
  334. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stun_attr_single_builder.py +0 -0
  335. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_swallowed_exception_names.py +0 -0
  336. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_talk.py +0 -0
  337. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  338. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_tap_to_mux_end_to_end.py +0 -0
  339. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_terminal_ack.py +0 -0
  340. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_tmmbr.py +0 -0
  341. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_token_file_robustness.py +0 -0
  342. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_token_refresh.py +0 -0
  343. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_transport_failure_reporting.py +0 -0
  344. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ts_fanout.py +0 -0
  345. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ts_tee.py +0 -0
  346. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_turn_create_permission.py +0 -0
  347. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_turn_target_shared.py +0 -0
  348. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
  349. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
  350. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_compat.py +0 -0
  351. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_seam_adapter.py +0 -0
  352. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_secrets_redacted.py +0 -0
  353. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_validator_shares_one_token.py +0 -0
  354. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_vendored_aiortc_integrity.py +0 -0
  355. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_presence_needs_a_keyframe.py +0 -0
  356. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_profile_log.py +0 -0
  357. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_pts_dts.py +0 -0
  358. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_ts_unwrap.py +0 -0
  359. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_check_throttle.py +0 -0
  360. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_presence.py +0 -0
  361. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_present_push_mode.py +0 -0
  362. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_wait_or_event.py +0 -0
  363. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_wake_gate_camera_evidence.py +0 -0
  364. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_wake_readiness_retry.py +0 -0
  365. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/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.0rc63
3
+ Version: 1.0.0rc65
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
@@ -1041,9 +1041,13 @@ def _build_sdes_serve_cmd(
1041
1041
  # instead. Moving `-t` to the input side does not help - both placements
1042
1042
  # fail. Only a bounded run gives this up, and a bounded run is a
1043
1043
  # snapshot or the `-f null` drain, never the live stream.
1044
+ # Nor for SD playback (``keep_probe_packets``): a recording arrives
1045
+ # faster than it plays, and its timing is put right afterwards from
1046
+ # the camera's per-frame recording time (sd_playback.PlaybackClock),
1047
+ # which needs the camera's own RTP timestamps in the file.
1044
1048
  *(
1045
1049
  ["-use_wallclock_as_timestamps", "1"]
1046
- if _SERVE_ARRIVAL_TS and not max_seconds
1050
+ if _SERVE_ARRIVAL_TS and not max_seconds and not keep_probe_packets
1047
1051
  else []
1048
1052
  ),
1049
1053
  # ``nobuffer`` drops what ffmpeg reads while probing the input (up to
@@ -2792,6 +2796,12 @@ class CameraMixin(
2792
2796
  Taken from the SDES bridge thread (SESSION_MODE, control commands) as
2793
2797
  well as the event loop (livePlayReq), so read-and-increment is one
2794
2798
  locked step: a repeated seq is a SESSION_MODE the camera ignores.
2799
+
2800
+ Starting again at 100 in every new client is safe: the camera keeps
2801
+ the last accepted seq per session, not per camera. Back-to-back
2802
+ playbacks from fresh clients, each counting from 100 where the one
2803
+ before had already gone past it, had SD mode echoed every time on all
2804
+ three models (2026-10-27), as the app's own restarts rely on.
2795
2805
  """
2796
2806
  with _DSEQ_LOCK:
2797
2807
  d = self._live_dseq
@@ -274,16 +274,6 @@ class _CameraSdPlaybackMixin:
274
274
  record=record,
275
275
  sd_channel=sd_channel,
276
276
  )
277
- peer_id = self.generate_webrtc_peer_id(
278
- live_type=PLAY_CMD_SDCARD,
279
- # Field 4, a literal 0 in the app - not the StreamID, which is
280
- # field 5 (``stream``).
281
- stream_id=0,
282
- sdes=bool(self.is_sdes_camera),
283
- device_id=self.device_id,
284
- client_class=PLAYBACK_CLIENT_CLASS,
285
- stream=PLAYBACK_STREAM,
286
- )
287
277
  return await self.async_open_webrtc_stream(
288
278
  on_frame,
289
279
  timeout=timeout,
@@ -291,11 +281,199 @@ class _CameraSdPlaybackMixin:
291
281
  max_seconds=max_seconds,
292
282
  status_callback=status_callback,
293
283
  rtsp_push_url=rtsp_push_url,
294
- reuse_peer_id=peer_id,
284
+ reuse_peer_id=playback_peer_id(self, sdes=bool(self.is_sdes_camera)),
295
285
  _sd_playback=start,
296
286
  )
297
287
 
298
288
 
289
+ def playback_peer_id(camera: Any, *, sdes: bool) -> str:
290
+ """The peer id a playback open announces, for the SDES or DTLS transport.
291
+
292
+ The SDES-to-DTLS fallback builds a fresh one with ``sdes=False``: the
293
+ SDES id's transport digit is wrong for DTLS, and a live id (class 2,
294
+ PRE_LINK) would get audio alone from an H265-recording camera.
295
+ """
296
+ return camera.generate_webrtc_peer_id(
297
+ live_type=PLAY_CMD_SDCARD,
298
+ # Field 4, a literal 0 in the app - not the StreamID, which is
299
+ # field 5 (``stream``).
300
+ stream_id=0,
301
+ sdes=sdes,
302
+ device_id=camera.device_id,
303
+ client_class=PLAYBACK_CLIENT_CLASS,
304
+ stream=PLAYBACK_STREAM,
305
+ )
306
+
307
+
308
+ # --- The recording's own clock (SDES) ---------------------------------------
309
+ #
310
+ # The camera stamps a recording's RTP as if it had been recorded at its live
311
+ # frame rate and sends it at that pace: an A001064 night recording made at
312
+ # 5 fps (200 ms a frame) went out at 20 fps (50 ms a frame), its 60 s in 15 s,
313
+ # and its sound - sent in real time - stopped when the video ran out
314
+ # (2026-10-29). The vendor app plays it at the right speed because every
315
+ # packet also carries the frame's time in the recording, in an RTP header
316
+ # extension the camera adds once the offer names transport-cc and
317
+ # rtp-stream-id (OfferUtils.processRtp keeps exactly those two extmap lines;
318
+ # the answer then says ``supportRtpExt: 1``). The 11-byte value is a 2-byte
319
+ # playback channel, a flag byte (0xf0) and the time in Unix milliseconds, all
320
+ # big-endian; all ones marks the end of the recording, on the packets the
321
+ # camera sends after its last frame.
322
+
323
+ #: The two header extensions the vendor app's offer keeps, with the ids this
324
+ #: library offers them under.
325
+ EXT_TRANSPORT_CC = (
326
+ "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
327
+ )
328
+ EXT_FRAME_TIME = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
329
+ EXT_TRANSPORT_CC_ID = 3
330
+ EXT_FRAME_TIME_ID = 4
331
+ #: The frame-time value the camera sends once the recording has run out.
332
+ FRAME_TIME_END = 0xFFFFFFFFFFFFFFFF
333
+
334
+ _AUDIO_CLOCKS = {0: 8000, 8: 8000}
335
+
336
+
337
+ def offer_with_frame_times(sdp: str) -> str:
338
+ """``sdp`` asking for the per-frame recording time: both extmap lines in
339
+ every audio and video section, after its mid, and transport-cc feedback
340
+ on each video payload type."""
341
+ extmaps = [
342
+ f"a=extmap:{EXT_TRANSPORT_CC_ID} {EXT_TRANSPORT_CC}",
343
+ f"a=extmap:{EXT_FRAME_TIME_ID} {EXT_FRAME_TIME}",
344
+ ]
345
+ out = []
346
+ kind, pts = "", []
347
+ for line in sdp.split("\r\n"):
348
+ if line.startswith("m="):
349
+ kind = line[2:].split(" ", 1)[0]
350
+ pts = line.split()[3:]
351
+ out.append(line)
352
+ if line.startswith("a=mid:") and kind in ("audio", "video"):
353
+ out.extend(extmaps)
354
+ if kind == "video":
355
+ out.extend(f"a=rtcp-fb:{pt} transport-cc" for pt in pts)
356
+ return "\r\n".join(out)
357
+
358
+
359
+ def rtp_extension(pkt: bytes, ext_id: int) -> "Optional[bytes]":
360
+ """The value of header extension ``ext_id`` in RTP packet ``pkt`` (RFC
361
+ 8285, one- or two-byte form), or None. The header is in the clear under
362
+ SRTP, so this reads a packet still encrypted."""
363
+ if len(pkt) < 12 or not pkt[0] & 0x10:
364
+ return None
365
+ off = 12 + 4 * (pkt[0] & 0x0F)
366
+ if len(pkt) < off + 4:
367
+ return None
368
+ profile = int.from_bytes(pkt[off : off + 2], "big")
369
+ end = off + 4 + 4 * int.from_bytes(pkt[off + 2 : off + 4], "big")
370
+ if end > len(pkt):
371
+ return None
372
+ i = off + 4
373
+ while i < end:
374
+ if profile == 0xBEDE:
375
+ b = pkt[i]
376
+ if b == 0:
377
+ i += 1
378
+ continue
379
+ eid, n, i = b >> 4, (b & 0x0F) + 1, i + 1
380
+ elif profile >> 4 == 0x100 and i + 1 < end:
381
+ if pkt[i] == 0:
382
+ i += 1
383
+ continue
384
+ eid, n, i = pkt[i], pkt[i + 1], i + 2
385
+ else:
386
+ return None
387
+ if eid == ext_id:
388
+ return bytes(pkt[i : i + n])
389
+ i += n
390
+ return None
391
+
392
+
393
+ def frame_time_ms(value: "Optional[bytes]") -> "Optional[int]":
394
+ """The recording time (Unix ms) in a frame-time extension value;
395
+ ``FRAME_TIME_END`` at the end of the recording; None if absent."""
396
+ if value is None or len(value) != 11:
397
+ return None
398
+ return int.from_bytes(value[3:11], "big")
399
+
400
+
401
+ class PlaybackClock:
402
+ """Pairs each stream's RTP timestamps with the recording's own time.
403
+
404
+ Fed every packet the playback forwards; keeps one (rtp timestamp, ms)
405
+ pair per change of recording time, per payload type. ``ended`` turns
406
+ True on the first end-of-recording mark. ``timing()`` is what
407
+ ``retime_playback_ts`` needs.
408
+ """
409
+
410
+ def __init__(self) -> None:
411
+ self._pairs: "dict[int, list[tuple[int, int]]]" = {}
412
+ self.ended = False
413
+
414
+ def observe(self, pkt: bytes) -> bool:
415
+ """Note ``pkt``; True if it is an end-of-recording packet (forward
416
+ nothing of those: the recording is over)."""
417
+ ms = frame_time_ms(rtp_extension(pkt, EXT_FRAME_TIME_ID))
418
+ if ms is None:
419
+ return False
420
+ if ms == FRAME_TIME_END:
421
+ self.ended = True
422
+ return True
423
+ pairs = self._pairs.setdefault(pkt[1] & 0x7F, [])
424
+ if not pairs or pairs[-1][1] != ms:
425
+ pairs.append((int.from_bytes(pkt[4:8], "big"), ms))
426
+ return False
427
+
428
+ def timing(self) -> "dict[str, tuple[float, float]]":
429
+ """Per stream kind (``"video"``, ``"audio"``): (scale, start_s) - each
430
+ second of RTP time is ``scale`` seconds of recording, and the stream
431
+ starts ``start_s`` after the earliest stream does. Empty when no
432
+ frame times arrived."""
433
+ # One stream per kind: the payload type with the most frame times
434
+ # (a stray second video type would otherwise decide by dict order).
435
+ best: "dict[str, int]" = {}
436
+ for pt, pairs in self._pairs.items():
437
+ kind = "audio" if pt in _AUDIO_CLOCKS else "video"
438
+ if len(pairs) > len(self._pairs.get(best.get(kind, -1), ())):
439
+ best[kind] = pt
440
+ firsts = [self._pairs[pt][0][1] for pt in best.values()]
441
+ if not firsts:
442
+ return {}
443
+ origin = min(firsts)
444
+ out = {}
445
+ for kind, pt in best.items():
446
+ pairs = self._pairs[pt]
447
+ clock = _AUDIO_CLOCKS.get(pt, 90000)
448
+ (r0, m0), (r1, m1) = pairs[0], pairs[-1]
449
+ rtp_s = ((r1 - r0) & 0xFFFFFFFF) / clock
450
+ scale = (m1 - m0) / 1000 / rtp_s if rtp_s > 0 else 1.0
451
+ out[kind] = (scale, (m0 - origin) / 1000)
452
+ return out
453
+
454
+
455
+ def retime_argv(
456
+ binary: str, src: str, dst: str, timing: "dict[str, tuple[float, float]]"
457
+ ) -> "list[str]":
458
+ """ffmpeg command that copies ``src`` to ``dst`` with each stream moved
459
+ onto the recording's clock (``PlaybackClock.timing``). An SDES capture's
460
+ G.711 rides a data stream, which takes the audio timing."""
461
+
462
+ def setts(scale: float, start_s: float) -> str:
463
+ def at(ts: str) -> str:
464
+ return f"({ts}-START{ts})*{scale:.6f}+{start_s:.6f}/TB"
465
+
466
+ return f"setts=pts={at('PTS')}:dts={at('DTS')}"
467
+
468
+ argv = [binary, "-hide_banner", "-loglevel", "error", "-nostdin", "-y"]
469
+ argv += ["-i", src, "-map", "0", "-c", "copy"]
470
+ if "video" in timing:
471
+ argv += ["-bsf:v", setts(*timing["video"])]
472
+ if "audio" in timing:
473
+ argv += ["-bsf:a", setts(*timing["audio"]), "-bsf:d", setts(*timing["audio"])]
474
+ return [*argv, "-f", "mpegts", dst]
475
+
476
+
299
477
  # --- DTLS (A000088): the recording's H265 is taken raw ----------------------
300
478
  #
301
479
  # aiortc has no H265, and the camera sends a recording it holds as H265 on a
@@ -459,13 +637,21 @@ class H265AccessUnits:
459
637
  self, seq: int, ts: int, marker: bool, payload: bytes
460
638
  ) -> "list[tuple[bytes, int, bool]]":
461
639
  out = []
462
- gap = self._last_seq is not None and (seq - self._last_seq) & 0xFFFF != 1
640
+ missing = 0
641
+ if self._last_seq is not None:
642
+ missing = (seq - self._last_seq - 1) & 0xFFFF
643
+ gap = missing != 0
463
644
  if self._ts is not None and ts != self._ts:
464
- # What went missing before this packet was the end of the unit
465
- # in hand (or whole units between): that unit is the damaged one.
466
- if gap:
645
+ # A unit in hand that never saw its marker lost at least its last
646
+ # packet; one already emitted at its marker lost nothing. Whatever
647
+ # else went missing is the head of this new unit (its VPS/SPS, or
648
+ # the first slice of a keyframe), so it is not handed on either.
649
+ pending = bool(self._nals) or self._fu is not None
650
+ if gap and pending:
467
651
  self._broken = True
468
652
  out += self._emit()
653
+ if missing > (1 if pending else 0):
654
+ self._broken = True
469
655
  elif gap:
470
656
  self._broken = True
471
657
  self._fu = None
@@ -478,6 +664,9 @@ class H265AccessUnits:
478
664
  i = 2
479
665
  while i + 2 <= len(payload):
480
666
  n = struct.unpack_from("!H", payload, i)[0]
667
+ if n == 0 or i + 2 + n > len(payload):
668
+ self._broken = True # truncated or corrupt
669
+ break
481
670
  self._nals.append(bytes(payload[i + 2 : i + 2 + n]))
482
671
  i += 2 + n
483
672
  elif t == 49 and len(payload) >= 3: # fragmentation unit
@@ -63,6 +63,8 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
63
63
  abort_chan=None,
64
64
  playback_waiter=None,
65
65
  playback=None,
66
+ playback_clock=None,
67
+ playback_output=None,
66
68
  ) -> None:
67
69
  # Which camera this session belongs to, for logging only. Optional so
68
70
  # an existing caller that does not pass it still works; the logs then
@@ -135,6 +137,11 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
135
137
  # channel), for pause, resume and seek; None for live.
136
138
  self._sd_playback = playback
137
139
  self._sd_seek_count = 0
140
+ # SD playback: the recording's own clock as the bridge saw it
141
+ # (sd_playback.PlaybackClock) and the .ts it was written to, which
142
+ # stop() moves onto that clock.
143
+ self._playback_clock = playback_clock
144
+ self._playback_output = playback_output
138
145
  # Shared talk state (dict) for outbound two-way audio, or None when the
139
146
  # session was not opened talk-capable (offer stayed recvonly). Populated
140
147
  # by the bridge (camera audio addr) and by async_start_talk (provider).
@@ -254,6 +261,13 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
254
261
  """time.monotonic() of the camera's last STUN answer, 0.0 if none yet."""
255
262
  return self._ice_progress[0]
256
263
 
264
+ @property
265
+ def playback_ended(self) -> bool:
266
+ """True once the camera has marked the end of the recording this
267
+ session plays (False for live, and before the end)."""
268
+ clock = self._playback_clock
269
+ return bool(clock is not None and clock.ended)
270
+
257
271
  def media_stats(self) -> dict:
258
272
  """Snapshot of media actually forwarded to ffmpeg by the bridge.
259
273
 
@@ -372,6 +386,62 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
372
386
  # others are waiting on.
373
387
  await asyncio.shield(task)
374
388
 
389
+ async def _retime_playback(self) -> None:
390
+ """Move a played recording's .ts onto the recording's own clock.
391
+
392
+ The camera's RTP clock runs at the live frame rate even for a
393
+ recording made slower (sd_playback.PlaybackClock), so the file ffmpeg
394
+ wrote plays fast; the frame-time extension says by how much. Left as
395
+ written when there is nothing to correct (no frame times, a live
396
+ session, an output that is not a .ts) or when the copy fails.
397
+ """
398
+ clock, path = self._playback_clock, self._playback_output
399
+ if clock is None or not path or not str(path).endswith(".ts"):
400
+ return
401
+ timing = clock.timing()
402
+ if not timing or all(
403
+ abs(s - 1.0) < 0.02 and o < 0.05 for s, o in timing.values()
404
+ ):
405
+ return
406
+ import os
407
+ import subprocess
408
+
409
+ from .sd_playback import retime_argv
410
+
411
+ tmp = f"{path}.retime.ts"
412
+ argv = retime_argv("ffmpeg", path, tmp, timing)
413
+
414
+ def _run() -> bool:
415
+ try:
416
+ done = subprocess.run(argv, capture_output=True, timeout=120)
417
+ except Exception:
418
+ return False
419
+ if done.returncode != 0 or not os.path.exists(tmp):
420
+ return False
421
+ os.replace(tmp, path)
422
+ return True
423
+
424
+ try:
425
+ ok = await asyncio.get_running_loop().run_in_executor(None, _run)
426
+ except Exception:
427
+ ok = False
428
+ if not ok:
429
+ try:
430
+ os.unlink(tmp)
431
+ except OSError:
432
+ pass
433
+ _LOGGER.warning(
434
+ "camera %s: the played recording could not be put on its own"
435
+ " clock and plays at the camera's send rate",
436
+ self._device_id,
437
+ )
438
+ return
439
+ _LOGGER.info(
440
+ "camera %s: played recording put on its own clock (%s)",
441
+ self._device_id,
442
+ ", ".join(f"{k} x{s:.2f} from +{o:.1f}s" for k, (s, o) in timing.items()),
443
+ )
444
+
375
445
  async def _stop_once(self) -> None:
376
446
  if self._talk_state is not None:
377
447
  # Ask the bridge to close the camera speaker, then give it a brief
@@ -454,6 +524,7 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
454
524
  except Exception:
455
525
  _LOGGER.debug("swallowed exception in %s", "_stop_once", exc_info=True)
456
526
  self._log_ffmpeg_stderr(stderr_bytes or self._drained_stderr_tail())
527
+ await self._retime_playback()
457
528
  import os
458
529
 
459
530
  try:
@@ -465,7 +536,9 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
465
536
  try:
466
537
  _sock.close()
467
538
  except Exception:
468
- _LOGGER.debug("swallowed exception in %s", "_stop_once", exc_info=True)
539
+ _LOGGER.debug(
540
+ "swallowed exception in %s", "_stop_once", exc_info=True
541
+ )
469
542
  self._outgoing_q.put_nowait(None)
470
543
  try:
471
544
  await asyncio.wait_for(self._mqtt_fut, timeout=_STOP_MQTT_WAIT_S)
@@ -8,6 +8,7 @@ breaks the client<->sdes_open cycle). CameraMixin inherits this mixin, so
8
8
  """
9
9
 
10
10
  import asyncio
11
+ import collections
11
12
  import functools
12
13
  import hashlib as _hashlib_mod
13
14
  import logging
@@ -35,6 +36,7 @@ from .constants import (
35
36
  )
36
37
  from .models import VideoFrame # noqa: F401 - forward-ref annotation
37
38
  from .sdes import SdesSession
39
+ from .sd_playback import EXT_FRAME_TIME, EXT_TRANSPORT_CC
38
40
  from .rtsp_publish import (
39
41
  redact_url,
40
42
  LoopbackRtpPublisher,
@@ -1109,9 +1111,18 @@ def _session_mode_echoed(frame: bytes) -> "Optional[int]":
1109
1111
 
1110
1112
  #: How many packets a playback open holds for its serve (see _new_media_hold).
1111
1113
  #: The serve is up within a second; ~70 packets/s on an A001064 recording.
1114
+ #: The hold is released once the serve binds or 4.5 s after it is launched
1115
+ #: (the bind wait and its fallback), so the cap is several times what one
1116
+ #: open can hold; a drop past it is logged as a warning, since the served
1117
+ #: clip then has a hole after its opening.
1112
1118
  _MEDIA_HOLD_MAX = 4000
1113
1119
 
1114
1120
 
1121
+ #: How many recent sequence numbers per stream a playback remembers, to drop
1122
+ #: a packet that arrives twice. A few seconds of video at 2560x1440.
1123
+ _DUPLICATE_WINDOW = 2048
1124
+
1125
+
1115
1126
  class _MediaHold:
1116
1127
  """Packets for the serve, held in order until it is listening.
1117
1128
 
@@ -1119,6 +1130,14 @@ class _MediaHold:
1119
1130
  loopback port; ``release`` by the open once the serve is bound. The first
1120
1131
  ``send`` (or ``flush``) after the release hands over everything held -
1121
1132
  still on the bridge thread, so nothing can overtake it.
1133
+
1134
+ It also drops an RTP packet already forwarded (same port, SSRC and
1135
+ sequence number). On a host that reaches the camera by two routes every
1136
+ packet can arrive twice: measured on the reference box 2026-10-28, each
1137
+ playback audio packet came twice and video packets up to six times. ffmpeg
1138
+ discards a repeated video packet, but the G.711 is copied as data, so
1139
+ every repeat was written too and the clip's sound ran twice as long as its
1140
+ picture.
1122
1141
  """
1123
1142
 
1124
1143
  def __init__(self, limit: int = 0) -> None:
@@ -1126,6 +1145,31 @@ class _MediaHold:
1126
1145
  self._released = False
1127
1146
  self._limit = limit or _MEDIA_HOLD_MAX
1128
1147
  self.dropped = 0
1148
+ self.duplicates = 0
1149
+ self._seen: "dict" = {}
1150
+ # The recording's own clock, from each packet's frame-time extension
1151
+ # (sd_playback.PlaybackClock); its end mark ends the recording.
1152
+ from .sd_playback import PlaybackClock
1153
+
1154
+ self.clock = PlaybackClock()
1155
+
1156
+ def _repeat(self, pkt: bytes, port: int) -> bool:
1157
+ """Is ``pkt`` an RTP packet this hold has already passed on?"""
1158
+ if len(pkt) < 12 or pkt[0] >> 6 != 2 or 192 <= pkt[1] <= 223:
1159
+ return False # not RTP, or RTCP (payload types 192-223)
1160
+ ssrc = int.from_bytes(pkt[8:12], "big")
1161
+ seq = int.from_bytes(pkt[2:4], "big")
1162
+ seen = self._seen.get((port, ssrc))
1163
+ if seen is None:
1164
+ seen = self._seen[(port, ssrc)] = (set(), collections.deque())
1165
+ recent, order = seen
1166
+ if seq in recent:
1167
+ return True
1168
+ recent.add(seq)
1169
+ order.append(seq)
1170
+ if len(order) > _DUPLICATE_WINDOW:
1171
+ recent.discard(order.popleft())
1172
+ return False
1129
1173
 
1130
1174
  def release(self) -> None:
1131
1175
  self._released = True
@@ -1134,6 +1178,24 @@ class _MediaHold:
1134
1178
  return self._released and self._held is not None
1135
1179
 
1136
1180
  def send(self, sock, pkt: bytes, port: int, *, pace=None, log=None) -> None:
1181
+ was_ended = self.clock.ended
1182
+ if self.clock.observe(pkt):
1183
+ # After its last frame the camera marks packets as the end of the
1184
+ # recording; there is nothing more of it to serve.
1185
+ if not was_ended and log is not None:
1186
+ try:
1187
+ log("bridge: the camera marked the end of the recording")
1188
+ except Exception:
1189
+ pass
1190
+ return
1191
+ if self._repeat(pkt, port):
1192
+ self.duplicates += 1
1193
+ if self.duplicates == 1 and log is not None:
1194
+ try:
1195
+ log("bridge: dropping RTP packets that arrive twice")
1196
+ except Exception:
1197
+ pass
1198
+ return
1137
1199
  if self._held is not None:
1138
1200
  if not self._released:
1139
1201
  if len(self._held) < self._limit:
@@ -1149,6 +1211,14 @@ class _MediaHold:
1149
1211
  first keyframe alone is ~130 packets and one burst of all of them can
1150
1212
  overrun the serve's socket. Returns how many were sent."""
1151
1213
  held, self._held = self._held or [], None
1214
+ if self.dropped:
1215
+ _LOGGER.warning(
1216
+ "SD playback: %d packet(s) over the %d-packet hold were"
1217
+ " dropped before the serve listened; the clip has a gap"
1218
+ " after its opening",
1219
+ self.dropped,
1220
+ self._limit,
1221
+ )
1152
1222
  sent = 0
1153
1223
  for i, (sock, pkt, port) in enumerate(held):
1154
1224
  try:
@@ -2046,6 +2116,11 @@ def turn_install_permissions(
2046
2116
  return _perm_ok
2047
2117
 
2048
2118
 
2119
+ #: The extmap lines the vendor app's offer filter keeps (OfferUtils.processRtp):
2120
+ #: the two a playback offer adds (sd_playback.offer_with_frame_times).
2121
+ _KEPT_EXTMAPS = (EXT_TRANSPORT_CC, EXT_FRAME_TIME)
2122
+
2123
+
2049
2124
  def compress_sdp_req(device_id, _sdp: str) -> str:
2050
2125
  """g.b() equivalent - selective SDP filter for wPayload."""
2051
2126
  _out: list = []
@@ -2074,6 +2149,14 @@ def compress_sdp_req(device_id, _sdp: str) -> str:
2074
2149
  if any(_d in _ln for _d in ("sendrecv", "recvonly", "sendonly")):
2075
2150
  _k(_ln)
2076
2151
  continue
2152
+ if _ln.startswith("a=extmap"):
2153
+ # OfferUtils.processRtp keeps these two, once each across the
2154
+ # SDP; firmware that reads wPayload sees no others. A live offer
2155
+ # names neither, so this changes nothing for it.
2156
+ for _uri in _KEPT_EXTMAPS:
2157
+ if _uri in _ln and _seen.get(_uri) is None:
2158
+ _k(_ln, _uri)
2159
+ continue
2077
2160
  for _ak in (
2078
2161
  "ice-ufrag",
2079
2162
  "ice-pwd",
@@ -6645,6 +6728,13 @@ async def _sdes_open_build_offer(self, ps: _SdesOpenState) -> None:
6645
6728
  )
6646
6729
  if ps._status:
6647
6730
  ps._status(f"SDES: offer pinned to video pt={_pin_video_pt}")
6731
+ if ps.sd_playback is not None:
6732
+ # The per-frame recording time the camera adds once the offer names
6733
+ # transport-cc and rtp-stream-id, as the vendor app's does: its RTP
6734
+ # clock runs at the live frame rate even for a slower recording.
6735
+ from .sd_playback import offer_with_frame_times
6736
+
6737
+ ps.sdes_offer_sdp = offer_with_frame_times(ps.sdes_offer_sdp)
6648
6738
 
6649
6739
  _relay_str = (
6650
6740
  f" relay-audio={ps._relay_addrs[ps._audio_sock][0]}:{ps._relay_addrs[ps._audio_sock][1]}"
@@ -9800,4 +9890,6 @@ class _SdesOpenMixin:
9800
9890
  # holding the started stream back for it.
9801
9891
  playback_waiter=_sd_start_waiter,
9802
9892
  playback=ps.sd_playback,
9893
+ playback_clock=_media_hold.clock if _media_hold is not None else None,
9894
+ playback_output=output_path if ps.sd_playback is not None else None,
9803
9895
  )
@@ -2458,6 +2458,18 @@ async def _dtls_open_sdes_branch(self, ps: _DtlsOpenState) -> _PhaseReturn | Non
2458
2458
  await asyncio.wait_for(ps.mqtt_fut, timeout=5.0)
2459
2459
  except Exception:
2460
2460
  pass # MQTT thread exit errors don't affect the retry
2461
+ # A playback keeps what it asked for across the fallback: its own
2462
+ # peer id rebuilt for DTLS (the live default is class 2 and
2463
+ # PRE_LINK, which an H265-recording camera sends audio alone),
2464
+ # and rtsp_push_url, so the DTLS open refuses what it cannot
2465
+ # serve instead of dropping it. A live open mints a fresh id.
2466
+ _fallback_peer_id = None
2467
+ _fallback_push_url = None
2468
+ if ps._sd_playback is not None:
2469
+ from .sd_playback import playback_peer_id
2470
+
2471
+ _fallback_peer_id = playback_peer_id(self, sdes=False)
2472
+ _fallback_push_url = ps.rtsp_push_url
2461
2473
  # Call the UNGATED impl, not the public async_open_webrtc_stream:
2462
2474
  # we are already holding the global open-gate permit (acquired in
2463
2475
  # async_open_webrtc_stream), and the gate's semaphore is not
@@ -2472,8 +2484,11 @@ async def _dtls_open_sdes_branch(self, ps: _DtlsOpenState) -> _PhaseReturn | Non
2472
2484
  stream_id=ps.stream_id,
2473
2485
  timeout=ps.timeout,
2474
2486
  output_path=ps.output_path,
2487
+ max_seconds=ps.max_seconds,
2475
2488
  status_callback=ps.status_callback,
2476
2489
  force_sdes=False,
2490
+ rtsp_push_url=_fallback_push_url,
2491
+ reuse_peer_id=_fallback_peer_id,
2477
2492
  _skip_ice_config=True,
2478
2493
  _ice_config=(
2479
2494
  ps.ice_config_fut.result() if ps.ice_config_fut.done() else None
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0rc63"
7
+ version = "1.0.0rc65"
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.0rc63
3
+ Version: 1.0.0rc65
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
@@ -259,6 +259,7 @@ tests/test_sd_event_decode.py
259
259
  tests/test_sd_event_probe_payloads.py
260
260
  tests/test_sd_listing.py
261
261
  tests/test_sd_playback.py
262
+ tests/test_sd_playback_clock.py
262
263
  tests/test_sd_playback_controls.py
263
264
  tests/test_sd_playback_dtls.py
264
265
  tests/test_sd_playback_entry.py