python-aidot-cameras 1.0.0rc62__tar.gz → 1.0.0rc63__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.0rc62/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc63}/PKG-INFO +26 -10
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/README.md +25 -9
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/__init__.py +2 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +12 -1
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/client.py +51 -13
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/protocol.py +94 -15
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/recording.py +13 -2
- python_aidot_cameras-1.0.0rc63/aidot_cameras/camera/sd_playback.py +574 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/sdes.py +13 -3
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/sdes_open.py +307 -33
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/webrtc.py +13 -2
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/webrtc_open.py +217 -12
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/exceptions.py +8 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63/python_aidot_cameras.egg-info}/PKG-INFO +26 -10
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/python_aidot_cameras.egg-info/SOURCES.txt +8 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_avio_ctrl_uses_the_session_dseq.py +41 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_retry_short_sessions.py +33 -4
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_open_errors_are_aidot_errors.py +2 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_peer_id_client_type.py +26 -0
- python_aidot_cameras-1.0.0rc63/tests/test_sd_playback.py +98 -0
- python_aidot_cameras-1.0.0rc63/tests/test_sd_playback_controls.py +165 -0
- python_aidot_cameras-1.0.0rc63/tests/test_sd_playback_dtls.py +417 -0
- python_aidot_cameras-1.0.0rc63/tests/test_sd_playback_entry.py +112 -0
- python_aidot_cameras-1.0.0rc63/tests/test_sd_playback_media_hold.py +140 -0
- python_aidot_cameras-1.0.0rc63/tests/test_sd_playback_open.py +306 -0
- python_aidot_cameras-1.0.0rc63/tests/test_session_mode_seq.py +82 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_log_redaction.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/agc.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/health.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/hls_ts.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/settings.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_arrival_timestamps.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_auth_error_routing.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_auto_alarm.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_auto_light_settings.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_avio_header_single_layout.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_battery_cap_spans_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_battery_wake_settles_before_the_command.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ble_gateway_is_gone.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_cloud_error_log_names_the_exception.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_cloud_error_severity.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_declared_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_detection_types.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_direct_ts_serve_only_for_http.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_recorder_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_refusal_after_the_echo.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_tap_states_no_reorder.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_ts_copy_recording.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_every_exception_is_exported.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_first_media_abandon_rules.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_health_line.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_health_line_counts_pictures.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_hls_ts_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_hls_ts_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ice_credentials_match_the_app.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ipc_envelope_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_login_accessors.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_retry_stops_on_close.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_lan_sweep_only_with_lights.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_live_validate_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_log_hygiene.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_loop_break_ends_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_mypy_new_findings.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_no_unbounded_hex_dumps.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_phase_returns_reach_the_caller.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_phase_state_is_complete.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_plaintext_mqtt_refused.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_region_allowlist.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_review_lan_retry_and_graces.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_rtsp_publish_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_open_yields_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_pinned_codec_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_loop_releases_on_error.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_session_stop_twice.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_settings_registry.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_skip_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_status_and_trace_channels.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_stun_attr_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_swallowed_exception_names.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_transport_failure_reporting.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_turn_target_shared.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_upstream_seam_adapter.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_upstream_secrets_redacted.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_vendored_aiortc_integrity.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_video_presence_needs_a_keyframe.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/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.0rc63
|
|
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
|
|
@@ -129,15 +129,15 @@ the authoritative table and per-model notes.
|
|
|
129
129
|
Two behaviours that are measured, understood as far as the evidence allows, and
|
|
130
130
|
not going to change in this release. Neither is a fault to report.
|
|
131
131
|
|
|
132
|
-
**Recorded video
|
|
133
|
-
records to the cloud or to its
|
|
134
|
-
device reports it as `IsSupportPlayback`. This library lists
|
|
135
|
-
(`async_get_cloud_recordings`) and resolves a playable HLS URL
|
|
136
|
-
(`async_get_event_video_media`); for a camera storing to a card it lists
|
|
137
|
-
on-device recordings (`async_get_sd_recordings`)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
**Recorded video comes from the cloud or from the camera's own SD card.**
|
|
133
|
+
Whether a camera records to the cloud or to its card is per-camera, not
|
|
134
|
+
per-model, and the device reports it as `IsSupportPlayback`. This library lists
|
|
135
|
+
cloud recordings (`async_get_cloud_recordings`) and resolves a playable HLS URL
|
|
136
|
+
for one (`async_get_event_video_media`); for a camera storing to a card it lists
|
|
137
|
+
the on-device recordings (`async_get_sd_recordings`) and plays one
|
|
138
|
+
(`async_open_sd_playback`), on all three models: an SDES camera (A001064,
|
|
139
|
+
A001513) serves it like a live stream, a DTLS camera (A000088) records it to a
|
|
140
|
+
`.ts` file. See `docs/CAMERAS.md`, "On-device (SD card) recordings".
|
|
141
141
|
|
|
142
142
|
**The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
|
|
143
143
|
app.** Measured from a capture of the app on the same camera: it takes about
|
|
@@ -514,6 +514,22 @@ battery camera asleep, or a model without the feature -- returns `None`, and
|
|
|
514
514
|
treating that as a set of disabled detectors would report a state the camera
|
|
515
515
|
never claimed. A write refuses outright when there is no current list to echo.
|
|
516
516
|
|
|
517
|
+
### Playing a recording off the SD card
|
|
518
|
+
|
|
519
|
+
```python
|
|
520
|
+
listing = await device_client.async_get_sd_recordings(session=live_session)
|
|
521
|
+
await device_client.async_stop_streaming() # the camera has one media slot
|
|
522
|
+
session = await device_client.async_open_sd_playback(
|
|
523
|
+
listing.records[-1], output_path="/tmp/clip.ts"
|
|
524
|
+
)
|
|
525
|
+
await session.async_sd_seek(20) # also async_sd_pause() / async_sd_resume()
|
|
526
|
+
# ... session.stop() when done
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
The camera sends the clip in real time, from a few seconds before the
|
|
530
|
+
record's listed time. On a DTLS camera (A000088) it can only be recorded to a
|
|
531
|
+
`.ts` file. See `docs/CAMERAS.md`, "Playing a recording".
|
|
532
|
+
|
|
517
533
|
### WiFi and SD card
|
|
518
534
|
|
|
519
535
|
```python
|
|
@@ -79,15 +79,15 @@ the authoritative table and per-model notes.
|
|
|
79
79
|
Two behaviours that are measured, understood as far as the evidence allows, and
|
|
80
80
|
not going to change in this release. Neither is a fault to report.
|
|
81
81
|
|
|
82
|
-
**Recorded video
|
|
83
|
-
records to the cloud or to its
|
|
84
|
-
device reports it as `IsSupportPlayback`. This library lists
|
|
85
|
-
(`async_get_cloud_recordings`) and resolves a playable HLS URL
|
|
86
|
-
(`async_get_event_video_media`); for a camera storing to a card it lists
|
|
87
|
-
on-device recordings (`async_get_sd_recordings`)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
**Recorded video comes from the cloud or from the camera's own SD card.**
|
|
83
|
+
Whether a camera records to the cloud or to its card is per-camera, not
|
|
84
|
+
per-model, and the device reports it as `IsSupportPlayback`. This library lists
|
|
85
|
+
cloud recordings (`async_get_cloud_recordings`) and resolves a playable HLS URL
|
|
86
|
+
for one (`async_get_event_video_media`); for a camera storing to a card it lists
|
|
87
|
+
the on-device recordings (`async_get_sd_recordings`) and plays one
|
|
88
|
+
(`async_open_sd_playback`), on all three models: an SDES camera (A001064,
|
|
89
|
+
A001513) serves it like a live stream, a DTLS camera (A000088) records it to a
|
|
90
|
+
`.ts` file. See `docs/CAMERAS.md`, "On-device (SD card) recordings".
|
|
91
91
|
|
|
92
92
|
**The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
|
|
93
93
|
app.** Measured from a capture of the app on the same camera: it takes about
|
|
@@ -464,6 +464,22 @@ battery camera asleep, or a model without the feature -- returns `None`, and
|
|
|
464
464
|
treating that as a set of disabled detectors would report a state the camera
|
|
465
465
|
never claimed. A write refuses outright when there is no current list to echo.
|
|
466
466
|
|
|
467
|
+
### Playing a recording off the SD card
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
listing = await device_client.async_get_sd_recordings(session=live_session)
|
|
471
|
+
await device_client.async_stop_streaming() # the camera has one media slot
|
|
472
|
+
session = await device_client.async_open_sd_playback(
|
|
473
|
+
listing.records[-1], output_path="/tmp/clip.ts"
|
|
474
|
+
)
|
|
475
|
+
await session.async_sd_seek(20) # also async_sd_pause() / async_sd_resume()
|
|
476
|
+
# ... session.stop() when done
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The camera sends the clip in real time, from a few seconds before the
|
|
480
|
+
record's listed time. On a DTLS camera (A000088) it can only be recorded to a
|
|
481
|
+
`.ts` file. See `docs/CAMERAS.md`, "Playing a recording".
|
|
482
|
+
|
|
467
483
|
### WiFi and SD card
|
|
468
484
|
|
|
469
485
|
```python
|
|
@@ -37,6 +37,7 @@ from .exceptions import (
|
|
|
37
37
|
AidotNoWebrtcAnswer,
|
|
38
38
|
AidotOSError,
|
|
39
39
|
AidotPlaintextMqttRefused,
|
|
40
|
+
AidotSdPlaybackUnavailable,
|
|
40
41
|
AidotStreamOpenError,
|
|
41
42
|
AidotUnknownRegion,
|
|
42
43
|
AidotUserOrPassIncorrect,
|
|
@@ -177,6 +178,7 @@ __all__ = [
|
|
|
177
178
|
"AidotNotLogin",
|
|
178
179
|
"AidotOSError",
|
|
179
180
|
"AidotPlaintextMqttRefused",
|
|
181
|
+
"AidotSdPlaybackUnavailable",
|
|
180
182
|
"AidotStreamOpenError",
|
|
181
183
|
"AidotUnknownRegion",
|
|
182
184
|
"AidotUserOrPassIncorrect",
|
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
import traceback
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Optional, Protocol, Type, TypeVar, Union
|
|
9
|
+
from typing import Callable, Optional, Protocol, Type, TypeVar, Union # aidot: Callable
|
|
10
10
|
|
|
11
11
|
import pylibsrtp
|
|
12
12
|
from cryptography import x509
|
|
@@ -253,6 +253,9 @@ class RtpRouter:
|
|
|
253
253
|
self.mid_table: dict[str, RtpReceiver] = {}
|
|
254
254
|
self.ssrc_table: dict[int, RtpReceiver] = {}
|
|
255
255
|
self.payload_type_table: dict[int, set[RtpReceiver]] = {}
|
|
256
|
+
# aidot: optional callable shown every RTP packet before routing,
|
|
257
|
+
# routed or not - SD playback takes H265 this way (see VENDOR.md).
|
|
258
|
+
self.raw_tap: Optional[Callable[[RtpPacket], None]] = None
|
|
256
259
|
|
|
257
260
|
def register_receiver(
|
|
258
261
|
self,
|
|
@@ -616,6 +619,14 @@ class RTCDtlsTransport(AsyncIOEventEmitter):
|
|
|
616
619
|
self.__log_debug("x RTP parsing failed: %s", exc)
|
|
617
620
|
return
|
|
618
621
|
|
|
622
|
+
# aidot: the raw tap sees the packet whether or not it routes.
|
|
623
|
+
tap = self._rtp_router.raw_tap
|
|
624
|
+
if tap is not None:
|
|
625
|
+
try:
|
|
626
|
+
tap(packet)
|
|
627
|
+
except Exception as exc:
|
|
628
|
+
self.__log_debug("x RTP raw tap failed: %s", exc)
|
|
629
|
+
|
|
619
630
|
# route RTP packet
|
|
620
631
|
receiver = self._rtp_router.route_rtp(packet)
|
|
621
632
|
if receiver is not None:
|
{python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/client.py
RENAMED
|
@@ -6,6 +6,7 @@ import logging
|
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
import os
|
|
8
8
|
import time
|
|
9
|
+
import threading
|
|
9
10
|
import asyncio
|
|
10
11
|
import dataclasses
|
|
11
12
|
from typing import TYPE_CHECKING, Any, Callable, List, NamedTuple, Optional, Union
|
|
@@ -61,6 +62,7 @@ from .sdes import (
|
|
|
61
62
|
) # re-exported (split into sdes.py); also the SDES return type
|
|
62
63
|
from .controls import _CameraControlsMixin
|
|
63
64
|
from .sd_listing import _CameraSdMixin
|
|
65
|
+
from .sd_playback import _CameraSdPlaybackMixin
|
|
64
66
|
from .webrtc_open import _WebRTCOpenMixin
|
|
65
67
|
from .sdes_open import _SdesOpenMixin
|
|
66
68
|
from .protocol import ( # noqa: F401 - used here and/or by the webrtc_open mixin; kept re-exported for back-compat
|
|
@@ -120,6 +122,9 @@ from .health import HealthReporter, dtls_snapshot as _dtls_snapshot
|
|
|
120
122
|
|
|
121
123
|
_LOGGER = logging.getLogger(__name__)
|
|
122
124
|
|
|
125
|
+
#: Guards every client's ``_live_dseq``; see ``_next_dseq``.
|
|
126
|
+
_DSEQ_LOCK = threading.Lock()
|
|
127
|
+
|
|
123
128
|
|
|
124
129
|
def _is_transient_cloud_error(exc: BaseException) -> bool:
|
|
125
130
|
"""True for the network failures a retry is expected to clear.
|
|
@@ -860,6 +865,7 @@ def _build_sdes_serve_cmd(
|
|
|
860
865
|
push_video_only: bool = False,
|
|
861
866
|
video_decoder: Optional[list] = None,
|
|
862
867
|
input_timeout_s: Optional[int] = None,
|
|
868
|
+
keep_probe_packets: bool = False,
|
|
863
869
|
) -> list:
|
|
864
870
|
"""Build the ffmpeg argv for the SDES bridge serve.
|
|
865
871
|
|
|
@@ -1040,8 +1046,15 @@ def _build_sdes_serve_cmd(
|
|
|
1040
1046
|
if _SERVE_ARRIVAL_TS and not max_seconds
|
|
1041
1047
|
else []
|
|
1042
1048
|
),
|
|
1049
|
+
# ``nobuffer`` drops what ffmpeg reads while probing the input (up to
|
|
1050
|
+
# -analyzeduration / -probesize) - latency a live view does not want.
|
|
1051
|
+
# A recording's opening seconds are not latency, they are the clip:
|
|
1052
|
+
# at 2560x1440 H265 the probe took its first ~3 s (A001064,
|
|
1053
|
+
# 2026-10-27), so SD playback keeps them (``keep_probe_packets``).
|
|
1043
1054
|
"-fflags",
|
|
1044
|
-
"+
|
|
1055
|
+
"+genpts+discardcorrupt"
|
|
1056
|
+
if keep_probe_packets
|
|
1057
|
+
else "+nobuffer+genpts+discardcorrupt",
|
|
1045
1058
|
"-analyzeduration",
|
|
1046
1059
|
"2000000",
|
|
1047
1060
|
"-probesize",
|
|
@@ -2428,7 +2441,11 @@ async def _sdes_keepalive_after_session(
|
|
|
2428
2441
|
|
|
2429
2442
|
|
|
2430
2443
|
class CameraMixin(
|
|
2431
|
-
_CameraControlsMixin,
|
|
2444
|
+
_CameraControlsMixin,
|
|
2445
|
+
_CameraSdMixin,
|
|
2446
|
+
_CameraSdPlaybackMixin,
|
|
2447
|
+
_WebRTCOpenMixin,
|
|
2448
|
+
_SdesOpenMixin,
|
|
2432
2449
|
):
|
|
2433
2450
|
"""All camera/streaming methods, mixed into DeviceClient via inheritance."""
|
|
2434
2451
|
|
|
@@ -2770,9 +2787,15 @@ class CameraMixin(
|
|
|
2770
2787
|
return getattr(self, "_last_live_play_code", None) == _LIVE_PLAY_NOT_READY
|
|
2771
2788
|
|
|
2772
2789
|
def _next_dseq(self) -> int:
|
|
2773
|
-
"""Next livePlayReq dseq (app parity: Q0() starts at 100, increments).
|
|
2774
|
-
|
|
2775
|
-
|
|
2790
|
+
"""Next livePlayReq dseq (app parity: Q0() starts at 100, increments).
|
|
2791
|
+
|
|
2792
|
+
Taken from the SDES bridge thread (SESSION_MODE, control commands) as
|
|
2793
|
+
well as the event loop (livePlayReq), so read-and-increment is one
|
|
2794
|
+
locked step: a repeated seq is a SESSION_MODE the camera ignores.
|
|
2795
|
+
"""
|
|
2796
|
+
with _DSEQ_LOCK:
|
|
2797
|
+
d = self._live_dseq
|
|
2798
|
+
self._live_dseq += 1
|
|
2776
2799
|
return d
|
|
2777
2800
|
|
|
2778
2801
|
def update_status_from_device(self, device: dict) -> "DeviceStatusData":
|
|
@@ -7109,6 +7132,8 @@ class CameraMixin(
|
|
|
7109
7132
|
*,
|
|
7110
7133
|
sdes: bool = False,
|
|
7111
7134
|
device_id: "Optional[str]" = None,
|
|
7135
|
+
client_class: int = 2,
|
|
7136
|
+
stream: "Optional[int]" = None,
|
|
7112
7137
|
) -> str:
|
|
7113
7138
|
"""Generate a peerId for a WebRTC connection.
|
|
7114
7139
|
|
|
@@ -7191,14 +7216,27 @@ class CameraMixin(
|
|
|
7191
7216
|
# smart-home classes (3, 4) that skip the camera's keepalive watchdog,
|
|
7192
7217
|
# so this changes what we claim to be and nothing about how the session
|
|
7193
7218
|
# is policed. Screen any future change on EACH model with a patched
|
|
7194
|
-
# build
|
|
7195
|
-
#
|
|
7196
|
-
#
|
|
7197
|
-
#
|
|
7198
|
-
#
|
|
7199
|
-
#
|
|
7200
|
-
|
|
7201
|
-
|
|
7219
|
+
# build.
|
|
7220
|
+
#
|
|
7221
|
+
# The class-0 result is explained now (firmware read 2026-10-27, the
|
|
7222
|
+
# same on the A000088, A001064 and A001513): the per-viewer codec
|
|
7223
|
+
# decision reads field2[0] itself and gives H265 only to class 0 or 1,
|
|
7224
|
+
# and only then adds a second, H265 video transceiver. An A000088 that
|
|
7225
|
+
# decides H265 sends its live video there, which the aiortc path
|
|
7226
|
+
# cannot receive - the session is up and empty. SD playback needs that
|
|
7227
|
+
# transceiver on a camera that records H265, so its open passes
|
|
7228
|
+
# ``client_class=0``; live views keep 2.
|
|
7229
|
+
#
|
|
7230
|
+
# ``stream`` is field 5, which the firmware logs as StreamID (0 = main
|
|
7231
|
+
# stream). The transport default it replaces is what every live open
|
|
7232
|
+
# has always sent.
|
|
7233
|
+
# Field 2 = client class + 5 random. The tail stays random: pinning all
|
|
7234
|
+
# six would make every peer id identical across opens, which is the
|
|
7235
|
+
# cross-session reuse the camera dedups on.
|
|
7236
|
+
if not 0 <= client_class <= 9:
|
|
7237
|
+
raise ValueError(f"client_class must be one digit, got {client_class}")
|
|
7238
|
+
rand6 = str(client_class) + os.urandom(3).hex()[1:]
|
|
7239
|
+
version = (1 if sdes else 2) if stream is None else stream
|
|
7202
7240
|
return f"{session}_{rand6}_{live_type}_{stream_id}_{version}"
|
|
7203
7241
|
|
|
7204
7242
|
async def async_open_webrtc_stream(
|
{python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -1788,7 +1788,15 @@ def video_pts_dts(state: dict, pts: int, slack: int) -> tuple:
|
|
|
1788
1788
|
|
|
1789
1789
|
|
|
1790
1790
|
def _dtls_av_mux_run(
|
|
1791
|
-
vq,
|
|
1791
|
+
vq,
|
|
1792
|
+
aq,
|
|
1793
|
+
out_fileobj,
|
|
1794
|
+
progress,
|
|
1795
|
+
stop_flag,
|
|
1796
|
+
*,
|
|
1797
|
+
align_audio_start=False,
|
|
1798
|
+
state=None,
|
|
1799
|
+
video_codec="h264",
|
|
1792
1800
|
) -> None:
|
|
1793
1801
|
"""Mux tapped video (H.264 copy) + audio (PCMA->AAC) to ``out_fileobj`` as
|
|
1794
1802
|
RTP-timestamped MPEG-TS. Runs in a worker thread; the serve's ffmpeg reads
|
|
@@ -1810,7 +1818,14 @@ def _dtls_av_mux_run(
|
|
|
1810
1818
|
the serve's timing is unchanged.
|
|
1811
1819
|
- ``state``, if given, gets ``state["failed"] = True`` when a write failed
|
|
1812
1820
|
and the mux ended early.
|
|
1821
|
+
- ``video_codec`` is the video stream's codec. None leaves it to the first
|
|
1822
|
+
keyframe: a queue item may carry its codec as a fourth element
|
|
1823
|
+
(``"hevc"`` from the SD playback H265 tap), and a three-element item is
|
|
1824
|
+
H.264. Only with ``align_audio_start``, which writes nothing before the
|
|
1825
|
+
first keyframe, so the stream can still be added then.
|
|
1813
1826
|
"""
|
|
1827
|
+
if video_codec is None and not align_audio_start:
|
|
1828
|
+
raise ValueError("video_codec=None needs align_audio_start")
|
|
1814
1829
|
import time as _t
|
|
1815
1830
|
import queue as _q
|
|
1816
1831
|
from fractions import Fraction
|
|
@@ -1880,8 +1895,10 @@ def _dtls_av_mux_run(
|
|
|
1880
1895
|
except Exception as exc:
|
|
1881
1896
|
_LOGGER.warning("DTLS A/V mux: av.open failed: %s", exc)
|
|
1882
1897
|
return
|
|
1883
|
-
vs =
|
|
1884
|
-
|
|
1898
|
+
vs = None
|
|
1899
|
+
if video_codec is not None:
|
|
1900
|
+
vs = out.add_stream(video_codec)
|
|
1901
|
+
vs.time_base = Fraction(1, 90000)
|
|
1885
1902
|
try:
|
|
1886
1903
|
adec = av.CodecContext.create("pcm_alaw", "r")
|
|
1887
1904
|
adec.sample_rate = 8000
|
|
@@ -1922,16 +1939,42 @@ def _dtls_av_mux_run(
|
|
|
1922
1939
|
# ffmpeg at its input probe so it never bound its -listen port. One dead
|
|
1923
1940
|
# write -> exit the thread and leave the queues alone.
|
|
1924
1941
|
mux_dead = [False]
|
|
1942
|
+
vs_box = [vs]
|
|
1943
|
+
vcodec_box = [video_codec]
|
|
1925
1944
|
|
|
1926
1945
|
def _flush_video():
|
|
1927
1946
|
while True:
|
|
1928
1947
|
try:
|
|
1929
|
-
|
|
1948
|
+
item = vq.get_nowait()
|
|
1930
1949
|
except _q.Empty:
|
|
1931
1950
|
break
|
|
1951
|
+
data, ts, kf = item[:3]
|
|
1952
|
+
_codec = item[3] if len(item) > 3 else "h264"
|
|
1953
|
+
if vs_box[0] is not None and video_codec is None:
|
|
1954
|
+
# Lazy mode: a camera sending both tracks must not put the
|
|
1955
|
+
# other codec into this stream. Compared by the name it was
|
|
1956
|
+
# added under: the stream reports its encoder (libx265).
|
|
1957
|
+
if vcodec_box[0] != _codec:
|
|
1958
|
+
continue
|
|
1932
1959
|
if not vstarted[0]:
|
|
1933
1960
|
if not kf:
|
|
1934
1961
|
continue # begin on a keyframe so the GOP is decodable
|
|
1962
|
+
if vs_box[0] is None:
|
|
1963
|
+
# The first keyframe says what the recording is.
|
|
1964
|
+
# The stream is only muxed into, but PyAV opens an
|
|
1965
|
+
# encoder for it, and x265 prints its banner straight to
|
|
1966
|
+
# stderr - into Home Assistant's log on every play.
|
|
1967
|
+
_vs = out.add_stream(
|
|
1968
|
+
_codec,
|
|
1969
|
+
options=(
|
|
1970
|
+
{"x265-params": "log-level=none"}
|
|
1971
|
+
if _codec == "hevc"
|
|
1972
|
+
else None
|
|
1973
|
+
),
|
|
1974
|
+
)
|
|
1975
|
+
_vs.time_base = Fraction(1, 90000)
|
|
1976
|
+
vs_box[0] = _vs
|
|
1977
|
+
vcodec_box[0] = _codec
|
|
1935
1978
|
vstarted[0] = True
|
|
1936
1979
|
v0[0] = ts
|
|
1937
1980
|
_rel = ts - v0[0]
|
|
@@ -1941,7 +1984,7 @@ def _dtls_av_mux_run(
|
|
|
1941
1984
|
# is_resent_video_frame. Dropping costs no media time.
|
|
1942
1985
|
continue
|
|
1943
1986
|
pkt = av.Packet(data)
|
|
1944
|
-
pkt.stream =
|
|
1987
|
+
pkt.stream = vs_box[0]
|
|
1945
1988
|
pkt.pts, pkt.dts = video_pts_dts(_ts_state, _rel, _slack)
|
|
1946
1989
|
pkt.time_base = Fraction(1, 90000)
|
|
1947
1990
|
if kf:
|
|
@@ -3166,22 +3209,50 @@ class _AvioWaiter:
|
|
|
3166
3209
|
look silent.
|
|
3167
3210
|
"""
|
|
3168
3211
|
|
|
3169
|
-
def __init__(self, router: "AvioResponseRouter", command: int) -> None:
|
|
3212
|
+
def __init__(self, router: "AvioResponseRouter", command: int, match=None) -> None:
|
|
3170
3213
|
self._router = router
|
|
3171
3214
|
self._command = command
|
|
3215
|
+
# Optional predicate on the reply: a command whose replies all share
|
|
3216
|
+
# one AVIO id (RECORD_PLAYCONTROL echoes the playback command in its
|
|
3217
|
+
# payload) can then have several questions out at once.
|
|
3218
|
+
self._match = match
|
|
3172
3219
|
self._future = _cfutures.Future()
|
|
3173
3220
|
|
|
3221
|
+
def _accepts(self, resp) -> bool:
|
|
3222
|
+
if self._match is None:
|
|
3223
|
+
return True
|
|
3224
|
+
try:
|
|
3225
|
+
return bool(self._match(resp))
|
|
3226
|
+
except Exception:
|
|
3227
|
+
return False
|
|
3228
|
+
|
|
3174
3229
|
def cancel(self) -> None:
|
|
3175
3230
|
"""Withdraw the question - used when the command could not be sent."""
|
|
3176
3231
|
self._router._forget(self)
|
|
3177
3232
|
|
|
3178
|
-
async def result(
|
|
3233
|
+
async def result(
|
|
3234
|
+
self, timeout: float, *, keep: bool = False
|
|
3235
|
+
) -> "Optional[AvioResponse]":
|
|
3179
3236
|
"""The camera's reply, or None if it did not answer within ``timeout``.
|
|
3180
3237
|
|
|
3181
3238
|
None is a real answer, not an error: this firmware may not implement a
|
|
3182
3239
|
response for every command we send, and a control call must never block
|
|
3183
3240
|
a view while we find out.
|
|
3241
|
+
|
|
3242
|
+
``keep`` leaves the question open when the wait times out, so a later
|
|
3243
|
+
call still gets a reply that arrives late. For a one-off question held
|
|
3244
|
+
for a session's life (the PLAY_START reply), not for a command sent
|
|
3245
|
+
repeatedly, whose registrations would pile up.
|
|
3184
3246
|
"""
|
|
3247
|
+
if keep and not self._future.done():
|
|
3248
|
+
try:
|
|
3249
|
+
# Shielded: a timed-out wait must not cancel the future the
|
|
3250
|
+
# bridge thread will still set.
|
|
3251
|
+
return await asyncio.wait_for(
|
|
3252
|
+
asyncio.shield(asyncio.wrap_future(self._future)), timeout
|
|
3253
|
+
)
|
|
3254
|
+
except TimeoutError:
|
|
3255
|
+
return None
|
|
3185
3256
|
if self._future.done():
|
|
3186
3257
|
# A wait that timed out cancels its own wrapper, and that
|
|
3187
3258
|
# cancellation can propagate down to this future - asking again
|
|
@@ -3223,13 +3294,16 @@ class AvioResponseRouter:
|
|
|
3223
3294
|
self._lock = threading.Lock()
|
|
3224
3295
|
self._waiting: "dict[int, list[_AvioWaiter]]" = {}
|
|
3225
3296
|
|
|
3226
|
-
def expect(self, command: int) -> _AvioWaiter:
|
|
3297
|
+
def expect(self, command: int, match=None) -> _AvioWaiter:
|
|
3227
3298
|
"""Register interest in the next response carrying ``command``.
|
|
3228
3299
|
|
|
3300
|
+
``match``, when given, narrows that to a response it returns True for;
|
|
3301
|
+
a response no waiter accepts is not answered to anyone.
|
|
3302
|
+
|
|
3229
3303
|
Call this before sending the request: the camera can answer before the
|
|
3230
3304
|
sender returns, and a registration made afterwards would miss the reply.
|
|
3231
3305
|
"""
|
|
3232
|
-
waiter = _AvioWaiter(self, command)
|
|
3306
|
+
waiter = _AvioWaiter(self, command, match)
|
|
3233
3307
|
with self._lock:
|
|
3234
3308
|
self._waiting.setdefault(command, []).append(waiter)
|
|
3235
3309
|
return waiter
|
|
@@ -3248,11 +3322,15 @@ class AvioResponseRouter:
|
|
|
3248
3322
|
with self._lock:
|
|
3249
3323
|
queue = self._waiting.get(resp.command)
|
|
3250
3324
|
waiter = None
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3325
|
+
if queue:
|
|
3326
|
+
# Oldest first, skipping questions already settled and
|
|
3327
|
+
# any whose match turns this reply down.
|
|
3328
|
+
queue[:] = [w for w in queue if not w._future.done()]
|
|
3329
|
+
for candidate in queue:
|
|
3330
|
+
if candidate._accepts(resp):
|
|
3331
|
+
waiter = candidate
|
|
3332
|
+
queue.remove(candidate)
|
|
3333
|
+
break
|
|
3256
3334
|
if queue is not None and not queue:
|
|
3257
3335
|
self._waiting.pop(resp.command, None)
|
|
3258
3336
|
if waiter is None:
|
|
@@ -3748,6 +3826,7 @@ class AvioRequestMixin:
|
|
|
3748
3826
|
*,
|
|
3749
3827
|
response_cmd: int,
|
|
3750
3828
|
timeout: float = AVIO_RESPONSE_TIMEOUT_S,
|
|
3829
|
+
match=None,
|
|
3751
3830
|
) -> "Optional[AvioResponse]":
|
|
3752
3831
|
"""Send ``cmd`` and return the camera's ``response_cmd`` reply, or None.
|
|
3753
3832
|
|
|
@@ -3755,7 +3834,7 @@ class AvioRequestMixin:
|
|
|
3755
3834
|
not a failure: this firmware may implement a request without a response,
|
|
3756
3835
|
and no reply at all points at the channel rather than the firmware.
|
|
3757
3836
|
"""
|
|
3758
|
-
waiter = self._avio_responses.expect(response_cmd)
|
|
3837
|
+
waiter = self._avio_responses.expect(response_cmd, match)
|
|
3759
3838
|
# Registered BEFORE the send: a camera on the LAN can answer before the
|
|
3760
3839
|
# sending call returns, and a reply that arrives with nobody listening
|
|
3761
3840
|
# would be reported as silence.
|
{python_aidot_cameras-1.0.0rc62 → python_aidot_cameras-1.0.0rc63}/aidot_cameras/camera/recording.py
RENAMED
|
@@ -50,8 +50,15 @@ class TsCopyRecorder:
|
|
|
50
50
|
video to :attr:`vq` and audio to :attr:`aq`.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
def __init__(
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
path: str,
|
|
56
|
+
device_id: Optional[str] = None,
|
|
57
|
+
video_codec: Optional[str] = "h264",
|
|
58
|
+
) -> None:
|
|
54
59
|
self._path = path
|
|
60
|
+
# None: the first keyframe names it (SD playback, H264 or H265).
|
|
61
|
+
self._video_codec = video_codec
|
|
55
62
|
self._device_id = device_id or "?"
|
|
56
63
|
self.vq: "queue.Queue" = queue.Queue(maxsize=_QUEUE_MAX)
|
|
57
64
|
self.aq: "queue.Queue" = queue.Queue(maxsize=_QUEUE_MAX)
|
|
@@ -71,7 +78,11 @@ class TsCopyRecorder:
|
|
|
71
78
|
self._thread = threading.Thread(
|
|
72
79
|
target=_dtls_av_mux_run,
|
|
73
80
|
args=(self.vq, self.aq, self._file, self._progress, self._stop_flag),
|
|
74
|
-
kwargs={
|
|
81
|
+
kwargs={
|
|
82
|
+
"align_audio_start": True,
|
|
83
|
+
"state": self._state,
|
|
84
|
+
"video_codec": self._video_codec,
|
|
85
|
+
},
|
|
75
86
|
name="aidot-dtls-record",
|
|
76
87
|
daemon=True,
|
|
77
88
|
)
|