python-aidot-cameras 1.0.0rc63__tar.gz → 1.0.0rc64__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 (364) hide show
  1. {python_aidot_cameras-1.0.0rc63/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc64}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/client.py +6 -0
  3. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/sd_playback.py +35 -15
  4. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/sdes_open.py +54 -0
  5. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/webrtc_open.py +15 -0
  6. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/pyproject.toml +1 -1
  7. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  8. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_playback_dtls.py +39 -0
  9. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_playback_entry.py +45 -1
  10. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_playback_media_hold.py +66 -1
  11. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/LICENSE +0 -0
  12. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/MANIFEST.in +0 -0
  13. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/README.md +0 -0
  14. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/__init__.py +0 -0
  15. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/__main__.py +0 -0
  16. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_log_redaction.py +0 -0
  17. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_upstream.py +0 -0
  18. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  20. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  21. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  22. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  23. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  24. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  25. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  26. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  27. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  28. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  29. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  30. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  31. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  32. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  33. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  34. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  35. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  36. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  37. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  38. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  39. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  40. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  41. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  42. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  43. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  44. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  45. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  46. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  47. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  48. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  49. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  50. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  51. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  52. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/__init__.py +0 -0
  53. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/aac_track.py +0 -0
  54. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/agc.py +0 -0
  55. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/constants.py +0 -0
  56. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/controls.py +0 -0
  57. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/go2rtc.py +0 -0
  58. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/h264_sps.py +0 -0
  59. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/health.py +0 -0
  60. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/hls_ts.py +0 -0
  61. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/hwaccel.py +0 -0
  62. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/lan_control.py +0 -0
  63. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/models.py +0 -0
  64. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/protocol.py +0 -0
  65. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/recording.py +0 -0
  66. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/rtp_h264.py +0 -0
  67. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/rtsp_publish.py +0 -0
  68. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/sd_events.py +0 -0
  69. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/sd_listing.py +0 -0
  70. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/sdes.py +0 -0
  71. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/ts_fanout.py +0 -0
  72. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/ts_tee.py +0 -0
  73. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/camera/webrtc.py +0 -0
  74. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/client.py +0 -0
  75. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/cloud_auth.py +0 -0
  76. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/const.py +0 -0
  77. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/credentials.py +0 -0
  78. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/crypto.py +0 -0
  79. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/device_client.py +0 -0
  80. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/discover.py +0 -0
  81. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/exceptions.py +0 -0
  82. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/g711.py +0 -0
  83. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/lan_retry.py +0 -0
  84. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/py.typed +0 -0
  85. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/aidot_cameras/settings.py +0 -0
  86. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  87. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  88. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  89. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/python_aidot_cameras.egg-info/requires.txt +0 -0
  90. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  91. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/setup.cfg +0 -0
  92. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_aac_track.py +0 -0
  93. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
  94. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
  95. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_aioice_compat.py +0 -0
  96. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_alarm_event.py +0 -0
  97. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_answer_pt_kinds.py +0 -0
  98. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_answer_sdp_video_pt.py +0 -0
  99. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_answer_section_selection.py +0 -0
  100. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_arrival_timestamps.py +0 -0
  101. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_audio_mux_offline.py +0 -0
  102. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_auth_error_routing.py +0 -0
  103. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_auto_alarm.py +0 -0
  104. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_auto_light_settings.py +0 -0
  105. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  106. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_avio_header_single_layout.py +0 -0
  107. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_avio_request.py +0 -0
  108. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_avio_response_parse.py +0 -0
  109. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_avio_response_router.py +0 -0
  110. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_backoff.py +0 -0
  111. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_battery_cap_spans_sessions.py +0 -0
  112. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_battery_detection.py +0 -0
  113. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_battery_evidence_corroboration.py +0 -0
  114. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  115. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_battery_wake_settles_before_the_command.py +0 -0
  116. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ble_gateway_is_gone.py +0 -0
  117. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_bridge_state_sentinels.py +0 -0
  118. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_busy_backoff.py +0 -0
  119. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_camera_properties.py +0 -0
  120. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_canary_log_names_the_camera.py +0 -0
  121. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
  122. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_cloud_count.py +0 -0
  123. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_cloud_error_log_names_the_exception.py +0 -0
  124. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_cloud_error_severity.py +0 -0
  125. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_cloud_plan.py +0 -0
  126. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_constants_imports_resolve.py +0 -0
  127. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_control_verdicts.py +0 -0
  128. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_declared_properties.py +0 -0
  129. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_deferred_hardening.py +0 -0
  130. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_detection_types.py +0 -0
  131. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_device_cmd_seq_match.py +0 -0
  132. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_device_login_guard.py +0 -0
  133. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_device_user_info_cache.py +0 -0
  134. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
  135. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_direct_ts_keyframe_signal.py +0 -0
  136. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_direct_ts_serve.py +0 -0
  137. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_direct_ts_serve_only_for_http.py +0 -0
  138. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dormant_camera_cleanup.py +0 -0
  139. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_drain_is_cancellable.py +0 -0
  140. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_mux_terminal_write.py +0 -0
  141. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_not_ready_burst.py +0 -0
  142. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
  143. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_pinning.py +0 -0
  144. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_recorder_wiring.py +0 -0
  145. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_refusal_after_the_echo.py +0 -0
  146. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_resolution_ack.py +0 -0
  147. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_serve_decode_skip.py +0 -0
  148. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_serve_ice_wait.py +0 -0
  149. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_serve_open_timeout.py +0 -0
  150. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_skip_signaling_wait.py +0 -0
  151. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_slow_probe.py +0 -0
  152. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_stdout_producer.py +0 -0
  153. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_tap_states_no_reorder.py +0 -0
  154. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_ts_copy_recording.py +0 -0
  155. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_dtls_video_presence.py +0 -0
  156. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_egress_guard.py +0 -0
  157. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_env_seams.py +0 -0
  158. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_every_exception_is_exported.py +0 -0
  159. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  160. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
  161. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  162. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_first_media_abandon_rules.py +0 -0
  163. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_futile_keepalive.py +0 -0
  164. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  165. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_go2rtc.py +0 -0
  166. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_go2rtc_cli.py +0 -0
  167. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_h264_decode_rate_limit.py +0 -0
  168. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_h264_sps.py +0 -0
  169. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_health_line.py +0 -0
  170. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_health_line_counts_pictures.py +0 -0
  171. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_highport_nomination.py +0 -0
  172. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_hls_ts_end_to_end.py +0 -0
  173. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_hls_ts_wiring.py +0 -0
  174. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_hwaccel_probe.py +0 -0
  175. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ice_config_cache.py +0 -0
  176. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ice_credentials_match_the_app.py +0 -0
  177. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ice_peer_reflexive.py +0 -0
  178. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_idle_release_both_loops.py +0 -0
  179. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ipc_envelope_single_builder.py +0 -0
  180. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_keepalive_renew.py +0 -0
  181. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_key_restart_sdp.py +0 -0
  182. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_keyframe_prompter.py +0 -0
  183. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_control.py +0 -0
  184. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_login_accessors.py +0 -0
  185. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  186. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_retry_short_sessions.py +0 -0
  187. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
  188. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_retry_stops_on_close.py +0 -0
  189. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_lan_sweep_only_with_lights.py +0 -0
  190. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_late_repeats_are_dropped.py +0 -0
  191. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_light_active_color_mode.py +0 -0
  192. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_live_session_carries_talk.py +0 -0
  193. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_live_stream_param.py +0 -0
  194. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_live_validate_policy.py +0 -0
  195. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_live_validate_token_refresh.py +0 -0
  196. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_live_video_canary.py +0 -0
  197. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_liveplay_echo_timeout.py +0 -0
  198. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_local_only_controls.py +0 -0
  199. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_log_hygiene.py +0 -0
  200. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_logging_caps.py +0 -0
  201. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_login_connect_deadline.py +0 -0
  202. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_login_reconnect_backoff.py +0 -0
  203. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_loop_break_ends_the_loop.py +0 -0
  204. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_media_counted_only_when_plaintext.py +0 -0
  205. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_media_path_is_reported.py +0 -0
  206. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_media_socket_rcvbuf.py +0 -0
  207. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_media_wait_floor.py +0 -0
  208. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_motion_poll.py +0 -0
  209. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mqtt_client_id_is_exact.py +0 -0
  210. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mqtt_credential_refetch.py +0 -0
  211. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mqtt_credential_self_heal.py +0 -0
  212. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mqtt_password_not_persisted.py +0 -0
  213. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mqtt_session_reconnect.py +0 -0
  214. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  215. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_mypy_new_findings.py +0 -0
  216. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_nack.py +0 -0
  217. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  218. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_narrow_pc_ice.py +0 -0
  219. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_no_key_material_in_logs.py +0 -0
  220. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
  221. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_no_self_referential_go2rtc.py +0 -0
  222. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_no_unbounded_hex_dumps.py +0 -0
  223. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_no_undefined_names.py +0 -0
  224. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
  225. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_off_subnet_candidate.py +0 -0
  226. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  227. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_offline_keepalive_pause.py +0 -0
  228. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_on_frame_is_an_av_frame.py +0 -0
  229. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_open_errors_are_aidot_errors.py +0 -0
  230. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_open_fail_logger.py +0 -0
  231. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_open_gate_delay.py +0 -0
  232. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_peer_id_client_type.py +0 -0
  233. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_peerid_reuse.py +0 -0
  234. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_persistent_mqtt.py +0 -0
  235. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_persistent_mqtt_robustness.py +0 -0
  236. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_phase_returns_reach_the_caller.py +0 -0
  237. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_phase_state_is_complete.py +0 -0
  238. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_plaintext_mqtt_refused.py +0 -0
  239. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_post_merge_hardening.py +0 -0
  240. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  241. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_public_signature_matches_impl.py +0 -0
  242. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_publish_target_readiness.py +0 -0
  243. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_quality_campaign_in_session.py +0 -0
  244. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_reboot_action.py +0 -0
  245. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_recent_recordings.py +0 -0
  246. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_region_allowlist.py +0 -0
  247. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_remb.py +0 -0
  248. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  249. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
  250. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_resent_frames_are_dropped.py +0 -0
  251. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_resolution_persists.py +0 -0
  252. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_restart_cycle.py +0 -0
  253. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_retry_policy.py +0 -0
  254. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_review_lan_retry_and_graces.py +0 -0
  255. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_rtp_h264.py +0 -0
  256. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_rtsp_publish.py +0 -0
  257. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_rtsp_publish_go2rtc.py +0 -0
  258. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_scripts_importable.py +0 -0
  259. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sctp_init_ack_tsn.py +0 -0
  260. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sctp_reassembly.py +0 -0
  261. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sctp_sack.py +0 -0
  262. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_card_presence.py +0 -0
  263. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_event_decode.py +0 -0
  264. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_event_probe_payloads.py +0 -0
  265. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_listing.py +0 -0
  266. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_playback.py +0 -0
  267. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_playback_controls.py +0 -0
  268. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sd_playback_open.py +0 -0
  269. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_avio_dispatch.py +0 -0
  270. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_bridge_break_gate.py +0 -0
  271. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_connection_mode.py +0 -0
  272. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
  273. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_fast_liveplay.py +0 -0
  274. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  275. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_first_media_stall_report.py +0 -0
  276. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_ice_teardown.py +0 -0
  277. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_idle_release.py +0 -0
  278. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_nack_bridge.py +0 -0
  279. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_offer_video_pin.py +0 -0
  280. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_open_cleanup.py +0 -0
  281. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_open_yields_the_loop.py +0 -0
  282. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_pinned_codec_retry.py +0 -0
  283. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_sctp_abort.py +0 -0
  284. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_serve_audio.py +0 -0
  285. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_serve_cmd.py +0 -0
  286. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_serve_stderr_drain.py +0 -0
  287. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_sprop.py +0 -0
  288. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_talk.py +0 -0
  289. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_teardown_exit_log.py +0 -0
  290. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_terminal_ack.py +0 -0
  291. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_terminal_ack_latency.py +0 -0
  292. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdes_watchdog.py +0 -0
  293. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdp_narrowing_properties.py +0 -0
  294. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sdp_payload_type_narrowing.py +0 -0
  295. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_security_hardening.py +0 -0
  296. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  297. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serializable_login_info.py +0 -0
  298. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
  299. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_input_timeout.py +0 -0
  300. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_loop_releases_on_error.py +0 -0
  301. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_loss_tolerance.py +0 -0
  302. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_pt_respects_the_pin.py +0 -0
  303. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_relay.py +0 -0
  304. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  305. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_session_mode_seq.py +0 -0
  306. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_session_stats.py +0 -0
  307. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_session_stop_twice.py +0 -0
  308. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_settings_registry.py +0 -0
  309. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_shared_house_seam.py +0 -0
  310. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  311. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_skip_guard.py +0 -0
  312. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_snapshot_cancellation.py +0 -0
  313. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_speak.py +0 -0
  314. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sprop_capture_gate.py +0 -0
  315. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sprop_staleness.py +0 -0
  316. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sprop_unstable_public.py +0 -0
  317. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  318. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_srtp_rx_rekey.py +0 -0
  319. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_srtp_tx_key_note.py +0 -0
  320. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stable_terminal_id.py +0 -0
  321. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stall_nudge_policy.py +0 -0
  322. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  323. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  324. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  325. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  326. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  327. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_status_and_trace_channels.py +0 -0
  328. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stream_cap.py +0 -0
  329. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stream_drain_ownership.py +0 -0
  330. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stream_idle.py +0 -0
  331. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stream_slot_cap.py +0 -0
  332. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stream_teardown.py +0 -0
  333. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_stun_attr_single_builder.py +0 -0
  334. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_swallowed_exception_names.py +0 -0
  335. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_talk.py +0 -0
  336. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  337. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_tap_to_mux_end_to_end.py +0 -0
  338. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_terminal_ack.py +0 -0
  339. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_tmmbr.py +0 -0
  340. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_token_file_robustness.py +0 -0
  341. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_token_refresh.py +0 -0
  342. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_transport_failure_reporting.py +0 -0
  343. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ts_fanout.py +0 -0
  344. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_ts_tee.py +0 -0
  345. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_turn_create_permission.py +0 -0
  346. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_turn_target_shared.py +0 -0
  347. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
  348. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
  349. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_upstream_compat.py +0 -0
  350. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_upstream_seam_adapter.py +0 -0
  351. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_upstream_secrets_redacted.py +0 -0
  352. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_validator_shares_one_token.py +0 -0
  353. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_vendored_aiortc_integrity.py +0 -0
  354. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_video_presence_needs_a_keyframe.py +0 -0
  355. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_video_profile_log.py +0 -0
  356. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_video_pts_dts.py +0 -0
  357. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_video_ts_unwrap.py +0 -0
  358. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_viewer_check_throttle.py +0 -0
  359. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_viewer_presence.py +0 -0
  360. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_viewer_present_push_mode.py +0 -0
  361. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_wait_or_event.py +0 -0
  362. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_wake_gate_camera_evidence.py +0 -0
  363. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/tests/test_wake_readiness_retry.py +0 -0
  364. {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc64}/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.0rc64
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
@@ -2792,6 +2792,12 @@ class CameraMixin(
2792
2792
  Taken from the SDES bridge thread (SESSION_MODE, control commands) as
2793
2793
  well as the event loop (livePlayReq), so read-and-increment is one
2794
2794
  locked step: a repeated seq is a SESSION_MODE the camera ignores.
2795
+
2796
+ Starting again at 100 in every new client is safe: the camera keeps
2797
+ the last accepted seq per session, not per camera. Back-to-back
2798
+ playbacks from fresh clients, each counting from 100 where the one
2799
+ before had already gone past it, had SD mode echoed every time on all
2800
+ three models (2026-10-27), as the app's own restarts rely on.
2795
2801
  """
2796
2802
  with _DSEQ_LOCK:
2797
2803
  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,30 @@ 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
+
299
308
  # --- DTLS (A000088): the recording's H265 is taken raw ----------------------
300
309
  #
301
310
  # aiortc has no H265, and the camera sends a recording it holds as H265 on a
@@ -459,13 +468,21 @@ class H265AccessUnits:
459
468
  self, seq: int, ts: int, marker: bool, payload: bytes
460
469
  ) -> "list[tuple[bytes, int, bool]]":
461
470
  out = []
462
- gap = self._last_seq is not None and (seq - self._last_seq) & 0xFFFF != 1
471
+ missing = 0
472
+ if self._last_seq is not None:
473
+ missing = (seq - self._last_seq - 1) & 0xFFFF
474
+ gap = missing != 0
463
475
  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:
476
+ # A unit in hand that never saw its marker lost at least its last
477
+ # packet; one already emitted at its marker lost nothing. Whatever
478
+ # else went missing is the head of this new unit (its VPS/SPS, or
479
+ # the first slice of a keyframe), so it is not handed on either.
480
+ pending = bool(self._nals) or self._fu is not None
481
+ if gap and pending:
467
482
  self._broken = True
468
483
  out += self._emit()
484
+ if missing > (1 if pending else 0):
485
+ self._broken = True
469
486
  elif gap:
470
487
  self._broken = True
471
488
  self._fu = None
@@ -478,6 +495,9 @@ class H265AccessUnits:
478
495
  i = 2
479
496
  while i + 2 <= len(payload):
480
497
  n = struct.unpack_from("!H", payload, i)[0]
498
+ if n == 0 or i + 2 + n > len(payload):
499
+ self._broken = True # truncated or corrupt
500
+ break
481
501
  self._nals.append(bytes(payload[i + 2 : i + 2 + n]))
482
502
  i += 2 + n
483
503
  elif t == 49 and len(payload) >= 3: # fragmentation unit
@@ -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
@@ -1109,9 +1110,18 @@ def _session_mode_echoed(frame: bytes) -> "Optional[int]":
1109
1110
 
1110
1111
  #: How many packets a playback open holds for its serve (see _new_media_hold).
1111
1112
  #: The serve is up within a second; ~70 packets/s on an A001064 recording.
1113
+ #: The hold is released once the serve binds or 4.5 s after it is launched
1114
+ #: (the bind wait and its fallback), so the cap is several times what one
1115
+ #: open can hold; a drop past it is logged as a warning, since the served
1116
+ #: clip then has a hole after its opening.
1112
1117
  _MEDIA_HOLD_MAX = 4000
1113
1118
 
1114
1119
 
1120
+ #: How many recent sequence numbers per stream a playback remembers, to drop
1121
+ #: a packet that arrives twice. A few seconds of video at 2560x1440.
1122
+ _DUPLICATE_WINDOW = 2048
1123
+
1124
+
1115
1125
  class _MediaHold:
1116
1126
  """Packets for the serve, held in order until it is listening.
1117
1127
 
@@ -1119,6 +1129,14 @@ class _MediaHold:
1119
1129
  loopback port; ``release`` by the open once the serve is bound. The first
1120
1130
  ``send`` (or ``flush``) after the release hands over everything held -
1121
1131
  still on the bridge thread, so nothing can overtake it.
1132
+
1133
+ It also drops an RTP packet already forwarded (same port, SSRC and
1134
+ sequence number). On a host that reaches the camera by two routes every
1135
+ packet can arrive twice: measured on the reference box 2026-10-28, each
1136
+ playback audio packet came twice and video packets up to six times. ffmpeg
1137
+ discards a repeated video packet, but the G.711 is copied as data, so
1138
+ every repeat was written too and the clip's sound ran twice as long as its
1139
+ picture.
1122
1140
  """
1123
1141
 
1124
1142
  def __init__(self, limit: int = 0) -> None:
@@ -1126,6 +1144,26 @@ class _MediaHold:
1126
1144
  self._released = False
1127
1145
  self._limit = limit or _MEDIA_HOLD_MAX
1128
1146
  self.dropped = 0
1147
+ self.duplicates = 0
1148
+ self._seen: "dict" = {}
1149
+
1150
+ def _repeat(self, pkt: bytes, port: int) -> bool:
1151
+ """Is ``pkt`` an RTP packet this hold has already passed on?"""
1152
+ if len(pkt) < 12 or pkt[0] >> 6 != 2 or 192 <= pkt[1] <= 223:
1153
+ return False # not RTP, or RTCP (payload types 192-223)
1154
+ ssrc = int.from_bytes(pkt[8:12], "big")
1155
+ seq = int.from_bytes(pkt[2:4], "big")
1156
+ seen = self._seen.get((port, ssrc))
1157
+ if seen is None:
1158
+ seen = self._seen[(port, ssrc)] = (set(), collections.deque())
1159
+ recent, order = seen
1160
+ if seq in recent:
1161
+ return True
1162
+ recent.add(seq)
1163
+ order.append(seq)
1164
+ if len(order) > _DUPLICATE_WINDOW:
1165
+ recent.discard(order.popleft())
1166
+ return False
1129
1167
 
1130
1168
  def release(self) -> None:
1131
1169
  self._released = True
@@ -1134,6 +1172,14 @@ class _MediaHold:
1134
1172
  return self._released and self._held is not None
1135
1173
 
1136
1174
  def send(self, sock, pkt: bytes, port: int, *, pace=None, log=None) -> None:
1175
+ if self._repeat(pkt, port):
1176
+ self.duplicates += 1
1177
+ if self.duplicates == 1 and log is not None:
1178
+ try:
1179
+ log("bridge: dropping RTP packets that arrive twice")
1180
+ except Exception:
1181
+ pass
1182
+ return
1137
1183
  if self._held is not None:
1138
1184
  if not self._released:
1139
1185
  if len(self._held) < self._limit:
@@ -1149,6 +1195,14 @@ class _MediaHold:
1149
1195
  first keyframe alone is ~130 packets and one burst of all of them can
1150
1196
  overrun the serve's socket. Returns how many were sent."""
1151
1197
  held, self._held = self._held or [], None
1198
+ if self.dropped:
1199
+ _LOGGER.warning(
1200
+ "SD playback: %d packet(s) over the %d-packet hold were"
1201
+ " dropped before the serve listened; the clip has a gap"
1202
+ " after its opening",
1203
+ self.dropped,
1204
+ self._limit,
1205
+ )
1152
1206
  sent = 0
1153
1207
  for i, (sock, pkt, port) in enumerate(held):
1154
1208
  try:
@@ -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.0rc64"
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.0rc64
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
@@ -415,3 +415,42 @@ def test_the_sd_mode_echo_has_one_parser():
415
415
  == _session_mode_echoed(avio_header(1, 5377, 0, 12, 0) + body)
416
416
  )
