python-aidot-cameras 0.12.13__tar.gz → 0.12.14__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.
- {python_aidot_cameras-0.12.13/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.14}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/client.py +9 -1
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/protocol.py +75 -7
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/pyproject.toml +1 -1
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.12.14/tests/test_dtls_mux_terminal_write.py +126 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/LICENSE +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/README.md +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.13/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.14}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.14
|
|
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
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/client.py
RENAMED
|
@@ -4080,7 +4080,15 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
4080
4080
|
# Unknown: keep the old staleness heuristic rather
|
|
4081
4081
|
# than holding the stream open forever.
|
|
4082
4082
|
idle_release = True
|
|
4083
|
-
|
|
4083
|
+
# Only an actual idle release ends this serve cycle.
|
|
4084
|
+
# (This break was unconditional - any idle_secs > 0
|
|
4085
|
+
# tore down a HEALTHY ffmpeg after one 0.5s tick,
|
|
4086
|
+
# respawning it forever: ffmpeg died at output-open
|
|
4087
|
+
# with "Immediate exit requested" and every DTLS
|
|
4088
|
+
# camera served nothing. stream_idle_s=0 masked it,
|
|
4089
|
+
# which is why never-release fleets still worked.)
|
|
4090
|
+
if idle_release:
|
|
4091
|
+
break
|
|
4084
4092
|
# Tear down this ffmpeg+mux cycle before the next.
|
|
4085
4093
|
stop_flag.set()
|
|
4086
4094
|
try:
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -925,8 +925,52 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
925
925
|
except Exception as exc: # pragma: no cover
|
|
926
926
|
_LOGGER.warning("DTLS A/V mux: PyAV unavailable: %s", exc)
|
|
927
927
|
return
|
|
928
|
+
|
|
929
|
+
class _FailFlagWriter:
|
|
930
|
+
"""Write-only stand-in for the pipe that RECORDS write failures.
|
|
931
|
+
|
|
932
|
+
FFmpeg's mpegts muxer checks avio write errors lazily: a dead pipe
|
|
933
|
+
(EPIPE after its ffmpeg exits) makes ``out.mux()`` appear to succeed
|
|
934
|
+
forever, and the error only surfaces - or hangs - at trailer time.
|
|
935
|
+
PyAV's pyio callback DOES see each failed ``write``; recording it here
|
|
936
|
+
lets the mux loop notice within one iteration and exit, instead of an
|
|
937
|
+
abandoned thread draining the SHARED vq/aq into a dead pipe and
|
|
938
|
+
starving the next cycle's ffmpeg (the 2026-07 dead-DTLS-video field
|
|
939
|
+
failure). Deliberately exposes no seek/tell: the pipe is streaming.
|
|
940
|
+
"""
|
|
941
|
+
|
|
942
|
+
def __init__(self, f):
|
|
943
|
+
self._f = f
|
|
944
|
+
self.failed = False
|
|
945
|
+
|
|
946
|
+
def write(self, b):
|
|
947
|
+
try:
|
|
948
|
+
return self._f.write(b)
|
|
949
|
+
except Exception:
|
|
950
|
+
self.failed = True
|
|
951
|
+
raise
|
|
952
|
+
|
|
953
|
+
def flush(self):
|
|
954
|
+
try:
|
|
955
|
+
self._f.flush()
|
|
956
|
+
except Exception:
|
|
957
|
+
self.failed = True
|
|
958
|
+
raise
|
|
959
|
+
|
|
960
|
+
def close(self):
|
|
961
|
+
self._f.close()
|
|
962
|
+
|
|
963
|
+
wsink = _FailFlagWriter(out_fileobj)
|
|
928
964
|
try:
|
|
929
|
-
|
|
965
|
+
# max_interleave_delta (default 10s of stream time): with an AAC stream
|
|
966
|
+
# declared but no audio packets arriving, libavformat's interleave
|
|
967
|
+
# queue holds EVERY video packet waiting to order them against audio -
|
|
968
|
+
# zero bytes reach the pipe, the serve ffmpeg starves at its input
|
|
969
|
+
# probe and never binds its -listen port (field failure 2026-07: all
|
|
970
|
+
# DTLS cameras dark while the taps were full of frames). 100ms keeps
|
|
971
|
+
# video flowing whether or not audio ever shows up.
|
|
972
|
+
out = av.open(wsink, "w", format="mpegts",
|
|
973
|
+
options={"max_interleave_delta": "100000"})
|
|
930
974
|
except Exception as exc:
|
|
931
975
|
_LOGGER.warning("DTLS A/V mux: av.open failed: %s", exc)
|
|
932
976
|
return
|
|
@@ -975,6 +1019,14 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
975
1019
|
a_rtp0 = [None] # first audio RTP timestamp (8 kHz units), for gap detection
|
|
976
1020
|
a_in = [0] # 8 kHz samples emitted to the resampler so far (incl. concealed)
|
|
977
1021
|
vstarted = [False]
|
|
1022
|
+
# A failed container write is TERMINAL for this mux: the pipe's ffmpeg is
|
|
1023
|
+
# gone (EPIPE) and can never come back for this cycle. Swallowing it and
|
|
1024
|
+
# looping used to leave abandoned mux threads (the teardown join(2.0) gives
|
|
1025
|
+
# up on a thread blocked in write) racing the NEXT cycle's thread for the
|
|
1026
|
+
# SHARED vq/aq - stealing its frames into a dead pipe, starving the new
|
|
1027
|
+
# ffmpeg at its input probe so it never bound its -listen port. One dead
|
|
1028
|
+
# write -> exit the thread and leave the queues alone.
|
|
1029
|
+
mux_dead = [False]
|
|
978
1030
|
|
|
979
1031
|
def _flush_video():
|
|
980
1032
|
while True:
|
|
@@ -995,7 +1047,9 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
995
1047
|
out.mux(pkt)
|
|
996
1048
|
progress[0] = _t.monotonic()
|
|
997
1049
|
except Exception:
|
|
998
|
-
|
|
1050
|
+
mux_dead[0] = True
|
|
1051
|
+
_LOGGER.debug("DTLS A/V mux: video write failed - ending mux thread", exc_info=True)
|
|
1052
|
+
return
|
|
999
1053
|
|
|
1000
1054
|
def _flush_audio(drain=False):
|
|
1001
1055
|
if not have_audio:
|
|
@@ -1115,20 +1169,34 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1115
1169
|
out.mux(opkt)
|
|
1116
1170
|
progress[0] = _t.monotonic()
|
|
1117
1171
|
except Exception:
|
|
1118
|
-
|
|
1172
|
+
mux_dead[0] = True
|
|
1173
|
+
_LOGGER.debug("DTLS A/V mux: audio write failed - ending mux thread", exc_info=True)
|
|
1174
|
+
return
|
|
1175
|
+
|
|
1176
|
+
def _dead() -> bool:
|
|
1177
|
+
return mux_dead[0] or wsink.failed
|
|
1119
1178
|
|
|
1120
|
-
while not stop_flag.is_set():
|
|
1179
|
+
while not stop_flag.is_set() and not _dead():
|
|
1121
1180
|
_flush_video()
|
|
1122
1181
|
_flush_audio()
|
|
1123
1182
|
_t.sleep(0.005) # 5 ms - processes audio within 5 ms of arrival vs 20 ms
|
|
1124
|
-
|
|
1125
|
-
|
|
1183
|
+
if not _dead():
|
|
1184
|
+
_flush_video()
|
|
1185
|
+
_flush_audio(drain=True) # final: pad the last partial frame so it flushes
|
|
1126
1186
|
try:
|
|
1127
|
-
if have_audio:
|
|
1187
|
+
if have_audio and not _dead():
|
|
1128
1188
|
for opkt in aenc.encode(None): # flush
|
|
1129
1189
|
out.mux(opkt)
|
|
1130
1190
|
except Exception:
|
|
1131
1191
|
_LOGGER.debug("swallowed exception in %s", '_dtls_av_mux_run', exc_info=True)
|
|
1192
|
+
if _dead():
|
|
1193
|
+
# Close the raw pipe FIRST: the container's trailer flush then fails
|
|
1194
|
+
# fast ("closed file") instead of hanging on the dead pipe.
|
|
1195
|
+
_LOGGER.debug("DTLS A/V mux: sink write failed - ending mux thread")
|
|
1196
|
+
try:
|
|
1197
|
+
wsink.close()
|
|
1198
|
+
except Exception:
|
|
1199
|
+
pass
|
|
1132
1200
|
try:
|
|
1133
1201
|
out.close()
|
|
1134
1202
|
except Exception:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.14"
|
|
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"
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.14
|
|
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
|
|
@@ -73,6 +73,7 @@ tests/test_deferred_hardening.py
|
|
|
73
73
|
tests/test_device_login_guard.py
|
|
74
74
|
tests/test_device_user_info_cache.py
|
|
75
75
|
tests/test_dormant_camera_cleanup.py
|
|
76
|
+
tests/test_dtls_mux_terminal_write.py
|
|
76
77
|
tests/test_dtls_not_ready_burst.py
|
|
77
78
|
tests/test_dtls_pinning.py
|
|
78
79
|
tests/test_dtls_serve_decode_skip.py
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""A failed container write must END the DTLS mux thread, not be swallowed.
|
|
2
|
+
|
|
3
|
+
Regression for the abandoned-mux-thread leak: teardown's join(2.0) gives up on
|
|
4
|
+
a thread blocked in a pipe write; once that pipe's ffmpeg dies the write raises
|
|
5
|
+
EPIPE forever. The old per-packet swallow kept the abandoned thread looping,
|
|
6
|
+
stealing frames from the SHARED vq/aq that the next cycle's thread needed - the
|
|
7
|
+
new ffmpeg starved at its input probe and never bound its -listen port (the
|
|
8
|
+
field "dead DTLS video" of 2026-07). A dead write now exits the thread and
|
|
9
|
+
leaves the queues alone.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import queue
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
av = pytest.importorskip("av")
|
|
20
|
+
|
|
21
|
+
from aidot_cameras.camera.protocol import _dtls_av_mux_run
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _h264_payloads(n=60):
|
|
25
|
+
"""Real encoded H.264 access units (the mux needs valid-ish annex-b).
|
|
26
|
+
|
|
27
|
+
Noise frames compress terribly on purpose: the total must exceed PyAV's
|
|
28
|
+
~32KB avio buffer so the container performs REAL pipe writes (a smaller
|
|
29
|
+
payload sits in the buffer forever and never observes the dead pipe).
|
|
30
|
+
"""
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
rng = np.random.default_rng(7)
|
|
34
|
+
enc = av.CodecContext.create("libx264", "w")
|
|
35
|
+
enc.width, enc.height, enc.pix_fmt = 320, 240, "yuv420p"
|
|
36
|
+
from fractions import Fraction
|
|
37
|
+
|
|
38
|
+
enc.time_base = Fraction(1, 15)
|
|
39
|
+
enc.options = {"tune": "zerolatency"}
|
|
40
|
+
out = []
|
|
41
|
+
for i in range(n):
|
|
42
|
+
img = rng.integers(0, 255, (240, 320, 3), dtype=np.uint8)
|
|
43
|
+
fr = av.VideoFrame.from_ndarray(img, format="rgb24").reformat(format="yuv420p")
|
|
44
|
+
fr.pts = i
|
|
45
|
+
for p in enc.encode(fr):
|
|
46
|
+
out.append((bytes(p), bool(p.is_keyframe)))
|
|
47
|
+
for p in enc.encode(None):
|
|
48
|
+
out.append((bytes(p), bool(p.is_keyframe)))
|
|
49
|
+
assert sum(len(d) for d, _ in out) > 100_000
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_dead_pipe_ends_mux_thread_and_spares_queues():
|
|
54
|
+
payloads = _h264_payloads()
|
|
55
|
+
assert payloads and payloads[0][1], "first packet should be a keyframe"
|
|
56
|
+
|
|
57
|
+
vq: queue.Queue = queue.Queue()
|
|
58
|
+
aq: queue.Queue = queue.Queue()
|
|
59
|
+
ts = 0
|
|
60
|
+
for data, kf in payloads:
|
|
61
|
+
vq.put((data, ts, kf))
|
|
62
|
+
ts += 6000
|
|
63
|
+
|
|
64
|
+
r, w = os.pipe()
|
|
65
|
+
os.close(r) # reader already gone -> every flushed write is EPIPE
|
|
66
|
+
wfile = os.fdopen(w, "wb", buffering=0)
|
|
67
|
+
progress = [time.monotonic()]
|
|
68
|
+
stop_flag = threading.Event()
|
|
69
|
+
|
|
70
|
+
t = threading.Thread(
|
|
71
|
+
target=_dtls_av_mux_run, args=(vq, aq, wfile, progress, stop_flag), daemon=True
|
|
72
|
+
)
|
|
73
|
+
t.start()
|
|
74
|
+
t.join(timeout=10.0)
|
|
75
|
+
|
|
76
|
+
# The thread must have exited on its own (stop_flag was never set).
|
|
77
|
+
assert not t.is_alive(), "mux thread must end on a dead pipe, not loop forever"
|
|
78
|
+
assert not stop_flag.is_set()
|
|
79
|
+
# And it must not have drained the whole queue into the dead pipe: once the
|
|
80
|
+
# avio buffer first flushes and EPIPEs, remaining frames stay for the next
|
|
81
|
+
# cycle's consumer. (The avio buffer absorbs the first ~32KB, so a few
|
|
82
|
+
# packets are legitimately consumed before the first real write.)
|
|
83
|
+
assert not vq.empty(), "a dead mux must leave queued frames for the next cycle"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_healthy_pipe_still_muxes():
|
|
87
|
+
payloads = _h264_payloads()
|
|
88
|
+
vq: queue.Queue = queue.Queue()
|
|
89
|
+
aq: queue.Queue = queue.Queue()
|
|
90
|
+
ts = 0
|
|
91
|
+
for data, kf in payloads:
|
|
92
|
+
vq.put((data, ts, kf))
|
|
93
|
+
ts += 6000
|
|
94
|
+
|
|
95
|
+
r, w = os.pipe()
|
|
96
|
+
got = [0]
|
|
97
|
+
|
|
98
|
+
def drain():
|
|
99
|
+
while True:
|
|
100
|
+
try:
|
|
101
|
+
b = os.read(r, 65536)
|
|
102
|
+
except OSError:
|
|
103
|
+
return
|
|
104
|
+
if not b:
|
|
105
|
+
return
|
|
106
|
+
got[0] += len(b)
|
|
107
|
+
|
|
108
|
+
threading.Thread(target=drain, daemon=True).start()
|
|
109
|
+
wfile = os.fdopen(w, "wb", buffering=0)
|
|
110
|
+
progress = [time.monotonic()]
|
|
111
|
+
stop_flag = threading.Event()
|
|
112
|
+
t = threading.Thread(
|
|
113
|
+
target=_dtls_av_mux_run, args=(vq, aq, wfile, progress, stop_flag), daemon=True
|
|
114
|
+
)
|
|
115
|
+
t.start()
|
|
116
|
+
time.sleep(1.5)
|
|
117
|
+
# REGRESSION (interleave stall): with an AAC stream declared but zero audio
|
|
118
|
+
# packets, libavformat used to buffer ALL video in its interleave queue -
|
|
119
|
+
# bytes only appeared at close(). They must flow DURING muxing.
|
|
120
|
+
assert got[0] > 10_000, (
|
|
121
|
+
"video must reach the pipe while muxing, without any audio packets"
|
|
122
|
+
)
|
|
123
|
+
stop_flag.set()
|
|
124
|
+
t.join(timeout=10.0)
|
|
125
|
+
assert not t.is_alive()
|
|
126
|
+
assert vq.empty(), "healthy mux consumes the queue"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/clock.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/go2rtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/models.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/sdes_open.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/webrtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/aidot_cameras/device_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dormant_camera_cleanup.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_serve_open_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_ice_config_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_idle_release_both_loops.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_keyframe_prompter.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_light_active_color_mode.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_live_stream_param.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_mqtt_credential_refetch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_open_fail_logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_bridge_break_gate.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_ffmpeg_stderr_log.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_idle_release.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_open_cleanup.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_serve_audio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_stream_drain_ownership.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.13 → python_aidot_cameras-0.12.14}/tests/test_viewer_check_throttle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|