python-aidot-cameras 0.12.12__tar.gz → 0.12.13__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 (145) hide show
  1. {python_aidot_cameras-0.12.12/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.13}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/client.py +21 -0
  3. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/sdes_open.py +11 -0
  4. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/pyproject.toml +1 -1
  5. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  7. python_aidot_cameras-0.12.13/tests/test_dormant_camera_cleanup.py +65 -0
  8. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_viewer_check_throttle.py +0 -1
  9. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/LICENSE +0 -0
  10. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/README.md +0 -0
  12. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/__init__.py +0 -0
  13. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/__main__.py +0 -0
  14. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/__init__.py +0 -0
  15. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  16. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  17. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  18. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  19. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  20. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  21. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  22. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  23. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  24. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  25. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  26. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  27. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  28. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  29. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  30. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  31. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  32. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  33. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  34. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  35. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  36. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  37. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  38. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  39. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  40. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  41. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  42. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  43. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  44. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  45. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  46. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  47. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/__init__.py +0 -0
  48. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/constants.py +0 -0
  49. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/controls.py +0 -0
  50. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/go2rtc.py +0 -0
  51. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/lan_control.py +0 -0
  52. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/models.py +0 -0
  53. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/playback.py +0 -0
  54. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/protocol.py +0 -0
  55. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/sdes.py +0 -0
  56. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/tutk.py +0 -0
  57. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/webrtc.py +0 -0
  58. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/camera/webrtc_open.py +0 -0
  59. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/client.py +0 -0
  60. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/const.py +0 -0
  61. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/credentials.py +0 -0
  62. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/crypto.py +0 -0
  63. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/device_client.py +0 -0
  64. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/discover.py +0 -0
  65. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/exceptions.py +0 -0
  66. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/g711.py +0 -0
  67. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/aidot_cameras/py.typed +0 -0
  68. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  69. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  70. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/python_aidot_cameras.egg-info/requires.txt +0 -0
  71. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  72. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/setup.cfg +0 -0
  73. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_aioice_compat.py +0 -0
  74. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_alarm_event.py +0 -0
  75. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_backoff.py +0 -0
  76. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_deferred_hardening.py +0 -0
  77. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_device_login_guard.py +0 -0
  78. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_device_user_info_cache.py +0 -0
  79. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_dtls_not_ready_burst.py +0 -0
  80. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_dtls_pinning.py +0 -0
  81. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_dtls_serve_decode_skip.py +0 -0
  82. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_dtls_serve_open_timeout.py +0 -0
  83. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_dtls_skip_signaling_wait.py +0 -0
  84. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_dtls_slow_probe.py +0 -0
  85. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_egress_guard.py +0 -0
  86. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_go2rtc.py +0 -0
  87. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_go2rtc_cli.py +0 -0
  88. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_h264_decode_rate_limit.py +0 -0
  89. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_highport_nomination.py +0 -0
  90. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_ice_config_cache.py +0 -0
  91. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_idle_release_both_loops.py +0 -0
  92. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_keepalive_renew.py +0 -0
  93. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_key_restart_sdp.py +0 -0
  94. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_keyframe_prompter.py +0 -0
  95. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_lan_control.py +0 -0
  96. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_light_active_color_mode.py +0 -0
  97. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_live_stream_param.py +0 -0
  98. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_logging_caps.py +0 -0
  99. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_motion_poll.py +0 -0
  100. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_mqtt_credential_refetch.py +0 -0
  101. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_mqtt_credential_self_heal.py +0 -0
  102. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_mqtt_password_not_persisted.py +0 -0
  103. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_mqtt_session_reconnect.py +0 -0
  104. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_narrow_pc_ice.py +0 -0
  105. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_no_self_referential_go2rtc.py +0 -0
  106. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_no_undefined_names.py +0 -0
  107. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_offline_keepalive_pause.py +0 -0
  108. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_open_fail_logger.py +0 -0
  109. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_open_gate_delay.py +0 -0
  110. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_persistent_mqtt.py +0 -0
  111. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_persistent_mqtt_robustness.py +0 -0
  112. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_playback_tls.py +0 -0
  113. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_post_merge_hardening.py +0 -0
  114. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_retry_policy.py +0 -0
  115. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_adaptive.py +0 -0
  116. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_bridge_break_gate.py +0 -0
  117. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_echo_wait_timeout.py +0 -0
  118. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_fast_liveplay.py +0 -0
  119. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  120. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_idle_release.py +0 -0
  121. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_open_cleanup.py +0 -0
  122. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_serve_audio.py +0 -0
  123. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_serve_cmd.py +0 -0
  124. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_sprop.py +0 -0
  125. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_talk.py +0 -0
  126. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_teardown_exit_log.py +0 -0
  127. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdes_watchdog.py +0 -0
  128. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_sdp_payload_type_narrowing.py +0 -0
  129. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_security_hardening.py +0 -0
  130. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_serializable_login_info.py +0 -0
  131. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_serve_relay.py +0 -0
  132. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_session_stats.py +0 -0
  133. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_speak.py +0 -0
  134. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_stream_cap.py +0 -0
  135. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_stream_drain_ownership.py +0 -0
  136. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_stream_idle.py +0 -0
  137. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_stream_slot_cap.py +0 -0
  138. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_stream_teardown.py +0 -0
  139. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_talk.py +0 -0
  140. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_terminal_ack.py +0 -0
  141. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_token_file_robustness.py +0 -0
  142. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_token_refresh.py +0 -0
  143. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_upstream_compat.py +0 -0
  144. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_viewer_presence.py +0 -0
  145. {python_aidot_cameras-0.12.12 → python_aidot_cameras-0.12.13}/tests/test_wait_or_event.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.12.12