417
417
  assert session_mode_of(b"\x00" * 8) is None
418
+
419
+
420
+ def test_a_lost_head_drops_the_new_unit_not_the_one_already_emitted():
421
+ aus = _hevc_access_units(5)
422
+ units = H265AccessUnits()
423
+ got, seq = [], 300
424
+ for i, au in enumerate(aus):
425
+ pkts = _packetize(au, i * 6000, seq, mtu=200)
426
+ seq += len(pkts)
427
+ for j, p in enumerate(pkts):
428
+ if i == 1 and j == 0:
429
+ continue # unit 1 loses its first packet, after 0's marker
430
+ got += units.feed(*p)
431
+ assert units.dropped == 1
432
+ assert [ts for _, ts, _ in got] == [0, 12000, 18000, 24000]
433
+
434
+
435
+ def test_a_gap_wider_than_the_lost_marker_drops_both_units():
436
+ aus = _hevc_access_units(5)
437
+ units = H265AccessUnits()
438
+ got, seq = [], 900
439
+ for i, au in enumerate(aus):
440
+ pkts = _packetize(au, i * 6000, seq, mtu=200)
441
+ seq += len(pkts)
442
+ for j, p in enumerate(pkts):
443
+ if (i == 0 and j == len(pkts) - 1) or (i == 1 and j == 0):
444
+ continue # 0's marker and 1's head, back to back
445
+ got += units.feed(*p)
446
+ assert units.dropped == 2
447
+ assert [ts for _, ts, _ in got] == [12000, 18000, 24000]
448
+
449
+
450
+ def test_a_truncated_aggregation_packet_breaks_its_unit():
451
+ units = H265AccessUnits()
452
+ vps = bytes([32 << 1, 1, 0x0C])
453
+ ap = bytes([48 << 1, 1]) + struct.pack("!H", 3) + vps + struct.pack("!H", 1200)
454
+ ap += bytes([19 << 1, 1]) + b"\x00" * 298
455
+ assert units.feed(1, 0, True, ap) == []
456
+ assert units.dropped == 1
@@ -31,7 +31,7 @@ class _Task:
31
31
 
