python-aidot-cameras 0.15.2__tar.gz → 0.15.4__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 (174) hide show
  1. {python_aidot_cameras-0.15.2/python_aidot_cameras.egg-info → python_aidot_cameras-0.15.4}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/sdes.py +26 -2
  3. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/sdes_open.py +57 -5
  4. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/pyproject.toml +1 -1
  5. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  7. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_ffmpeg_stderr_log.py +16 -2
  8. python_aidot_cameras-0.15.4/tests/test_sdes_serve_stderr_drain.py +190 -0
  9. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_teardown_exit_log.py +21 -0
  10. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/LICENSE +0 -0
  11. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/MANIFEST.in +0 -0
  12. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/README.md +0 -0
  13. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/__init__.py +0 -0
  14. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/__main__.py +0 -0
  15. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_upstream.py +0 -0
  16. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/__init__.py +0 -0
  17. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  18. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  19. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  20. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  21. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  22. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  23. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  24. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  25. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  26. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  27. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  28. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  29. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  30. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  31. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  32. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  33. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  34. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  35. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  36. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  37. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  38. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  39. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  40. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  41. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  42. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  43. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  44. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  45. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  46. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  47. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  48. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  49. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/ble_gateway.py +0 -0
  50. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/__init__.py +0 -0
  51. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/client.py +0 -0
  52. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/constants.py +0 -0
  53. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/controls.py +0 -0
  54. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/go2rtc.py +0 -0
  55. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/hwaccel.py +0 -0
  56. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/lan_control.py +0 -0
  57. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/models.py +0 -0
  58. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/playback.py +0 -0
  59. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/protocol.py +0 -0
  60. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/tutk.py +0 -0
  61. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/webrtc.py +0 -0
  62. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/camera/webrtc_open.py +0 -0
  63. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/client.py +0 -0
  64. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/const.py +0 -0
  65. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/credentials.py +0 -0
  66. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/crypto.py +0 -0
  67. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/device_client.py +0 -0
  68. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/discover.py +0 -0
  69. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/exceptions.py +0 -0
  70. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/g711.py +0 -0
  71. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/aidot_cameras/py.typed +0 -0
  72. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  73. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  74. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/python_aidot_cameras.egg-info/requires.txt +0 -0
  75. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  76. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/setup.cfg +0 -0
  77. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_aioice_compat.py +0 -0
  78. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_alarm_event.py +0 -0
  79. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_answer_sdp_video_pt.py +0 -0
  80. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_audio_mux_offline.py +0 -0
  81. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_backoff.py +0 -0
  82. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_battery_detection.py +0 -0
  83. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_battery_evidence_corroboration.py +0 -0
  84. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_battery_skip_turn.py +0 -0
  85. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_ble_gateway.py +0 -0
  86. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_constants_imports_resolve.py +0 -0
  87. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_deferred_hardening.py +0 -0
  88. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_device_login_guard.py +0 -0
  89. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_device_user_info_cache.py +0 -0
  90. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dormant_camera_cleanup.py +0 -0
  91. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_mux_terminal_write.py +0 -0
  92. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_not_ready_burst.py +0 -0
  93. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_pinning.py +0 -0
  94. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_serve_decode_skip.py +0 -0
  95. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_serve_open_timeout.py +0 -0
  96. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_skip_signaling_wait.py +0 -0
  97. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_dtls_slow_probe.py +0 -0
  98. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_egress_guard.py +0 -0
  99. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_env_seams.py +0 -0
  100. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_go2rtc.py +0 -0
  101. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_go2rtc_cli.py +0 -0
  102. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_h264_decode_rate_limit.py +0 -0
  103. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_highport_nomination.py +0 -0
  104. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_hwaccel_probe.py +0 -0
  105. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_ice_config_cache.py +0 -0
  106. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_ice_peer_reflexive.py +0 -0
  107. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_idle_release_both_loops.py +0 -0
  108. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_keepalive_renew.py +0 -0
  109. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_key_restart_sdp.py +0 -0
  110. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_keyframe_prompter.py +0 -0
  111. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_lan_control.py +0 -0
  112. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_light_active_color_mode.py +0 -0
  113. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_live_stream_param.py +0 -0
  114. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_live_validate_policy.py +0 -0
  115. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_logging_caps.py +0 -0
  116. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_media_wait_floor.py +0 -0
  117. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_motion_poll.py +0 -0
  118. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_mqtt_credential_refetch.py +0 -0
  119. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_mqtt_credential_self_heal.py +0 -0
  120. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_mqtt_password_not_persisted.py +0 -0
  121. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_mqtt_session_reconnect.py +0 -0
  122. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_narrow_pc_ice.py +0 -0
  123. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_no_self_referential_go2rtc.py +0 -0
  124. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_no_undefined_names.py +0 -0
  125. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_offline_keepalive_pause.py +0 -0
  126. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_open_fail_logger.py +0 -0
  127. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_open_gate_delay.py +0 -0
  128. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_peerid_reuse.py +0 -0
  129. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_persistent_mqtt.py +0 -0
  130. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_persistent_mqtt_robustness.py +0 -0
  131. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_playback_tls.py +0 -0
  132. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_post_merge_hardening.py +0 -0
  133. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_resolution_persists.py +0 -0
  134. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_restart_cycle.py +0 -0
  135. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_retry_policy.py +0 -0
  136. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_scripts_importable.py +0 -0
  137. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_adaptive.py +0 -0
  138. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_bridge_break_gate.py +0 -0
  139. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_echo_wait_timeout.py +0 -0
  140. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_fast_liveplay.py +0 -0
  141. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_idle_release.py +0 -0
  142. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_open_cleanup.py +0 -0
  143. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_serve_audio.py +0 -0
  144. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_serve_cmd.py +0 -0
  145. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_sprop.py +0 -0
  146. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_talk.py +0 -0
  147. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_terminal_ack.py +0 -0
  148. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_terminal_ack_latency.py +0 -0
  149. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdes_watchdog.py +0 -0
  150. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdp_narrowing_properties.py +0 -0
  151. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sdp_payload_type_narrowing.py +0 -0
  152. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_security_hardening.py +0 -0
  153. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_serializable_login_info.py +0 -0
  154. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_serve_relay.py +0 -0
  155. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_session_stats.py +0 -0
  156. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_shared_house_seam.py +0 -0
  157. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_speak.py +0 -0
  158. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_sprop_staleness.py +0 -0
  159. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_stream_cap.py +0 -0
  160. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_stream_drain_ownership.py +0 -0
  161. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_stream_idle.py +0 -0
  162. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_stream_slot_cap.py +0 -0
  163. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_stream_teardown.py +0 -0
  164. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_talk.py +0 -0
  165. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_terminal_ack.py +0 -0
  166. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_token_file_robustness.py +0 -0
  167. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_token_refresh.py +0 -0
  168. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_turn_create_permission.py +0 -0
  169. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_upstream_compat.py +0 -0
  170. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_viewer_check_throttle.py +0 -0
  171. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_viewer_presence.py +0 -0
  172. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_viewer_present_push_mode.py +0 -0
  173. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_wait_or_event.py +0 -0
  174. {python_aidot_cameras-0.15.2 → python_aidot_cameras-0.15.4}/tests/test_wake_readiness_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.15.2
