python-aidot-cameras 1.0.0rc65__tar.gz → 1.0.0rc67__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-1.0.0rc65/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc67}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/recording.py +15 -4
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sd_playback.py +87 -7
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/webrtc_open.py +98 -21
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_recorder_wiring.py +34 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_dtls.py +127 -6
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/README.md +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_log_redaction.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/agc.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/health.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/hls_ts.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/settings.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_arrival_timestamps.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_auth_error_routing.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_auto_alarm.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_auto_light_settings.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_header_single_layout.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_cap_spans_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_battery_wake_settles_before_the_command.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ble_gateway_is_gone.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_error_log_names_the_exception.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_error_severity.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_declared_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_detection_types.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_direct_ts_serve_only_for_http.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_refusal_after_the_echo.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_tap_states_no_reorder.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_ts_copy_recording.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_every_exception_is_exported.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_first_media_abandon_rules.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_health_line.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_health_line_counts_pictures.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_hls_ts_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_hls_ts_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ice_credentials_match_the_app.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ipc_envelope_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_login_accessors.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_short_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_retry_stops_on_close.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_lan_sweep_only_with_lights.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_validate_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_log_hygiene.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_loop_break_ends_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_mypy_new_findings.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_unbounded_hex_dumps.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_open_errors_are_aidot_errors.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_phase_returns_reach_the_caller.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_phase_state_is_complete.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_plaintext_mqtt_refused.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_region_allowlist.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_review_lan_retry_and_graces.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_rtsp_publish_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_clock.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_entry.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_media_hold.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_open.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_open_yields_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_pinned_codec_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_loop_releases_on_error.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_session_mode_seq.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_session_stop_twice.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_settings_registry.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_skip_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_status_and_trace_channels.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_stun_attr_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_swallowed_exception_names.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_transport_failure_reporting.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_turn_target_shared.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_upstream_seam_adapter.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_upstream_secrets_redacted.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_vendored_aiortc_integrity.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_presence_needs_a_keyframe.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_webrtc_answer_race.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc67
|
|
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-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/camera/recording.py
RENAMED
|
@@ -55,6 +55,7 @@ class TsCopyRecorder:
|
|
|
55
55
|
path: str,
|
|
56
56
|
device_id: Optional[str] = None,
|
|
57
57
|
video_codec: Optional[str] = "h264",
|
|
58
|
+
file=None,
|
|
58
59
|
) -> None:
|
|
59
60
|
self._path = path
|
|
60
61
|
# None: the first keyframe names it (SD playback, H264 or H265).
|
|
@@ -67,10 +68,15 @@ class TsCopyRecorder:
|
|
|
67
68
|
self._thread: Optional[threading.Thread] = None
|
|
68
69
|
self._state: dict = {}
|
|
69
70
|
self._stopped = False
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
# Called first thing in stop(), for whoever still holds packets for
|
|
72
|
+
# this recording (the SD playback tap's reorder buffer).
|
|
73
|
+
self.on_stop: "list" = []
|
|
74
|
+
# Opened before the session connects, so an unwritable path fails
|
|
75
|
+
# while there is nothing to clean up: by the caller in an executor
|
|
76
|
+
# (``file``; an open() on an event loop blocks it), or here. Held open
|
|
77
|
+
# until the mux thread is done with it - no single block encloses
|
|
78
|
+
# that, so no context manager.
|
|
79
|
+
self._file = file if file is not None else open(self._path, "wb") # noqa: SIM115
|
|
74
80
|
|
|
75
81
|
async def start(self) -> None:
|
|
76
82
|
if self._thread is not None or self._stopped:
|
|
@@ -93,6 +99,11 @@ class TsCopyRecorder:
|
|
|
93
99
|
if self._stopped:
|
|
94
100
|
return
|
|
95
101
|
self._stopped = True
|
|
102
|
+
for _cb in self.on_stop:
|
|
103
|
+
try:
|
|
104
|
+
_cb()
|
|
105
|
+
except Exception:
|
|
106
|
+
_LOGGER.debug("recording stop callback failed", exc_info=True)
|
|
96
107
|
if self._thread is None:
|
|
97
108
|
self._close()
|
|
98
109
|
return
|
|
@@ -597,37 +597,117 @@ def answer_h265_parameter_sets(sdp: str, pt: Optional[int]) -> "list[bytes]":
|
|
|
597
597
|
return out
|
|
598
598
|
|
|
599
599
|
|
|
600
|
+
class SeqReorder:
|
|
601
|
+
"""Puts one RTP stream back in sequence order, waiting a little for gaps.
|
|
602
|
+
|
|
603
|
+
A packet the camera resends after a NACK arrives after the ones that
|
|
604
|
+
followed it; the access-unit assembler takes packets in order, so they
|
|
605
|
+
are held here until the gap fills or ``max_wait_s`` passes, then handed
|
|
606
|
+
on in order (a gap given up on reaches the assembler as a gap, and its
|
|
607
|
+
unit is handed on damaged). ``push`` returns what is ready; ``flush``
|
|
608
|
+
returns the rest at the end.
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
#: Most packets held; past this the oldest gap is given up at once.
|
|
612
|
+
MAX_HELD = 1500
|
|
613
|
+
|
|
614
|
+
def __init__(self, max_wait_s: float = 0.3) -> None:
|
|
615
|
+
self.max_wait_s = max_wait_s
|
|
616
|
+
self._next: "Optional[int]" = None
|
|
617
|
+
self._held: "dict[int, tuple[float, Any]]" = {}
|
|
618
|
+
self.late = 0 # arrived after its gap was given up
|
|
619
|
+
|
|
620
|
+
def push(self, seq: int, item: Any, now: float) -> "list":
|
|
621
|
+
if self._next is None:
|
|
622
|
+
self._next = seq
|
|
623
|
+
if (seq - self._next) & 0x8000: # before the next expected one
|
|
624
|
+
self.late += 1
|
|
625
|
+
return []
|
|
626
|
+
self._held.setdefault(seq, (now, item))
|
|
627
|
+
return self._release(now)
|
|
628
|
+
|
|
629
|
+
def _release(self, now: float) -> "list":
|
|
630
|
+
out = []
|
|
631
|
+
while True:
|
|
632
|
+
while self._next in self._held:
|
|
633
|
+
out.append(self._held.pop(self._next)[1])
|
|
634
|
+
self._next = (self._next + 1) & 0xFFFF
|
|
635
|
+
if not self._held:
|
|
636
|
+
return out
|
|
637
|
+
oldest = min(t for t, _ in self._held.values())
|
|
638
|
+
if now - oldest < self.max_wait_s and len(self._held) < self.MAX_HELD:
|
|
639
|
+
return out
|
|
640
|
+
# Give up on the gap: carry on from the earliest packet held.
|
|
641
|
+
self._next = min(self._held, key=lambda s: (s - self._next) & 0xFFFF)
|
|
642
|
+
|
|
643
|
+
def flush(self) -> "list":
|
|
644
|
+
out = []
|
|
645
|
+
while self._held:
|
|
646
|
+
if self._next not in self._held:
|
|
647
|
+
self._next = min(self._held, key=lambda s: (s - self._next) & 0xFFFF)
|
|
648
|
+
out.append(self._held.pop(self._next)[1])
|
|
649
|
+
self._next = (self._next + 1) & 0xFFFF
|
|
650
|
+
return out
|
|
651
|
+
|
|
652
|
+
|
|
600
653
|
class H265AccessUnits:
|
|
601
654
|
"""RTP payloads (RFC 7798) in, Annex B access units out.
|
|
602
655
|
|
|
603
656
|
``feed`` takes one packet's sequence number, RTP timestamp, marker bit and
|
|
604
657
|
payload, and returns the access units it completed as
|
|
605
658
|
``(bytes, rtp_timestamp, is_keyframe)`` - the shape the DTLS recorder's
|
|
606
|
-
video queue takes. A unit with a sequence gap in it is
|
|
607
|
-
|
|
608
|
-
|
|
659
|
+
video queue takes. A unit with a sequence gap in it is handed on with
|
|
660
|
+
what arrived (``damaged`` counts them): every later frame up to the next
|
|
661
|
+
keyframe refers to it, so dropping it made all of those undecodable - an
|
|
662
|
+
A000088 playback lost 122 frames to one lost packet (2026-10-29), where
|
|
663
|
+
the decoder conceals a damaged frame and carries on. ``keep_damaged=False``
|
|
664
|
+
drops them instead (``dropped``). ``parameter_sets`` (from the answer's
|
|
665
|
+
fmtp) go in front of a keyframe that does not carry its own VPS.
|
|
609
666
|
"""
|
|
610
667
|
|
|
611
|
-
def __init__(
|
|
668
|
+
def __init__(
|
|
669
|
+
self,
|
|
670
|
+
parameter_sets: "Optional[list[bytes]]" = None,
|
|
671
|
+
*,
|
|
672
|
+
keep_damaged: bool = True,
|
|
673
|
+
) -> None:
|
|
612
674
|
self.parameter_sets = list(parameter_sets or [])
|
|
675
|
+
self.keep_damaged = keep_damaged
|
|
613
676
|
self._ts: "Optional[int]" = None
|
|
614
677
|
self._nals: "list[bytes]" = []
|
|
615
678
|
self._fu: "Optional[bytearray]" = None
|
|
616
679
|
self._last_seq: "Optional[int]" = None
|
|
617
680
|
self._broken = False
|
|
618
681
|
self.dropped = 0
|
|
682
|
+
self.damaged = 0
|
|
683
|
+
# The camera sends a recording's last frame twice, under a new
|
|
684
|
+
# timestamp (A000088, 2026-10-30): a decoder then sees the same picture
|
|
685
|
+
# order count twice. A unit identical to the one before is not handed
|
|
686
|
+
# on (``repeats`` counts them).
|
|
687
|
+
self.repeats = 0
|
|
688
|
+
self._last_unit: "Optional[bytes]" = None
|
|
619
689
|
|
|
620
690
|
def _emit(self) -> "list[tuple[bytes, int, bool]]":
|
|
621
691
|
out = []
|
|
622
692
|
if self._fu is not None:
|
|
623
693
|
self._broken = True # a fragment never finished
|
|
624
|
-
|
|
694
|
+
if self.keep_damaged and len(self._fu) > 2:
|
|
695
|
+
self._nals.append(bytes(self._fu)) # what arrived of it
|
|
696
|
+
keep = not self._broken or self.keep_damaged
|
|
697
|
+
if self._nals and keep and self._ts is not None:
|
|
625
698
|
types = [(n[0] >> 1) & 0x3F for n in self._nals if n]
|
|
626
699
|
kf = any(16 <= t <= 21 for t in types)
|
|
627
700
|
nals = self._nals
|
|
628
701
|
if kf and 32 not in types and self.parameter_sets:
|
|
629
702
|
nals = self.parameter_sets + nals
|
|
630
|
-
|
|
703
|
+
unit = b"".join(_NAL_SC + n for n in nals)
|
|
704
|
+
if unit == self._last_unit:
|
|
705
|
+
self.repeats += 1
|
|
706
|
+
else:
|
|
707
|
+
out.append((unit, self._ts, kf))
|
|
708
|
+
self._last_unit = unit
|
|
709
|
+
if self._broken:
|
|
710
|
+
self.damaged += 1
|
|
631
711
|
elif self._nals or self._broken:
|
|
632
712
|
self.dropped += 1
|
|
633
713
|
self._nals, self._fu, self._broken = [], None, False
|
|
@@ -665,7 +745,7 @@ class H265AccessUnits:
|
|
|
665
745
|
while i + 2 <= len(payload):
|
|
666
746
|
n = struct.unpack_from("!H", payload, i)[0]
|
|
667
747
|
if n == 0 or i + 2 + n > len(payload):
|
|
668
|
-
self._broken = True # truncated or corrupt
|
|
748
|
+
self._broken = True # truncated or corrupt: the rest is lost
|
|
669
749
|
break
|
|
670
750
|
self._nals.append(bytes(payload[i + 2 : i + 2 + n]))
|
|
671
751
|
i += 2 + n
|
|
@@ -295,7 +295,8 @@ def _install_sd_h265_tap(pc, h265, status) -> int:
|
|
|
295
295
|
PCMU by number, but its answer's audio was PCMA alone, so nothing else
|
|
296
296
|
claims it. Returns the routers tapped.
|
|
297
297
|
"""
|
|
298
|
-
from .
|
|
298
|
+
from .._vendor.aiortc.rtcrtpreceiver import NackGenerator
|
|
299
|
+
from .sd_playback import SeqReorder, starts_h265_stream
|
|
299
300
|
|
|
300
301
|
if h265 is None:
|
|
301
302
|
return 0
|
|
@@ -308,19 +309,24 @@ def _install_sd_h265_tap(pc, h265, status) -> int:
|
|
|
308
309
|
routers.append(router)
|
|
309
310
|
|
|
310
311
|
def _make(router):
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
312
|
+
# A video receiver on this transport sends the NACKs: aiortc only
|
|
313
|
+
# asks for resends on streams it routes, and this one it does not.
|
|
314
|
+
rx = None
|
|
315
|
+
for t in pc.getTransceivers():
|
|
316
|
+
r = t.receiver
|
|
317
|
+
if getattr(getattr(r, "transport", None), "_rtp_router", None) is router:
|
|
318
|
+
if rx is None or getattr(t, "kind", None) == "video":
|
|
319
|
+
rx = r
|
|
320
|
+
reorder = SeqReorder()
|
|
321
|
+
nack_gen = NackGenerator()
|
|
322
|
+
# The loop holds tasks weakly: a NACK not referenced here can be
|
|
323
|
+
# collected before it is sent.
|
|
324
|
+
nack_tasks: "set" = set()
|
|
325
|
+
|
|
326
|
+
def _feed(packet) -> None:
|
|
322
327
|
units = h265["units"]
|
|
323
328
|
lost_before = units.dropped
|
|
329
|
+
damaged_before = units.damaged
|
|
324
330
|
for data, ts, kf in units.feed(
|
|
325
331
|
packet.sequence_number,
|
|
326
332
|
packet.timestamp,
|
|
@@ -335,6 +341,48 @@ def _install_sd_h265_tap(pc, h265, status) -> int:
|
|
|
335
341
|
status("SD playback: the recorder fell behind - H265 dropped")
|
|
336
342
|
if units.dropped and not lost_before:
|
|
337
343
|
status("SD playback: an H265 frame with a lost packet was dropped")
|
|
344
|
+
if units.damaged and not damaged_before:
|
|
345
|
+
status(
|
|
346
|
+
"SD playback: an H265 frame with a lost packet was passed on"
|
|
347
|
+
" damaged (the decoder conceals it)"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def _flush() -> None:
|
|
351
|
+
for held in reorder.flush():
|
|
352
|
+
_feed(held)
|
|
353
|
+
|
|
354
|
+
on_stop = getattr(h265["recorder"], "on_stop", None)
|
|
355
|
+
if on_stop is not None:
|
|
356
|
+
on_stop.append(_flush)
|
|
357
|
+
|
|
358
|
+
def _tap(packet) -> None:
|
|
359
|
+
pt = h265["pt"]
|
|
360
|
+
if pt is None:
|
|
361
|
+
if packet.payload_type in router.payload_type_table or not (
|
|
362
|
+
starts_h265_stream(packet.payload)
|
|
363
|
+
):
|
|
364
|
+
return
|
|
365
|
+
h265["pt"] = pt = packet.payload_type
|
|
366
|
+
status(f"SD playback: taking pt={pt} as the recording's H265")
|
|
367
|
+
if packet.payload_type != pt:
|
|
368
|
+
return
|
|
369
|
+
# Ask for what never arrived; the resend is put back in place
|
|
370
|
+
# by the reorder buffer before the assembler sees it.
|
|
371
|
+
if nack_gen.add(packet) and rx is not None and nack_gen.missing:
|
|
372
|
+
h265["nacks"] = h265.get("nacks", 0) + 1
|
|
373
|
+
if h265["nacks"] == 1:
|
|
374
|
+
status("SD playback: asking the camera to resend lost H265 (NACK)")
|
|
375
|
+
try:
|
|
376
|
+
task = asyncio.get_running_loop().create_task(
|
|
377
|
+
rx._send_rtcp_nack(packet.ssrc, sorted(nack_gen.missing))
|
|
378
|
+
)
|
|
379
|
+
except RuntimeError:
|
|
380
|
+
pass # no loop: nothing to send on
|
|
381
|
+
else:
|
|
382
|
+
nack_tasks.add(task)
|
|
383
|
+
task.add_done_callback(nack_tasks.discard)
|
|
384
|
+
for ready in reorder.push(packet.sequence_number, packet, time.monotonic()):
|
|
385
|
+
_feed(ready)
|
|
338
386
|
|
|
339
387
|
return _tap
|
|
340
388
|
|
|
@@ -3155,14 +3203,24 @@ async def _dtls_open_peer_connection_setup(self, ps: _DtlsOpenState) -> None:
|
|
|
3155
3203
|
track,
|
|
3156
3204
|
)
|
|
3157
3205
|
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3206
|
+
# Opened off the event loop: a caller such as Home Assistant flags (and
|
|
3207
|
+
# may be stalled by) a blocking open() on it. The recorder takes the open
|
|
3208
|
+
# file, so an unwritable path still fails before the session connects.
|
|
3209
|
+
_rec_file = await _open_ts_output(ps.output_path)
|
|
3210
|
+
try:
|
|
3211
|
+
ps.recorder = self._attach_file_recorder(
|
|
3212
|
+
ps.pc,
|
|
3213
|
+
ps.output_path,
|
|
3214
|
+
ps.on_frame,
|
|
3215
|
+
# SD playback: H264 or H265, whichever the recording is; the first
|
|
3216
|
+
# keyframe names it.
|
|
3217
|
+
video_codec=None if ps._sd_playback is not None else "h264",
|
|
3218
|
+
file=_rec_file,
|
|
3219
|
+
)
|
|
3220
|
+
finally:
|
|
3221
|
+
# A recorder that took the file closes it; nobody else will.
|
|
3222
|
+
if _rec_file is not None and getattr(ps, "recorder", None) is None:
|
|
3223
|
+
_rec_file.close()
|
|
3166
3224
|
if ps._sd_playback is not None and ps.recorder is not None:
|
|
3167
3225
|
from .sd_playback import H265AccessUnits
|
|
3168
3226
|
|
|
@@ -4693,8 +4751,26 @@ async def _dtls_open_ice_candidates_and_connect(
|
|
|
4693
4751
|
return None
|
|
4694
4752
|
|
|
4695
4753
|
|
|
4754
|
+
async def _open_ts_output(path):
|
|
4755
|
+
"""``path`` opened for the TS recorder in an executor, or None: not a .ts
|
|
4756
|
+
output (the recorder is not ours), or the open failed - the recorder then
|
|
4757
|
+
tries itself and reports the error as it always has."""
|
|
4758
|
+
from .recording import is_ts_path
|
|
4759
|
+
|
|
4760
|
+
if not path or not is_ts_path(path):
|
|
4761
|
+
return None
|
|
4762
|
+
try:
|
|
4763
|
+
return await asyncio.get_running_loop().run_in_executor(
|
|
4764
|
+
None, functools.partial(open, path, "wb")
|
|
4765
|
+
)
|
|
4766
|
+
except OSError:
|
|
4767
|
+
return None
|
|
4768
|
+
|
|
4769
|
+
|
|
4696
4770
|
class _WebRTCOpenMixin:
|
|
4697
|
-
def _attach_file_recorder(
|
|
4771
|
+
def _attach_file_recorder(
|
|
4772
|
+
self, pc, output_path, on_frame, video_codec="h264", file=None
|
|
4773
|
+
):
|
|
4698
4774
|
"""The recorder for a DTLS ``output_path``, wired to ``pc``'s tracks.
|
|
4699
4775
|
|
|
4700
4776
|
MPEG-TS paths copy the camera's own streams (``TsCopyRecorder``): the
|
|
@@ -4716,6 +4792,7 @@ class _WebRTCOpenMixin:
|
|
|
4716
4792
|
output_path,
|
|
4717
4793
|
device_id=getattr(self, "device_id", None),
|
|
4718
4794
|
video_codec=video_codec,
|
|
4795
|
+
file=file,
|
|
4719
4796
|
)
|
|
4720
4797
|
except OSError as exc:
|
|
4721
4798
|
_LOGGER.warning(
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0rc67"
|
|
8
8
|
description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc67
|
|
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-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_dtls_recorder_wiring.py
RENAMED
|
@@ -122,3 +122,37 @@ def test_tapping_a_receiver_twice_for_different_queues_is_flagged(caplog):
|
|
|
122
122
|
with caplog.at_level("WARNING"):
|
|
123
123
|
CameraMixin._install_encoded_tap(rcv, second, True, serve=True)
|
|
124
124
|
assert any("already tapped" in r.getMessage() for r in caplog.records)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_the_recording_file_is_opened_off_the_event_loop(tmp_path, monkeypatch):
|
|
128
|
+
# Home Assistant flagged "Detected blocking call to open ... .part.ts"
|
|
129
|
+
# on every DTLS card-recording fetch (2026-10-29): the recorder opened its
|
|
130
|
+
# file in its constructor, on the loop. The open phase now opens it in an
|
|
131
|
+
# executor and hands it over.
|
|
132
|
+
import asyncio
|
|
133
|
+
import threading
|
|
134
|
+
|
|
135
|
+
from aidot_cameras.camera import webrtc_open
|
|
136
|
+
from aidot_cameras.camera.recording import TsCopyRecorder
|
|
137
|
+
|
|
138
|
+
loop_thread, opened_on = [], []
|
|
139
|
+
real_open = open
|
|
140
|
+
|
|
141
|
+
def _spy(*a, **k):
|
|
142
|
+
opened_on.append(threading.current_thread())
|
|
143
|
+
return real_open(*a, **k)
|
|
144
|
+
|
|
145
|
+
async def main():
|
|
146
|
+
loop_thread.append(threading.current_thread())
|
|
147
|
+
monkeypatch.setattr("builtins.open", _spy)
|
|
148
|
+
fh = await webrtc_open._open_ts_output(str(tmp_path / "clip.part.ts"))
|
|
149
|
+
monkeypatch.setattr("builtins.open", real_open)
|
|
150
|
+
assert await webrtc_open._open_ts_output(str(tmp_path / "clip.mp4")) is None
|
|
151
|
+
assert await webrtc_open._open_ts_output(None) is None
|
|
152
|
+
return fh
|
|
153
|
+
|
|
154
|
+
fh = asyncio.run(main())
|
|
155
|
+
assert fh is not None and opened_on and opened_on[0] is not loop_thread[0]
|
|
156
|
+
rec = TsCopyRecorder(str(tmp_path / "clip.part.ts"), file=fh)
|
|
157
|
+
assert rec._file is fh
|
|
158
|
+
fh.close()
|
{python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/tests/test_sd_playback_dtls.py
RENAMED
|
@@ -164,9 +164,9 @@ def test_access_units_survive_rtp_and_back():
|
|
|
164
164
|
assert [ts for _, ts, _ in got] == [90000 + i * 6000 for i in range(len(aus))]
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
def
|
|
167
|
+
def test_in_drop_mode_a_unit_with_a_lost_packet_is_dropped():
|
|
168
168
|
aus = _hevc_access_units(4)
|
|
169
|
-
units = H265AccessUnits()
|
|
169
|
+
units = H265AccessUnits(keep_damaged=False)
|
|
170
170
|
got, seq = [], 7
|
|
171
171
|
for i, au in enumerate(aus):
|
|
172
172
|
pkts = _packetize(au, i * 6000, seq, mtu=200)
|
|
@@ -356,7 +356,7 @@ def test_a_recording_held_as_h264_is_muxed_as_h264():
|
|
|
356
356
|
|
|
357
357
|
def test_a_lost_last_packet_drops_its_own_unit_not_the_next():
|
|
358
358
|
aus = _hevc_access_units(5)
|
|
359
|
-
units = H265AccessUnits()
|
|
359
|
+
units = H265AccessUnits(keep_damaged=False)
|
|
360
360
|
got, seq = [], 50
|
|
361
361
|
for i, au in enumerate(aus):
|
|
362
362
|
pkts = _packetize(au, i * 6000, seq, mtu=200)
|
|
@@ -419,7 +419,7 @@ def test_the_sd_mode_echo_has_one_parser():
|
|
|
419
419
|
|
|
420
420
|
def test_a_lost_head_drops_the_new_unit_not_the_one_already_emitted():
|
|
421
421
|
aus = _hevc_access_units(5)
|
|
422
|
-
units = H265AccessUnits()
|
|
422
|
+
units = H265AccessUnits(keep_damaged=False)
|
|
423
423
|
got, seq = [], 300
|
|
424
424
|
for i, au in enumerate(aus):
|
|
425
425
|
pkts = _packetize(au, i * 6000, seq, mtu=200)
|
|
@@ -434,7 +434,7 @@ def test_a_lost_head_drops_the_new_unit_not_the_one_already_emitted():
|
|
|
434
434
|
|
|
435
435
|
def test_a_gap_wider_than_the_lost_marker_drops_both_units():
|
|
436
436
|
aus = _hevc_access_units(5)
|
|
437
|
-
units = H265AccessUnits()
|
|
437
|
+
units = H265AccessUnits(keep_damaged=False)
|
|
438
438
|
got, seq = [], 900
|
|
439
439
|
for i, au in enumerate(aus):
|
|
440
440
|
pkts = _packetize(au, i * 6000, seq, mtu=200)
|
|
@@ -448,9 +448,130 @@ def test_a_gap_wider_than_the_lost_marker_drops_both_units():
|
|
|
448
448
|
|
|
449
449
|
|
|
450
450
|
def test_a_truncated_aggregation_packet_breaks_its_unit():
|
|
451
|
-
units = H265AccessUnits()
|
|
451
|
+
units = H265AccessUnits(keep_damaged=False)
|
|
452
452
|
vps = bytes([32 << 1, 1, 0x0C])
|
|
453
453
|
ap = bytes([48 << 1, 1]) + struct.pack("!H", 3) + vps + struct.pack("!H", 1200)
|
|
454
454
|
ap += bytes([19 << 1, 1]) + b"\x00" * 298
|
|
455
455
|
assert units.feed(1, 0, True, ap) == []
|
|
456
456
|
assert units.dropped == 1
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_by_default_a_damaged_unit_is_handed_on_and_counted():
|
|
460
|
+
# One lost packet used to cost every frame up to the next keyframe
|
|
461
|
+
# (A000088, 2026-10-29: 122 undecodable frames); handed on, the decoder
|
|
462
|
+
# conceals the one damaged frame.
|
|
463
|
+
aus = _hevc_access_units(5)
|
|
464
|
+
units = H265AccessUnits()
|
|
465
|
+
got, seq = [], 600
|
|
466
|
+
for i, au in enumerate(aus):
|
|
467
|
+
pkts = _packetize(au, i * 6000, seq, mtu=200)
|
|
468
|
+
seq += len(pkts)
|
|
469
|
+
for j, p in enumerate(pkts):
|
|
470
|
+
if i == 0 and j == 3:
|
|
471
|
+
continue # one packet inside the first (keyframe) unit
|
|
472
|
+
got += units.feed(*p)
|
|
473
|
+
assert [ts for _, ts, _ in got] == [i * 6000 for i in range(len(aus))]
|
|
474
|
+
assert units.damaged == 1 and units.dropped == 0
|
|
475
|
+
assert got[0][2] is True # still marked a keyframe
|
|
476
|
+
assert got[1:] and [_nals(d) for d, _, _ in got[1:]] == [_nals(a) for a in aus[1:]]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_a_unit_sent_twice_is_handed_on_once():
|
|
480
|
+
# The A000088 sent a recording's last frame twice under a new timestamp
|
|
481
|
+
# (2026-10-30); the decoder then reported a duplicate picture order count.
|
|
482
|
+
aus = _hevc_access_units(3)
|
|
483
|
+
units = H265AccessUnits()
|
|
484
|
+
got, seq = [], 10
|
|
485
|
+
for i, au in enumerate([*aus, aus[-1]]):
|
|
486
|
+
pkts = _packetize(au, i * 6000, seq)
|
|
487
|
+
seq += len(pkts)
|
|
488
|
+
for p in pkts:
|
|
489
|
+
got += units.feed(*p)
|
|
490
|
+
assert [ts for _, ts, _ in got] == [0, 6000, 12000]
|
|
491
|
+
assert units.repeats == 1
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_a_resent_packet_is_put_back_in_place():
|
|
495
|
+
from aidot_cameras.camera.sd_playback import SeqReorder
|
|
496
|
+
|
|
497
|
+
r = SeqReorder(max_wait_s=0.3)
|
|
498
|
+
assert r.push(10, "a", 0.0) == ["a"]
|
|
499
|
+
assert r.push(12, "c", 0.01) == [] # 11 missing: hold 12
|
|
500
|
+
assert r.push(13, "d", 0.02) == []
|
|
501
|
+
assert r.push(11, "b", 0.1) == ["b", "c", "d"] # the resend fills it
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_a_gap_that_never_fills_is_given_up_in_time():
|
|
505
|
+
from aidot_cameras.camera.sd_playback import SeqReorder
|
|
506
|
+
|
|
507
|
+
r = SeqReorder(max_wait_s=0.3)
|
|
508
|
+
r.push(65534, "a", 0.0)
|
|
509
|
+
assert r.push(1, "d", 0.01) == [] # 65535 and 0 missing, across the wrap
|
|
510
|
+
assert r.push(2, "e", 0.35) == ["d", "e"] # past the wait: carry on
|
|
511
|
+
assert r.push(0, "late", 0.4) == [] and r.late == 1
|
|
512
|
+
r.push(5, "h", 0.5)
|
|
513
|
+
assert r.flush() == ["h"]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def test_the_tap_asks_for_a_lost_packet_and_puts_the_resend_in_place():
|
|
517
|
+
# The A000088 resends what a NACK names (2026-10-30: three plays, no
|
|
518
|
+
# frame damaged or dropped, 0 decode errors, against 147 error lines
|
|
519
|
+
# from one dropped frame before). aiortc does not NACK this stream, as it
|
|
520
|
+
# does not route it; the tap asks, and holds packets until the resend.
|
|
521
|
+
import asyncio
|
|
522
|
+
|
|
523
|
+
from aidot_cameras.camera.webrtc_open import _install_sd_h265_tap
|
|
524
|
+
|
|
525
|
+
sent = []
|
|
526
|
+
|
|
527
|
+
class _Rx:
|
|
528
|
+
def __init__(self, router):
|
|
529
|
+
self.transport = type("T", (), {"_rtp_router": router})()
|
|
530
|
+
|
|
531
|
+
async def _send_rtcp_nack(self, ssrc, lost):
|
|
532
|
+
sent.append((ssrc, list(lost)))
|
|
533
|
+
|
|
534
|
+
class _PcNack:
|
|
535
|
+
def __init__(self, router):
|
|
536
|
+
self._t = [type("Tr", (), {"receiver": _Rx(router), "kind": "video"})()]
|
|
537
|
+
|
|
538
|
+
def getTransceivers(self):
|
|
539
|
+
return self._t
|
|
540
|
+
|
|
541
|
+
def _p(seq, ts, marker, payload):
|
|
542
|
+
p = _pkt(98, seq, ts, marker, payload)
|
|
543
|
+
p.ssrc = 0x1234
|
|
544
|
+
return p
|
|
545
|
+
|
|
546
|
+
rec = type("Rec", (), {"vq": queue.Queue(), "on_stop": []})()
|
|
547
|
+
state = {"pt": 98, "units": H265AccessUnits(), "recorder": rec, "dropped": 0}
|
|
548
|
+
router = _Router()
|
|
549
|
+
|
|
550
|
+
# One IDR in three fragments, and the next unit.
|
|
551
|
+
idr = bytes([19 << 1, 1]) + bytes(range(60))
|
|
552
|
+
fu_ind = bytes([(idr[0] & 0x81) | (49 << 1), idr[1]])
|
|
553
|
+
body = idr[2:]
|
|
554
|
+
frags = [
|
|
555
|
+
fu_ind + bytes([19 | 0x80]) + body[:20],
|
|
556
|
+
fu_ind + bytes([19]) + body[20:40],
|
|
557
|
+
fu_ind + bytes([19 | 0x40]) + body[40:],
|
|
558
|
+
]
|
|
559
|
+
trail = bytes([1 << 1, 1, 0xBB])
|
|
560
|
+
|
|
561
|
+
async def main():
|
|
562
|
+
assert _install_sd_h265_tap(_PcNack(router), state, lambda m: None) == 1
|
|
563
|
+
router.raw_tap(_p(100, 3000, False, frags[0]))
|
|
564
|
+
router.raw_tap(_p(102, 3000, True, frags[2])) # 101 lost
|
|
565
|
+
router.raw_tap(_p(103, 9000, True, trail))
|
|
566
|
+
await asyncio.sleep(0)
|
|
567
|
+
assert sent == [(0x1234, [101])]
|
|
568
|
+
assert rec.vq.empty() # held for the resend
|
|
569
|
+
router.raw_tap(_p(101, 3000, False, frags[1])) # the resend
|
|
570
|
+
await asyncio.sleep(0)
|
|
571
|
+
|
|
572
|
+
asyncio.run(main())
|
|
573
|
+
first = rec.vq.get_nowait()
|
|
574
|
+
assert first == (b"\x00\x00\x00\x01" + idr, 3000, True, "hevc")
|
|
575
|
+
assert state["units"].damaged == 0 and state["units"].dropped == 0
|
|
576
|
+
assert rec.vq.get_nowait()[1] == 9000
|
|
577
|
+
assert rec.on_stop # the tap flushes what it holds when the recording stops
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_log_redaction.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_upstream.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc65 → python_aidot_cameras-1.0.0rc67}/aidot_cameras/_vendor/__init__.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
|
|
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
|