32
32
 
33
33
  class _Camera(_CameraSdPlaybackMixin):
34
- device_id = "12b144cb12da"
34
+ device_id = "0123456789ab"
35
35
  generate_webrtc_peer_id = staticmethod(CameraMixin.generate_webrtc_peer_id)
36
36
 
37
37
  def __init__(self, sdes=True, live=False, streaming=False):
@@ -110,3 +110,47 @@ def test_every_camera_client_has_it():
110
110
  assert CameraMixin.async_open_sd_playback is (
111
111
  _CameraSdPlaybackMixin.async_open_sd_playback
112
112
  )
113
+
114
+
115
+ def test_the_dtls_fallback_keeps_the_playback_peer_id_and_its_options():
116
+ # An SDES-flagged camera that answers only DTLS: the retry must still
117
+ # ask for the recording (class 0, SD card), not open a live view.
118
+ from aidot_cameras.camera.webrtc_open import _dtls_open_sdes_branch
119
+
120
+ class _Fut:
121
+ def done(self):
122
+ return False
123
+
124
+ class _Ps:
125
+ use_sdes = True
126
+ terminal_error_fut = _Fut()
127
+ ice_config_fut = _Fut()
128
+ mqtt_fut = None
129
+ output_path = "clip.part.ts"
130
+ max_seconds = 70.0
131
+ rtsp_push_url = "rtsp://127.0.0.1:1/x"
132
+ _sd_playback = SdPlaybackStart(request=b"", record=REC)
133
+
134
+ def _status(self, _msg):
135
+ pass
136
+
137
+ def __getattr__(self, _name):
138
+ return None
139
+
140
+ class _Cam(_Camera):
141
+ async def _open_sdes_stream(self, **_kw):
142
+ raise CameraMixin._SdesNoAnswerError()
143
+
144
+ async def _async_open_webrtc_stream_impl(self, on_frame, **kw):
145
+ self.opens.append((on_frame, kw))
146
+ return "dtls"
147
+
148
+ cam = _Cam()
149
+ got = asyncio.run(_dtls_open_sdes_branch(cam, _Ps()))
150
+ assert got.value == "dtls"
151
+ ((_, kw),) = cam.opens
152
+ assert kw["force_sdes"] is False
153
+ assert PEER.match(kw["reuse_peer_id"]).groups()[:3] == ("0", "1", "0")
154
+ assert kw["max_seconds"] == 70.0
155
+ # Carried so the DTLS open refuses it rather than dropping it.
156
+ assert kw["rtsp_push_url"] == "rtsp://127.0.0.1:1/x"
@@ -68,7 +68,7 @@ def test_the_flush_is_paced_and_reported():
68
68
  assert lines == ["bridge: released 40 held packet(s) to the serve"]