3
+ Version: 0.15.4
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
@@ -38,7 +38,12 @@ class SdesSession:
38
38
  teardown_requested=None,
39
39
  first_video_pt=None,
40
40
  first_audio_pt=None,
41
+ device_id=None,
41
42
  ) -> None:
43
+ # Which camera this session belongs to, for logging only. Optional so
44
+ # an existing caller that does not pass it still works; the logs then
45
+ # read "?" exactly as they did before.
46
+ self._device_id = device_id or "?"
42
47
  self._proc = proc
43
48
  self._sdp_path = sdp_path
44
49
  self._outgoing_q = outgoing_q
@@ -177,6 +182,24 @@ class SdesSession:
177
182
  "audio_pt": self._first_audio_pt[0],
178
183
  }
179
184
 
185
+ def _drained_stderr_tail(self) -> bytes:
186
+ """The stderr the drain thread already consumed, for the teardown log.
187
+
188
+ The serve's stderr is drained continuously on a daemon thread (see
189
+ ``_start_serve_stderr_drain`` in sdes_open) so an un-drained pipe cannot
190
+ fill and stall ffmpeg. A consequence is that by teardown the pipe is at
191
+ EOF and stop()'s ``stderr.read()`` returns nothing, which would leave
192
+ ``_log_ffmpeg_stderr`` with an empty buffer and silently drop the
193
+ teardown diagnostic - the mid-stream ffmpeg error it exists to surface
194
+ included. Fall back to the bounded tail the drainer kept. Returns
195
+ ``b""`` when there is no drainer or it captured nothing, which
196
+ ``_log_ffmpeg_stderr`` already treats as "nothing to say".
197
+ """
198
+ tail = getattr(self._proc, "_aidot_stderr_tail", None)
199
+ if not tail:
200
+ return b""
201
+ return "\n".join(tail).encode("utf-8", "replace")
202
+
180
203
  def _log_ffmpeg_stderr(self, stderr_bytes: bytes) -> None:
181
204
  """Log ffmpeg's captured stderr at a level that matches what happened.
182
205
 
@@ -197,7 +220,8 @@ class SdesSession:
197
220
  or "Could not find codec parameters" in text
198
221
  )
199
222
  (_LOGGER.debug if expected_no_media else _LOGGER.warning)(
200
- "ffmpeg SDES stderr:\n%s", text
223
+ "camera %s: ffmpeg SDES stderr:\n%s",
224
+ getattr(self, "_device_id", "?"), text
201
225
  )
202
226
 
203
227
  @staticmethod
@@ -285,7 +309,7 @@ class SdesSession:
285
309
  self._proc.stderr.close()
286
310
  except Exception:
287
311
  _LOGGER.debug("swallowed exception in %s", 'stop', exc_info=True)
288
- self._log_ffmpeg_stderr(stderr_bytes)
312
+ self._log_ffmpeg_stderr(stderr_bytes or self._drained_stderr_tail())
289
313
  import os
290
314
  try:
291
315
  os.unlink(self._sdp_path)
@@ -38,6 +38,38 @@ from .protocol import (
38
38
 
39
39
  _LOGGER = logging.getLogger(__name__)
40
40
 
41
+
42
+ def _start_serve_stderr_drain(proc, *, maxlines: int = 40) -> None:
43
+ """Drain a serve ffmpeg's stderr continuously into a bounded tail on the proc.
44
+
45
+ The serve is spawned with ``stderr=PIPE`` but the bridge loop only polls the
46
+ process and reads its RTP sockets - it never reads stderr. An un-drained
47
+ ``PIPE`` fills its ~64KB kernel buffer and then blocks ffmpeg on its next
48
+ stderr write, stalling the serve; and when ffmpeg exits non-zero the reason is
49
+ otherwise lost (the RTSP-push ANNOUNCE error in particular). Read it on a
50
+ daemon thread that ends at EOF when the process exits, keeping the last
51
+ ``maxlines`` lines on ``proc._aidot_stderr_tail`` for the exit logger.
52
+ """
53
+ import collections
54
+ import threading
55
+
56
+ tail = collections.deque(maxlen=maxlines)
57
+ proc._aidot_stderr_tail = tail
58
+ if proc.stderr is None:
59
+ return
60
+
61
+ def _drain() -> None:
62
+ try:
63
+ for _line in iter(proc.stderr.readline, b""):
64
+ tail.append(_line.decode("utf-8", "replace").rstrip())
65
+ except Exception:
66
+ pass
67
+
68
+ threading.Thread(
69
+ target=_drain, daemon=True, name="aidot-sdes-serve-stderr"
70
+ ).start()
71
+
72
+
41
73
  # Strong references to detached per-session helper tasks, so the event loop
42
74
  # cannot garbage-collect one mid-flight. Entries remove themselves on done.
43
75
  _SDES_BACKGROUND_TASKS: set = set()
@@ -2793,16 +2825,33 @@ class _SdesOpenMixin:
2793
2825
  _br_level = _classify_ffmpeg_exit(
2794
2826
  _br_rc, _br_teardown_requested
2795
2827
  )
2828
+ # Name the camera. These lines used to carry no
2829
+ # device id at all, and this account runs several
2830
+ # SDES cameras at once, so a burst of exits could
2831
+ # not be attributed to one of them without extra
2832
+ # tooling - which sent a live investigation down
2833
+ # the wrong path once already.
2834
+ _br_dev = getattr(self, "device_id", "?")
2796
2835
  _br_msg = (
2797
- "SDES bridge: ffmpeg exited with code %d"
2798
- " - stopped by teardown"
2836
+ "camera %s: SDES bridge: ffmpeg exited with"
2837
+ " code %d - stopped by teardown"
2799
2838
  if _br_level < _log_br.WARNING else
2800
- "SDES bridge: ffmpeg exited with code %d"
2801
- " - stream ended"
2839
+ "camera %s: SDES bridge: ffmpeg exited with"
2840
+ " code %d - stream ended"
2802
2841
  )
2803
2842
  _log_br.getLogger(__name__).log(
2804
- _br_level, _br_msg, _br_rc
2843
+ _br_level, _br_msg, _br_dev, _br_rc
2805
2844
  )
2845
+ if _br_level >= _log_br.WARNING:
2846
+ _serr_tail = getattr(
2847
+ _br_proc, "_aidot_stderr_tail", None)
2848
+ if _serr_tail:
2849
+ _log_br.getLogger(__name__).warning(
2850
+ "camera %s: SDES serve ffmpeg stderr"
2851
+ " (last %d lines):\n%s", _br_dev,
2852
+ len(_serr_tail),
2853
+ "\n".join(_serr_tail),
2854
+ )
2806
2855
  if _bridge_should_break(
2807
2856
  _br_rc, _br_teardown_requested
2808
2857
  ):
@@ -4462,6 +4511,7 @@ class _SdesOpenMixin:
4462
4511
  stdout=subprocess.DEVNULL,
4463
4512
  stderr=subprocess.PIPE,
4464
4513
  )
4514
+ _start_serve_stderr_drain(proc)
4465
4515
  _proc_holder[0] = proc
4466
4516
  _cl(_reap, proc) # kill ffmpeg if the open is cancelled before hand-off
4467
4517
  except FileNotFoundError:
@@ -4745,6 +4795,7 @@ class _SdesOpenMixin:
4745
4795
  stdout=subprocess.DEVNULL,
4746
4796
  stderr=subprocess.PIPE,
4747
4797
  )
4798
+ _start_serve_stderr_drain(proc)
4748
4799
  # Point the shared holder at the live proc immediately.
4749
4800
  # The bridge thread polls _proc_holder[0]; if it still
4750
4801
  # sees the terminated old proc it logs "stream ended",
@@ -4866,4 +4917,5 @@ class _SdesOpenMixin:
4866
4917
  teardown_requested=_teardown_holder,
4867
4918
  first_video_pt=_first_video_pt,
4868
4919
  first_audio_pt=_first_audio_pt,
4920
+ device_id=getattr(self, "device_id", None),
4869
4921
  )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.15.2"
7
+ version = "0.15.4"
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: 0.15.2
3
+ Version: 0.15.4
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
@@ -138,6 +138,7 @@ tests/test_sdes_idle_release.py
138
138
  tests/test_sdes_open_cleanup.py
139
139
  tests/test_sdes_serve_audio.py
140
140
  tests/test_sdes_serve_cmd.py
141
+ tests/test_sdes_serve_stderr_drain.py
141
142
  tests/test_sdes_sprop.py
142
143
  tests/test_sdes_talk.py
143
144
  tests/test_sdes_teardown_exit_log.py
@@ -12,8 +12,10 @@ import types
12
12
  from aidot_cameras.camera.sdes import SdesSession
13
13
 
14
14
 
15
- def _log_fn(last_media):
16
- obj = types.SimpleNamespace(last_media_monotonic=last_media)
15
+ def _log_fn(last_media, device_id="dev-abc"):
16
+ obj = types.SimpleNamespace(
17
+ last_media_monotonic=last_media, _device_id=device_id
18
+ )
17
19
  return SdesSession._log_ffmpeg_stderr.__get__(obj)
18
20
 
19
21
 
@@ -31,6 +33,18 @@ def test_no_media_expected_shape_logs_debug(caplog):
31
33
  assert recs and recs[0].levelno == logging.DEBUG, msg
32
34
 
33
35
 
36
+ def test_the_log_names_the_camera(caplog):
37
+ """Several SDES cameras stream at once; an unattributed line is near-useless.
38
+
39
+ A burst of these lines with no device id could not be pinned to a camera
40
+ during a live investigation, which cost real time and one wrong conclusion.
41
+ """
42
+ with caplog.at_level(logging.DEBUG, logger="aidot_cameras.camera.sdes"):
43
+ _log_fn(1.0, device_id="12b144cb12da")(b"Error writing trailer")
44
+ recs = _stderr_records(caplog)
45
+ assert recs and "12b144cb12da" in recs[0].getMessage()
46
+
47
+
34
48
  def test_no_media_unexpected_error_still_warns(caplog):
35
49
  # No media, but NOT the expected empty-output shape -> a real ffmpeg failure.
36
50
  with caplog.at_level(logging.DEBUG, logger="aidot_cameras.camera.sdes"):
@@ -0,0 +1,190 @@
1
+ """Tests for the SDES serve ffmpeg stderr drain.
2
+
3
+ The serve is spawned with ``stderr=PIPE`` but the bridge loop never reads it, so an
4
+ un-drained pipe can fill and stall ffmpeg and a non-zero exit loses its reason.
5
+ ``_start_serve_stderr_drain`` reads the pipe on a daemon thread and keeps a bounded
6
+ tail on ``proc._aidot_stderr_tail`` for the exit logger.
7
+ """
8
+
9
+ import asyncio
10
+ import io
11
+ import logging
12
+ import time
13
+
14
+ from aidot_cameras.camera.sdes import SdesSession
15
+ from aidot_cameras.camera.sdes_open import _start_serve_stderr_drain
16
+
17
+
18
+ class _FakeProc:
19
+ """Minimal stand-in exposing a readable ``stderr``, like ``subprocess.Popen``."""
20
+
21
+ def __init__(self, data: bytes) -> None:
22
+ self.stderr = io.BytesIO(data)
23
+
24
+
25
+ def _tail(proc) -> list:
26
+ """The captured stderr tail (attribute is set dynamically by the drainer)."""
27
+ return list(getattr(proc, "_aidot_stderr_tail"))
28
+
29
+
30
+ def _wait_until(pred, timeout: float = 2.0) -> None:
31
+ deadline = time.monotonic() + timeout
32
+ while not pred():
33
+ if time.monotonic() > deadline:
34
+ break
35
+ time.sleep(0.01)
36
+
37
+
38
+ def test_stderr_drain_captures_lines() -> None:
39
+ proc = _FakeProc(b"line one\nline two\nline three\n")
40
+ _start_serve_stderr_drain(proc)
41
+ _wait_until(lambda: _tail(proc)[-1:] == ["line three"])
42
+ assert _tail(proc) == ["line one", "line two", "line three"]
43
+
44
+
45
+ def test_stderr_drain_is_bounded_to_the_tail() -> None:
46
+ data = b"".join(f"l{i}\n".encode() for i in range(100))
47
+ proc = _FakeProc(data)
48
+ _start_serve_stderr_drain(proc, maxlines=10)
49
+ _wait_until(lambda: _tail(proc)[-1:] == ["l99"])
50
+ assert _tail(proc) == [f"l{i}" for i in range(90, 100)]
51
+
52
+
53
+ def test_stderr_none_is_safe() -> None:
54
+ class _NoStderr:
55
+ stderr = None
56
+
57
+ proc = _NoStderr()
58
+ _start_serve_stderr_drain(proc) # must not raise
59
+ assert _tail(proc) == []
60
+
61
+
62
+ def test_every_piped_spawn_installs_the_drain() -> None:
63
+ """A serve spawned with an un-drained ``stderr=PIPE`` is the outage itself.
64
+
65
+ There are two serve-spawn sites (cold open and key-restart), and the bug
66
+ shipped because a pipe was opened at one of them with nothing reading it.
67
+ A third site, or a drain call deleted from an existing one, reintroduces it
68
+ exactly - and no behavioural test can see the difference until ffmpeg has
69
+ written ~64KB, which is far past where a unit test looks. Pin the source.
70
+ """
71
+ import inspect
72
+
73
+ from aidot_cameras.camera import sdes_open
74
+
75
+ src = inspect.getsource(sdes_open).splitlines()
76
+ spawns = [i for i, line in enumerate(src) if "stderr=subprocess.PIPE" in line]
77
+ assert spawns, (
78
+ "no piped spawn found in sdes_open - if the serve stopped using a pipe "
79
+ "this guard is obsolete, but do not delete it without checking why"
80
+ )
81
+ for line_no in spawns:
82
+ window = "\n".join(src[line_no:line_no + 12])
83
+ assert "_start_serve_stderr_drain(" in window, (
84
+ f"{sdes_open.__name__}:{line_no + 1} spawns with stderr=PIPE and "
85
+ "nothing drains it within the next few lines. An un-drained pipe "
86
+ "fills at ~64KB and blocks ffmpeg on its next write, which kills "
87
+ "the RTSP publisher and 404s every viewer"
88
+ )
89
+
90
+
91
+ class _TornDownProc:
92
+ """A serve proc whose stderr pipe the drainer already emptied.
93
+
94
+ ``stop()`` reads ``stderr`` after the process is gone; with the drain thread
95
+ running that read returns nothing, so the teardown diagnostic has to come
96
+ from the tail the drainer kept.
97
+ """
98
+
99
+ def __init__(self, tail: list) -> None:
100
+ self._alive = True
101
+ self.returncode = 0
102
+ self.stderr = self
103
+ self._aidot_stderr_tail = tail
104
+
105
+ def poll(self):
106
+ return None if self._alive else self.returncode
107
+
108
+ def terminate(self):
109
+ self._alive = False
110
+
111
+ def wait(self, timeout=None):
112
+ self._alive = False
113
+ return self.returncode
114
+
115
+ def kill(self):
116
+ self._alive = False
117
+
118
+ def read(self): # stderr.read() - the drainer got there first
119
+ return b""
120
+
121
+
122
+ class _FakeSock:
123
+ def close(self) -> None:
124
+ pass
125
+
126
+
127
+ class _FakeQ:
128
+ def put_nowait(self, _x) -> None:
129
+ pass
130
+
131
+
132
+ def _stopped_session(tail: list) -> None:
133
+ """Run the real ``SdesSession.stop()`` against a drained serve proc."""
134
+ session = SdesSession(
135
+ proc=_TornDownProc(tail),
136
+ sdp_path="/tmp/aidot_test_does_not_exist.sdp",
137
+ outgoing_q=_FakeQ(),
138
+ mqtt_fut=None,
139
+ audio_sock=_FakeSock(),
140
+ video_sock=_FakeSock(),
141
+ cmd_chan=[None],
142
+ talk_state=None,
143
+ )
144
+ asyncio.run(session.stop())
145
+
146
+
147
+ def test_teardown_logs_the_drained_tail_when_the_pipe_is_empty(caplog) -> None:
148
+ # Without the fallback this diagnostic is lost entirely: the drain thread
149
+ # consumed the pipe, so stop()'s stderr.read() has nothing left to log.
150
+ caplog.set_level(logging.DEBUG, logger="aidot_cameras.camera.sdes")
151
+ _stopped_session(["frame= 120 fps=20", "Error writing trailer: Broken pipe"])
152
+ warnings = [
153
+ r for r in caplog.records
154
+ if r.levelno == logging.WARNING and "ffmpeg SDES stderr" in r.getMessage()
155
+ ]
156
+ assert warnings, "a mid-stream ffmpeg error must still reach the log"
157
+ assert "Error writing trailer: Broken pipe" in warnings[0].getMessage()
158
+
159
+
160
+ def test_teardown_tail_still_demotes_the_expected_no_media_shape(caplog) -> None:
161
+ # The tail feeds the SAME classifier, so a camera that never sent media does
162
+ # not start warning once per retry just because the stderr route changed.
163
+ caplog.set_level(logging.DEBUG, logger="aidot_cameras.camera.sdes")
164
+ _stopped_session(["Output file is empty, nothing was encoded"])
165
+ assert not [
166
+ r for r in caplog.records
167
+ if r.levelno == logging.WARNING and "ffmpeg SDES stderr" in r.getMessage()
168
+ ]
169
+ assert [
170
+ r for r in caplog.records
171
+ if r.levelno == logging.DEBUG and "ffmpeg SDES stderr" in r.getMessage()
172
+ ]
173
+
174
+
175
+ def test_teardown_without_a_tail_logs_nothing(caplog) -> None:
176
+ caplog.set_level(logging.DEBUG, logger="aidot_cameras.camera.sdes")
177
+ _stopped_session([])
178
+ assert not [
179
+ r for r in caplog.records if "ffmpeg SDES stderr" in r.getMessage()
180
+ ]
181
+
182
+
183
+ def test_undecodable_bytes_do_not_crash_the_drain() -> None:
184
+ proc = _FakeProc(b"good\n\xff\xfe bad bytes\ndone\n")
185
+ _start_serve_stderr_drain(proc)
186
+ _wait_until(lambda: _tail(proc)[-1:] == ["done"])
187
+ tail = _tail(proc)
188
+ assert tail[0] == "good"
189
+ assert tail[-1] == "done"
190
+ assert len(tail) == 3
@@ -61,6 +61,27 @@ def test_reap_sets_teardown_flag_before_kill():
61
61
  )
62
62
 
63
63
 
64
+ def test_the_bridge_exit_log_names_the_camera():
65
+ """The exit line is emitted from a closure the tests cannot call directly.
66
+
67
+ It is still worth pinning: this account streams several SDES cameras at
68
+ once, and while these lines carried no device id a burst of exits could not
69
+ be attributed to a camera at all. Assert the format string takes one, and
70
+ that the session is handed the id it needs for its own stderr log.
71
+ """
72
+ src = inspect.getsource(sdes_open)
73
+ assert "camera %s: SDES bridge: ffmpeg exited" in src, (
74
+ "the bridge exit log lost its device id - a burst of these is "
75
+ "unattributable without it"
76
+ )
77
+ assert "camera %s: SDES serve ffmpeg stderr" in src
78
+ start = src.index("return SdesSession(")
79
+ assert "device_id=" in src[start:start + 800], (
80
+ "SdesSession is constructed without a device_id, so its own stderr "
81
+ "log cannot name the camera"
82
+ )
83
+
84
+
64
85
  class _FakeProc:
65
86
  def __init__(self):
66
87
  self._alive = True