3
+ Version: 0.12.13
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
@@ -3155,6 +3155,19 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3155
3155
  self._cancel_keepalive_renew()
3156
3156
  self._keepalive_rtsp_url = None
3157
3157
  self._serve_ready.clear()
3158
+ # Take the stream out of go2rtc too. Leaving it registered
3159
+ # points go2rtc at a source that no longer exists, so a viewer
3160
+ # attaching while the camera is dormant gets a hard refusal
3161
+ # ("connection refused" on the serve port) instead of a clean
3162
+ # "no such stream" - and in push mode that source was never
3163
+ # dialable in the first place. stream_source() re-registers on
3164
+ # the next view.
3165
+ try:
3166
+ await self._deregister_go2rtc()
3167
+ except Exception:
3168
+ _LOGGER.debug("camera %s: swallowed exception in %s",
3169
+ getattr(self, "device_id", "?"),
3170
+ "_deregister_go2rtc", exc_info=True)
3158
3171
  _LOGGER.debug(
3159
3172
  "SDES serve: %s idle (no viewer) - released until next view",
3160
3173
  self.device_id,
@@ -4116,6 +4129,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
4116
4129
  self._cancel_keepalive_renew()
4117
4130
  self._keepalive_rtsp_url = None
4118
4131
  self._serve_ready.clear()
4132
+ # Same as the SDES path: do not leave go2rtc holding a stream
4133
+ # whose source no longer exists.
4134
+ try:
4135
+ await self._deregister_go2rtc()
4136
+ except Exception:
4137
+ _LOGGER.debug("camera %s: swallowed exception in %s",
4138
+ getattr(self, "device_id", "?"),
4139
+ "_deregister_go2rtc", exc_info=True)
4119
4140
  _LOGGER.debug(
4120
4141
  "DTLS serve: %s idle (no viewer) - released until next view",
4121
4142
  self.device_id,
@@ -102,6 +102,11 @@ def _sdes_echo_wait_timeout(skip_liveplay: bool) -> float:
102
102
  return 0.0 if skip_liveplay else 2.0
103
103
 
104
104
 
105
+ # ffmpeg returns AVERROR(EPIPE) = -32 when its output consumer disappears, and a
106
+ # process exit status is truncated to an unsigned byte: -32 & 0xFF = 224.
107
+ _FFMPEG_EXIT_EPIPE = 224
108
+
109
+
105
110
  def _classify_ffmpeg_exit(rc: int, teardown_requested: bool) -> int:
106
111
  """Log level for the bridge observe loop's "ffmpeg exited" line.
107
112
 
@@ -116,6 +121,12 @@ def _classify_ffmpeg_exit(rc: int, teardown_requested: bool) -> int:
116
121
  """
117
122
  if rc < 0 and teardown_requested:
118
123
  return logging.DEBUG
124
+ if rc == _FFMPEG_EXIT_EPIPE:
125
+ # Broken pipe: the consumer went away. That is the NORMAL end of a
126
+ # `-listen 1` serve - go2rtc disconnects and ffmpeg exits - so warning
127
+ # about it trains the reader to ignore real failures. A process exit
128
+ # status is an unsigned byte, so AVERROR(EPIPE) = -32 surfaces as 224.
129
+ return logging.DEBUG
119
130
  return logging.WARNING
120
131
 
121
132
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.12.12"
7
+ version = "0.12.13"
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.12.12
3
+ Version: 0.12.13
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
@@ -72,6 +72,7 @@ tests/test_backoff.py
72
72
  tests/test_deferred_hardening.py
73
73
  tests/test_device_login_guard.py
74
74
  tests/test_device_user_info_cache.py
75
+ tests/test_dormant_camera_cleanup.py
75
76
  tests/test_dtls_not_ready_burst.py
76
77
  tests/test_dtls_pinning.py
77
78
  tests/test_dtls_serve_decode_skip.py
@@ -0,0 +1,65 @@
1
+ """A dormant camera must not leave a dead stream registered in go2rtc.
2
+
3
+ Idle-release stops the serve but used to leave the go2rtc stream in place,
4
+ pointing at a source that no longer exists. A viewer attaching then gets a hard
5
+ "connection refused" on the serve port rather than a clean miss - and in PUSH mode
6
+ that source was never dialable at all, because the keepalive publishes INTO go2rtc
7
+ rather than serving over HTTP.
8
+
9
+ Observed live: four go2rtc streams registered against serve ports with nothing
10
+ listening, and go2rtc reporting
11
+ `dial tcp 127.0.0.1:18981: connect: connection refused`.
12
+ """
13
+ import inspect
14
+
15
+ import aidot_cameras.camera.client as cc
16
+ from aidot_cameras.camera.sdes_open import (
17
+ _FFMPEG_EXIT_EPIPE,
18
+ _classify_ffmpeg_exit,
19
+ )
20
+ import logging
21
+
22
+
23
+ def _src(name):
24
+ return inspect.getsource(getattr(cc.CameraMixin, name))
25
+
26
+
27
+ def test_sdes_idle_release_deregisters_from_go2rtc():
28
+ src = _src("_sdes_keepalive_loop_inner")
29
+ idle = src[src.index("if _idle_release:"):]
30
+ assert "_deregister_go2rtc" in idle
31
+
32
+
33
+ def test_dtls_idle_release_deregisters_from_go2rtc():
34
+ src = _src("_dtls_serve_loop_inner")
35
+ idle = src[src.index("if idle_release:"):]
36
+ assert "_deregister_go2rtc" in idle
37
+
38
+
39
+ def test_deregistration_failure_cannot_break_the_release():
40
+ # Going dormant must still complete if go2rtc is unreachable.
41
+ for name, marker in (("_sdes_keepalive_loop_inner", "if _idle_release:"),
42
+ ("_dtls_serve_loop_inner", "if idle_release:")):
43
+ idle = _src(name)
44
+ idle = idle[idle.index(marker):]
45
+ block = idle[idle.index("_deregister_go2rtc") - 200:
46
+ idle.index("_deregister_go2rtc") + 300]
47
+ assert "try:" in block and "except Exception" in block
48
+
49
+
50
+ def test_consumer_disconnect_is_not_logged_as_a_failure():
51
+ # ffmpeg returns AVERROR(EPIPE) = -32 when its consumer goes away, and an exit
52
+ # status is an unsigned byte, so it surfaces as 224. That is the normal end of
53
+ # a `-listen 1` serve, not a fault.
54
+ assert _FFMPEG_EXIT_EPIPE == 224
55
+ assert _classify_ffmpeg_exit(224, teardown_requested=False) == logging.DEBUG
56
+ assert _classify_ffmpeg_exit(224, teardown_requested=True) == logging.DEBUG
57
+
58
+
59
+ def test_real_failures_still_warn():
60
+ assert _classify_ffmpeg_exit(1, teardown_requested=False) == logging.WARNING
61
+ assert _classify_ffmpeg_exit(255, teardown_requested=False) == logging.WARNING
62
+ # A signal death with no teardown in flight is still unexpected.
63
+ assert _classify_ffmpeg_exit(-9, teardown_requested=False) == logging.WARNING
64
+ # ...but an expected teardown is quiet.
65
+ assert _classify_ffmpeg_exit(-9, teardown_requested=True) == logging.DEBUG
@@ -8,7 +8,6 @@ anywhere. Idle release is measured in minutes, so a few seconds of staleness in
8
8
  the answer costs nothing.
9
9
  """
10
10
  import asyncio
11
- import time
12
11
 
13
12
  import aidot_cameras.camera.client as cc
14
13
  from aidot_cameras.device_client import CameraDeviceClient