69
69
 
70
70
 
71
- def test_the_hold_is_bounded():
71
+ def test_the_hold_is_bounded(caplog):
72
72
  sent = []
73
73
  hold = _MediaHold(limit=3)
74
74
  for i in range(5):
@@ -77,6 +77,11 @@ def test_the_hold_is_bounded():
77
77
  hold.release()
78
78
  assert hold.flush() == 3
79
79
  assert _MEDIA_HOLD_MAX >= 1000
80
+ # The hole it leaves is not a debug line only.
81
+ assert any(
82
+ r.levelname == "WARNING" and "2 packet(s)" in r.getMessage()
83
+ for r in caplog.records
84
+ )
80
85
 
81
86
 
82
87
  def test_every_loopback_send_in_the_bridge_goes_through_the_hold():
@@ -138,3 +143,63 @@ def calls_to_serve_cmd():
138
143
  return calls_to(
139
144
  module_tree("aidot_cameras.camera.sdes_open"), "_build_sdes_serve_cmd"
140
145
  )
146
+
147
+
148
+ def _rtp(seq, ssrc=0x1257, pt=8):
149
+ return (
150
+ bytes([0x80, pt])
151
+ + seq.to_bytes(2, "big")
152
+ + bytes(4)
153
+ + ssrc.to_bytes(4, "big")
154
+ + b"x"
155
+ )
156
+
157
+
158
+ def test_a_packet_that_arrives_twice_is_forwarded_once():
159
+ # The reference box, 2026-10-28: every audio packet came twice (two
160
+ # routes to the camera), and the copied G.711 ran twice as long as the
161
+ # picture.
162
+ sent, lines = [], []
163
+ hold = _MediaHold()
164
+ hold.release()
165
+ for seq in (1, 1, 2, 3, 2, 3, 4):
166
+ hold.send(_Sock(sent), _rtp(seq), 5000, log=lines.append)
167
+ assert [int.from_bytes(p[2:4], "big") for p, _ in sent] == [1, 2, 3, 4]
168
+ assert hold.duplicates == 3
169
+ assert lines.count("bridge: dropping RTP packets that arrive twice") == 1
170
+
171
+
172
+ def test_only_the_same_stream_and_port_count_as_a_repeat():
173
+ sent = []
174
+ hold = _MediaHold()
175
+ hold.release()
176
+ hold.send(_Sock(sent), _rtp(7), 5000)
177
+ hold.send(_Sock(sent), _rtp(7, ssrc=0x1259, pt=97), 5000) # other SSRC
178
+ hold.send(_Sock(sent), _rtp(7), 5002) # other port
179
+ rtcp = bytes([0x80, 200]) + bytes(10)
180
+ hold.send(_Sock(sent), rtcp, 5000)
181
+ hold.send(_Sock(sent), rtcp, 5000) # RTCP is never deduplicated
182
+ assert len(sent) == 5 and hold.duplicates == 0
183
+
184
+
185
+ def test_a_repeat_while_held_is_dropped_too():
186
+ sent = []
187
+ hold = _MediaHold()
188
+ for seq in (10, 10, 11):
189
+ hold.send(_Sock(sent), _rtp(seq), 5000)
190
+ hold.release()
191
+ hold.flush()
192
+ assert len(sent) == 2 and hold.duplicates == 1
193
+
194
+
195
+ def test_the_window_forgets_old_numbers_so_a_wrapped_sequence_plays():
196
+ from aidot_cameras.camera.sdes_open import _DUPLICATE_WINDOW
197
+
198
+ sent = []
199
+ hold = _MediaHold()
200
+ hold.release()
201
+ n = 65536 + 10 # past a full wrap of the 16-bit sequence number
202
+ for i in range(n):
203
+ hold.send(_Sock(sent), _rtp(i & 0xFFFF), 5000)
204
+ assert len(sent) == n and hold.duplicates == 0
205
+ assert _DUPLICATE_WINDOW < 65536 // 4