python-aidot-cameras 1.0.0rc63__tar.gz → 1.0.0rc65__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.0rc63/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc65}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/client.py +11 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_playback.py +204 -15
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes.py +74 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes_open.py +92 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/webrtc_open.py +15 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-1.0.0rc65/tests/test_sd_playback_clock.py +225 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_dtls.py +39 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_entry.py +45 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_media_hold.py +66 -1
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/README.md +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_log_redaction.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/agc.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/health.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/hls_ts.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/recording.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/settings.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_arrival_timestamps.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_auth_error_routing.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_auto_alarm.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_auto_light_settings.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_header_single_layout.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_cap_spans_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_wake_settles_before_the_command.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ble_gateway_is_gone.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_error_log_names_the_exception.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_error_severity.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_declared_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_detection_types.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_serve_only_for_http.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_recorder_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_refusal_after_the_echo.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_tap_states_no_reorder.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_ts_copy_recording.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_every_exception_is_exported.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_first_media_abandon_rules.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_health_line.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_health_line_counts_pictures.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_hls_ts_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_hls_ts_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_credentials_match_the_app.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ipc_envelope_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_login_accessors.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_short_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_stops_on_close.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_sweep_only_with_lights.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_validate_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_log_hygiene.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_loop_break_ends_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_mypy_new_findings.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_unbounded_hex_dumps.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_open_errors_are_aidot_errors.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_phase_returns_reach_the_caller.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_phase_state_is_complete.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_plaintext_mqtt_refused.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_region_allowlist.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_review_lan_retry_and_graces.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_rtsp_publish_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_open.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_open_yields_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_pinned_codec_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_loop_releases_on_error.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_session_mode_seq.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_session_stop_twice.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_settings_registry.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_skip_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_status_and_trace_channels.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_stun_attr_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_swallowed_exception_names.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_transport_failure_reporting.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_turn_target_shared.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_seam_adapter.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_secrets_redacted.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_vendored_aiortc_integrity.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_presence_needs_a_keyframe.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/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.0rc65
|
|
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.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/client.py
RENAMED
|
@@ -1041,9 +1041,13 @@ def _build_sdes_serve_cmd(
|
|
|
1041
1041
|
# instead. Moving `-t` to the input side does not help - both placements
|
|
1042
1042
|
# fail. Only a bounded run gives this up, and a bounded run is a
|
|
1043
1043
|
# snapshot or the `-f null` drain, never the live stream.
|
|
1044
|
+
# Nor for SD playback (``keep_probe_packets``): a recording arrives
|
|
1045
|
+
# faster than it plays, and its timing is put right afterwards from
|
|
1046
|
+
# the camera's per-frame recording time (sd_playback.PlaybackClock),
|
|
1047
|
+
# which needs the camera's own RTP timestamps in the file.
|
|
1044
1048
|
*(
|
|
1045
1049
|
["-use_wallclock_as_timestamps", "1"]
|
|
1046
|
-
if _SERVE_ARRIVAL_TS and not max_seconds
|
|
1050
|
+
if _SERVE_ARRIVAL_TS and not max_seconds and not keep_probe_packets
|
|
1047
1051
|
else []
|
|
1048
1052
|
),
|
|
1049
1053
|
# ``nobuffer`` drops what ffmpeg reads while probing the input (up to
|
|
@@ -2792,6 +2796,12 @@ class CameraMixin(
|
|
|
2792
2796
|
Taken from the SDES bridge thread (SESSION_MODE, control commands) as
|
|
2793
2797
|
well as the event loop (livePlayReq), so read-and-increment is one
|
|
2794
2798
|
locked step: a repeated seq is a SESSION_MODE the camera ignores.
|
|
2799
|
+
|
|
2800
|
+
Starting again at 100 in every new client is safe: the camera keeps
|
|
2801
|
+
the last accepted seq per session, not per camera. Back-to-back
|
|
2802
|
+
playbacks from fresh clients, each counting from 100 where the one
|
|
2803
|
+
before had already gone past it, had SD mode echoed every time on all
|
|
2804
|
+
three models (2026-10-27), as the app's own restarts rely on.
|
|
2795
2805
|
"""
|
|
2796
2806
|
with _DSEQ_LOCK:
|
|
2797
2807
|
d = self._live_dseq
|
|
@@ -274,16 +274,6 @@ class _CameraSdPlaybackMixin:
|
|
|
274
274
|
record=record,
|
|
275
275
|
sd_channel=sd_channel,
|
|
276
276
|
)
|
|
277
|
-
peer_id = self.generate_webrtc_peer_id(
|
|
278
|
-
live_type=PLAY_CMD_SDCARD,
|
|
279
|
-
# Field 4, a literal 0 in the app - not the StreamID, which is
|
|
280
|
-
# field 5 (``stream``).
|
|
281
|
-
stream_id=0,
|
|
282
|
-
sdes=bool(self.is_sdes_camera),
|
|
283
|
-
device_id=self.device_id,
|
|
284
|
-
client_class=PLAYBACK_CLIENT_CLASS,
|
|
285
|
-
stream=PLAYBACK_STREAM,
|
|
286
|
-
)
|
|
287
277
|
return await self.async_open_webrtc_stream(
|
|
288
278
|
on_frame,
|
|
289
279
|
timeout=timeout,
|
|
@@ -291,11 +281,199 @@ class _CameraSdPlaybackMixin:
|
|
|
291
281
|
max_seconds=max_seconds,
|
|
292
282
|
status_callback=status_callback,
|
|
293
283
|
rtsp_push_url=rtsp_push_url,
|
|
294
|
-
reuse_peer_id=
|
|
284
|
+
reuse_peer_id=playback_peer_id(self, sdes=bool(self.is_sdes_camera)),
|
|
295
285
|
_sd_playback=start,
|
|
296
286
|
)
|
|
297
287
|
|
|
298
288
|
|
|
289
|
+
def playback_peer_id(camera: Any, *, sdes: bool) -> str:
|
|
290
|
+
"""The peer id a playback open announces, for the SDES or DTLS transport.
|
|
291
|
+
|
|
292
|
+
The SDES-to-DTLS fallback builds a fresh one with ``sdes=False``: the
|
|
293
|
+
SDES id's transport digit is wrong for DTLS, and a live id (class 2,
|
|
294
|
+
PRE_LINK) would get audio alone from an H265-recording camera.
|
|
295
|
+
"""
|
|
296
|
+
return camera.generate_webrtc_peer_id(
|
|
297
|
+
live_type=PLAY_CMD_SDCARD,
|
|
298
|
+
# Field 4, a literal 0 in the app - not the StreamID, which is
|
|
299
|
+
# field 5 (``stream``).
|
|
300
|
+
stream_id=0,
|
|
301
|
+
sdes=sdes,
|
|
302
|
+
device_id=camera.device_id,
|
|
303
|
+
client_class=PLAYBACK_CLIENT_CLASS,
|
|
304
|
+
stream=PLAYBACK_STREAM,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# --- The recording's own clock (SDES) ---------------------------------------
|
|
309
|
+
#
|
|
310
|
+
# The camera stamps a recording's RTP as if it had been recorded at its live
|
|
311
|
+
# frame rate and sends it at that pace: an A001064 night recording made at
|
|
312
|
+
# 5 fps (200 ms a frame) went out at 20 fps (50 ms a frame), its 60 s in 15 s,
|
|
313
|
+
# and its sound - sent in real time - stopped when the video ran out
|
|
314
|
+
# (2026-10-29). The vendor app plays it at the right speed because every
|
|
315
|
+
# packet also carries the frame's time in the recording, in an RTP header
|
|
316
|
+
# extension the camera adds once the offer names transport-cc and
|
|
317
|
+
# rtp-stream-id (OfferUtils.processRtp keeps exactly those two extmap lines;
|
|
318
|
+
# the answer then says ``supportRtpExt: 1``). The 11-byte value is a 2-byte
|
|
319
|
+
# playback channel, a flag byte (0xf0) and the time in Unix milliseconds, all
|
|
320
|
+
# big-endian; all ones marks the end of the recording, on the packets the
|
|
321
|
+
# camera sends after its last frame.
|
|
322
|
+
|
|
323
|
+
#: The two header extensions the vendor app's offer keeps, with the ids this
|
|
324
|
+
#: library offers them under.
|
|
325
|
+
EXT_TRANSPORT_CC = (
|
|
326
|
+
"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
|
|
327
|
+
)
|
|
328
|
+
EXT_FRAME_TIME = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
|
|
329
|
+
EXT_TRANSPORT_CC_ID = 3
|
|
330
|
+
EXT_FRAME_TIME_ID = 4
|
|
331
|
+
#: The frame-time value the camera sends once the recording has run out.
|
|
332
|
+
FRAME_TIME_END = 0xFFFFFFFFFFFFFFFF
|
|
333
|
+
|
|
334
|
+
_AUDIO_CLOCKS = {0: 8000, 8: 8000}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def offer_with_frame_times(sdp: str) -> str:
|
|
338
|
+
"""``sdp`` asking for the per-frame recording time: both extmap lines in
|
|
339
|
+
every audio and video section, after its mid, and transport-cc feedback
|
|
340
|
+
on each video payload type."""
|
|
341
|
+
extmaps = [
|
|
342
|
+
f"a=extmap:{EXT_TRANSPORT_CC_ID} {EXT_TRANSPORT_CC}",
|
|
343
|
+
f"a=extmap:{EXT_FRAME_TIME_ID} {EXT_FRAME_TIME}",
|
|
344
|
+
]
|
|
345
|
+
out = []
|
|
346
|
+
kind, pts = "", []
|
|
347
|
+
for line in sdp.split("\r\n"):
|
|
348
|
+
if line.startswith("m="):
|
|
349
|
+
kind = line[2:].split(" ", 1)[0]
|
|
350
|
+
pts = line.split()[3:]
|
|
351
|
+
out.append(line)
|
|
352
|
+
if line.startswith("a=mid:") and kind in ("audio", "video"):
|
|
353
|
+
out.extend(extmaps)
|
|
354
|
+
if kind == "video":
|
|
355
|
+
out.extend(f"a=rtcp-fb:{pt} transport-cc" for pt in pts)
|
|
356
|
+
return "\r\n".join(out)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def rtp_extension(pkt: bytes, ext_id: int) -> "Optional[bytes]":
|
|
360
|
+
"""The value of header extension ``ext_id`` in RTP packet ``pkt`` (RFC
|
|
361
|
+
8285, one- or two-byte form), or None. The header is in the clear under
|
|
362
|
+
SRTP, so this reads a packet still encrypted."""
|
|
363
|
+
if len(pkt) < 12 or not pkt[0] & 0x10:
|
|
364
|
+
return None
|
|
365
|
+
off = 12 + 4 * (pkt[0] & 0x0F)
|
|
366
|
+
if len(pkt) < off + 4:
|
|
367
|
+
return None
|
|
368
|
+
profile = int.from_bytes(pkt[off : off + 2], "big")
|
|
369
|
+
end = off + 4 + 4 * int.from_bytes(pkt[off + 2 : off + 4], "big")
|
|
370
|
+
if end > len(pkt):
|
|
371
|
+
return None
|
|
372
|
+
i = off + 4
|
|
373
|
+
while i < end:
|
|
374
|
+
if profile == 0xBEDE:
|
|
375
|
+
b = pkt[i]
|
|
376
|
+
if b == 0:
|
|
377
|
+
i += 1
|
|
378
|
+
continue
|
|
379
|
+
eid, n, i = b >> 4, (b & 0x0F) + 1, i + 1
|
|
380
|
+
elif profile >> 4 == 0x100 and i + 1 < end:
|
|
381
|
+
if pkt[i] == 0:
|
|
382
|
+
i += 1
|
|
383
|
+
continue
|
|
384
|
+
eid, n, i = pkt[i], pkt[i + 1], i + 2
|
|
385
|
+
else:
|
|
386
|
+
return None
|
|
387
|
+
if eid == ext_id:
|
|
388
|
+
return bytes(pkt[i : i + n])
|
|
389
|
+
i += n
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def frame_time_ms(value: "Optional[bytes]") -> "Optional[int]":
|
|
394
|
+
"""The recording time (Unix ms) in a frame-time extension value;
|
|
395
|
+
``FRAME_TIME_END`` at the end of the recording; None if absent."""
|
|
396
|
+
if value is None or len(value) != 11:
|
|
397
|
+
return None
|
|
398
|
+
return int.from_bytes(value[3:11], "big")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class PlaybackClock:
|
|
402
|
+
"""Pairs each stream's RTP timestamps with the recording's own time.
|
|
403
|
+
|
|
404
|
+
Fed every packet the playback forwards; keeps one (rtp timestamp, ms)
|
|
405
|
+
pair per change of recording time, per payload type. ``ended`` turns
|
|
406
|
+
True on the first end-of-recording mark. ``timing()`` is what
|
|
407
|
+
``retime_playback_ts`` needs.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
def __init__(self) -> None:
|
|
411
|
+
self._pairs: "dict[int, list[tuple[int, int]]]" = {}
|
|
412
|
+
self.ended = False
|
|
413
|
+
|
|
414
|
+
def observe(self, pkt: bytes) -> bool:
|
|
415
|
+
"""Note ``pkt``; True if it is an end-of-recording packet (forward
|
|
416
|
+
nothing of those: the recording is over)."""
|
|
417
|
+
ms = frame_time_ms(rtp_extension(pkt, EXT_FRAME_TIME_ID))
|
|
418
|
+
if ms is None:
|
|
419
|
+
return False
|
|
420
|
+
if ms == FRAME_TIME_END:
|
|
421
|
+
self.ended = True
|
|
422
|
+
return True
|
|
423
|
+
pairs = self._pairs.setdefault(pkt[1] & 0x7F, [])
|
|
424
|
+
if not pairs or pairs[-1][1] != ms:
|
|
425
|
+
pairs.append((int.from_bytes(pkt[4:8], "big"), ms))
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
def timing(self) -> "dict[str, tuple[float, float]]":
|
|
429
|
+
"""Per stream kind (``"video"``, ``"audio"``): (scale, start_s) - each
|
|
430
|
+
second of RTP time is ``scale`` seconds of recording, and the stream
|
|
431
|
+
starts ``start_s`` after the earliest stream does. Empty when no
|
|
432
|
+
frame times arrived."""
|
|
433
|
+
# One stream per kind: the payload type with the most frame times
|
|
434
|
+
# (a stray second video type would otherwise decide by dict order).
|
|
435
|
+
best: "dict[str, int]" = {}
|
|
436
|
+
for pt, pairs in self._pairs.items():
|
|
437
|
+
kind = "audio" if pt in _AUDIO_CLOCKS else "video"
|
|
438
|
+
if len(pairs) > len(self._pairs.get(best.get(kind, -1), ())):
|
|
439
|
+
best[kind] = pt
|
|
440
|
+
firsts = [self._pairs[pt][0][1] for pt in best.values()]
|
|
441
|
+
if not firsts:
|
|
442
|
+
return {}
|
|
443
|
+
origin = min(firsts)
|
|
444
|
+
out = {}
|
|
445
|
+
for kind, pt in best.items():
|
|
446
|
+
pairs = self._pairs[pt]
|
|
447
|
+
clock = _AUDIO_CLOCKS.get(pt, 90000)
|
|
448
|
+
(r0, m0), (r1, m1) = pairs[0], pairs[-1]
|
|
449
|
+
rtp_s = ((r1 - r0) & 0xFFFFFFFF) / clock
|
|
450
|
+
scale = (m1 - m0) / 1000 / rtp_s if rtp_s > 0 else 1.0
|
|
451
|
+
out[kind] = (scale, (m0 - origin) / 1000)
|
|
452
|
+
return out
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def retime_argv(
|
|
456
|
+
binary: str, src: str, dst: str, timing: "dict[str, tuple[float, float]]"
|
|
457
|
+
) -> "list[str]":
|
|
458
|
+
"""ffmpeg command that copies ``src`` to ``dst`` with each stream moved
|
|
459
|
+
onto the recording's clock (``PlaybackClock.timing``). An SDES capture's
|
|
460
|
+
G.711 rides a data stream, which takes the audio timing."""
|
|
461
|
+
|
|
462
|
+
def setts(scale: float, start_s: float) -> str:
|
|
463
|
+
def at(ts: str) -> str:
|
|
464
|
+
return f"({ts}-START{ts})*{scale:.6f}+{start_s:.6f}/TB"
|
|
465
|
+
|
|
466
|
+
return f"setts=pts={at('PTS')}:dts={at('DTS')}"
|
|
467
|
+
|
|
468
|
+
argv = [binary, "-hide_banner", "-loglevel", "error", "-nostdin", "-y"]
|
|
469
|
+
argv += ["-i", src, "-map", "0", "-c", "copy"]
|
|
470
|
+
if "video" in timing:
|
|
471
|
+
argv += ["-bsf:v", setts(*timing["video"])]
|
|
472
|
+
if "audio" in timing:
|
|
473
|
+
argv += ["-bsf:a", setts(*timing["audio"]), "-bsf:d", setts(*timing["audio"])]
|
|
474
|
+
return [*argv, "-f", "mpegts", dst]
|
|
475
|
+
|
|
476
|
+
|
|
299
477
|
# --- DTLS (A000088): the recording's H265 is taken raw ----------------------
|
|
300
478
|
#
|
|
301
479
|
# aiortc has no H265, and the camera sends a recording it holds as H265 on a
|
|
@@ -459,13 +637,21 @@ class H265AccessUnits:
|
|
|
459
637
|
self, seq: int, ts: int, marker: bool, payload: bytes
|
|
460
638
|
) -> "list[tuple[bytes, int, bool]]":
|
|
461
639
|
out = []
|
|
462
|
-
|
|
640
|
+
missing = 0
|
|
641
|
+
if self._last_seq is not None:
|
|
642
|
+
missing = (seq - self._last_seq - 1) & 0xFFFF
|
|
643
|
+
gap = missing != 0
|
|
463
644
|
if self._ts is not None and ts != self._ts:
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
|
|
645
|
+
# A unit in hand that never saw its marker lost at least its last
|
|
646
|
+
# packet; one already emitted at its marker lost nothing. Whatever
|
|
647
|
+
# else went missing is the head of this new unit (its VPS/SPS, or
|
|
648
|
+
# the first slice of a keyframe), so it is not handed on either.
|
|
649
|
+
pending = bool(self._nals) or self._fu is not None
|
|
650
|
+
if gap and pending:
|
|
467
651
|
self._broken = True
|
|
468
652
|
out += self._emit()
|
|
653
|
+
if missing > (1 if pending else 0):
|
|
654
|
+
self._broken = True
|
|
469
655
|
elif gap:
|
|
470
656
|
self._broken = True
|
|
471
657
|
self._fu = None
|
|
@@ -478,6 +664,9 @@ class H265AccessUnits:
|
|
|
478
664
|
i = 2
|
|
479
665
|
while i + 2 <= len(payload):
|
|
480
666
|
n = struct.unpack_from("!H", payload, i)[0]
|
|
667
|
+
if n == 0 or i + 2 + n > len(payload):
|
|
668
|
+
self._broken = True # truncated or corrupt
|
|
669
|
+
break
|
|
481
670
|
self._nals.append(bytes(payload[i + 2 : i + 2 + n]))
|
|
482
671
|
i += 2 + n
|
|
483
672
|
elif t == 49 and len(payload) >= 3: # fragmentation unit
|
{python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes.py
RENAMED
|
@@ -63,6 +63,8 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
|
|
|
63
63
|
abort_chan=None,
|
|
64
64
|
playback_waiter=None,
|
|
65
65
|
playback=None,
|
|
66
|
+
playback_clock=None,
|
|
67
|
+
playback_output=None,
|
|
66
68
|
) -> None:
|
|
67
69
|
# Which camera this session belongs to, for logging only. Optional so
|
|
68
70
|
# an existing caller that does not pass it still works; the logs then
|
|
@@ -135,6 +137,11 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
|
|
|
135
137
|
# channel), for pause, resume and seek; None for live.
|
|
136
138
|
self._sd_playback = playback
|
|
137
139
|
self._sd_seek_count = 0
|
|
140
|
+
# SD playback: the recording's own clock as the bridge saw it
|
|
141
|
+
# (sd_playback.PlaybackClock) and the .ts it was written to, which
|
|
142
|
+
# stop() moves onto that clock.
|
|
143
|
+
self._playback_clock = playback_clock
|
|
144
|
+
self._playback_output = playback_output
|
|
138
145
|
# Shared talk state (dict) for outbound two-way audio, or None when the
|
|
139
146
|
# session was not opened talk-capable (offer stayed recvonly). Populated
|
|
140
147
|
# by the bridge (camera audio addr) and by async_start_talk (provider).
|
|
@@ -254,6 +261,13 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
|
|
|
254
261
|
"""time.monotonic() of the camera's last STUN answer, 0.0 if none yet."""
|
|
255
262
|
return self._ice_progress[0]
|
|
256
263
|
|
|
264
|
+
@property
|
|
265
|
+
def playback_ended(self) -> bool:
|
|
266
|
+
"""True once the camera has marked the end of the recording this
|
|
267
|
+
session plays (False for live, and before the end)."""
|
|
268
|
+
clock = self._playback_clock
|
|
269
|
+
return bool(clock is not None and clock.ended)
|
|
270
|
+
|
|
257
271
|
def media_stats(self) -> dict:
|
|
258
272
|
"""Snapshot of media actually forwarded to ffmpeg by the bridge.
|
|
259
273
|
|
|
@@ -372,6 +386,62 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
|
|
|
372
386
|
# others are waiting on.
|
|
373
387
|
await asyncio.shield(task)
|
|
374
388
|
|
|
389
|
+
async def _retime_playback(self) -> None:
|
|
390
|
+
"""Move a played recording's .ts onto the recording's own clock.
|
|
391
|
+
|
|
392
|
+
The camera's RTP clock runs at the live frame rate even for a
|
|
393
|
+
recording made slower (sd_playback.PlaybackClock), so the file ffmpeg
|
|
394
|
+
wrote plays fast; the frame-time extension says by how much. Left as
|
|
395
|
+
written when there is nothing to correct (no frame times, a live
|
|
396
|
+
session, an output that is not a .ts) or when the copy fails.
|
|
397
|
+
"""
|
|
398
|
+
clock, path = self._playback_clock, self._playback_output
|
|
399
|
+
if clock is None or not path or not str(path).endswith(".ts"):
|
|
400
|
+
return
|
|
401
|
+
timing = clock.timing()
|
|
402
|
+
if not timing or all(
|
|
403
|
+
abs(s - 1.0) < 0.02 and o < 0.05 for s, o in timing.values()
|
|
404
|
+
):
|
|
405
|
+
return
|
|
406
|
+
import os
|
|
407
|
+
import subprocess
|
|
408
|
+
|
|
409
|
+
from .sd_playback import retime_argv
|
|
410
|
+
|
|
411
|
+
tmp = f"{path}.retime.ts"
|
|
412
|
+
argv = retime_argv("ffmpeg", path, tmp, timing)
|
|
413
|
+
|
|
414
|
+
def _run() -> bool:
|
|
415
|
+
try:
|
|
416
|
+
done = subprocess.run(argv, capture_output=True, timeout=120)
|
|
417
|
+
except Exception:
|
|
418
|
+
return False
|
|
419
|
+
if done.returncode != 0 or not os.path.exists(tmp):
|
|
420
|
+
return False
|
|
421
|
+
os.replace(tmp, path)
|
|
422
|
+
return True
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
ok = await asyncio.get_running_loop().run_in_executor(None, _run)
|
|
426
|
+
except Exception:
|
|
427
|
+
ok = False
|
|
428
|
+
if not ok:
|
|
429
|
+
try:
|
|
430
|
+
os.unlink(tmp)
|
|
431
|
+
except OSError:
|
|
432
|
+
pass
|
|
433
|
+
_LOGGER.warning(
|
|
434
|
+
"camera %s: the played recording could not be put on its own"
|
|
435
|
+
" clock and plays at the camera's send rate",
|
|
436
|
+
self._device_id,
|
|
437
|
+
)
|
|
438
|
+
return
|
|
439
|
+
_LOGGER.info(
|
|
440
|
+
"camera %s: played recording put on its own clock (%s)",
|
|
441
|
+
self._device_id,
|
|
442
|
+
", ".join(f"{k} x{s:.2f} from +{o:.1f}s" for k, (s, o) in timing.items()),
|
|
443
|
+
)
|
|
444
|
+
|
|
375
445
|
async def _stop_once(self) -> None:
|
|
376
446
|
if self._talk_state is not None:
|
|
377
447
|
# Ask the bridge to close the camera speaker, then give it a brief
|
|
@@ -454,6 +524,7 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
|
|
|
454
524
|
except Exception:
|
|
455
525
|
_LOGGER.debug("swallowed exception in %s", "_stop_once", exc_info=True)
|
|
456
526
|
self._log_ffmpeg_stderr(stderr_bytes or self._drained_stderr_tail())
|
|
527
|
+
await self._retime_playback()
|
|
457
528
|
import os
|
|
458
529
|
|
|
459
530
|
try:
|
|
@@ -465,7 +536,9 @@ class SdesSession(_PlaybackControls, AvioRequestMixin):
|
|
|
465
536
|
try:
|
|
466
537
|
_sock.close()
|
|
467
538
|
except Exception:
|
|
468
|
-
_LOGGER.debug(
|
|
539
|
+
_LOGGER.debug(
|
|
540
|
+
"swallowed exception in %s", "_stop_once", exc_info=True
|
|
541
|
+
)
|
|
469
542
|
self._outgoing_q.put_nowait(None)
|
|
470
543
|
try:
|
|
471
544
|
await asyncio.wait_for(self._mqtt_fut, timeout=_STOP_MQTT_WAIT_S)
|
{python_aidot_cameras-1.0.0rc63 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -8,6 +8,7 @@ breaks the client<->sdes_open cycle). CameraMixin inherits this mixin, so
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import collections
|
|
11
12
|
import functools
|
|
12
13
|
import hashlib as _hashlib_mod
|
|
13
14
|
import logging
|
|
@@ -35,6 +36,7 @@ from .constants import (
|
|
|
35
36
|
)
|
|
36
37
|
from .models import VideoFrame # noqa: F401 - forward-ref annotation
|
|
37
38
|
from .sdes import SdesSession
|
|
39
|
+
from .sd_playback import EXT_FRAME_TIME, EXT_TRANSPORT_CC
|
|
38
40
|
from .rtsp_publish import (
|
|
39
41
|
redact_url,
|
|
40
42
|
LoopbackRtpPublisher,
|
|
@@ -1109,9 +1111,18 @@ def _session_mode_echoed(frame: bytes) -> "Optional[int]":
|
|
|
1109
1111
|
|
|
1110
1112
|
#: How many packets a playback open holds for its serve (see _new_media_hold).
|
|
1111
1113
|
#: The serve is up within a second; ~70 packets/s on an A001064 recording.
|
|
1114
|
+
#: The hold is released once the serve binds or 4.5 s after it is launched
|
|
1115
|
+
#: (the bind wait and its fallback), so the cap is several times what one
|
|
1116
|
+
#: open can hold; a drop past it is logged as a warning, since the served
|
|
1117
|
+
#: clip then has a hole after its opening.
|
|
1112
1118
|
_MEDIA_HOLD_MAX = 4000
|
|
1113
1119
|
|
|
1114
1120
|
|
|
1121
|
+
#: How many recent sequence numbers per stream a playback remembers, to drop
|
|
1122
|
+
#: a packet that arrives twice. A few seconds of video at 2560x1440.
|
|
1123
|
+
_DUPLICATE_WINDOW = 2048
|
|
1124
|
+
|
|
1125
|
+
|
|
1115
1126
|
class _MediaHold:
|
|
1116
1127
|
"""Packets for the serve, held in order until it is listening.
|
|
1117
1128
|
|
|
@@ -1119,6 +1130,14 @@ class _MediaHold:
|
|
|
1119
1130
|
loopback port; ``release`` by the open once the serve is bound. The first
|
|
1120
1131
|
``send`` (or ``flush``) after the release hands over everything held -
|
|
1121
1132
|
still on the bridge thread, so nothing can overtake it.
|
|
1133
|
+
|
|
1134
|
+
It also drops an RTP packet already forwarded (same port, SSRC and
|
|
1135
|
+
sequence number). On a host that reaches the camera by two routes every
|
|
1136
|
+
packet can arrive twice: measured on the reference box 2026-10-28, each
|
|
1137
|
+
playback audio packet came twice and video packets up to six times. ffmpeg
|
|
1138
|
+
discards a repeated video packet, but the G.711 is copied as data, so
|
|
1139
|
+
every repeat was written too and the clip's sound ran twice as long as its
|
|
1140
|
+
picture.
|
|
1122
1141
|
"""
|
|
1123
1142
|
|
|
1124
1143
|
def __init__(self, limit: int = 0) -> None:
|
|
@@ -1126,6 +1145,31 @@ class _MediaHold:
|
|
|
1126
1145
|
self._released = False
|
|
1127
1146
|
self._limit = limit or _MEDIA_HOLD_MAX
|
|
1128
1147
|
self.dropped = 0
|
|
1148
|
+
self.duplicates = 0
|
|
1149
|
+
self._seen: "dict" = {}
|
|
1150
|
+
# The recording's own clock, from each packet's frame-time extension
|
|
1151
|
+
# (sd_playback.PlaybackClock); its end mark ends the recording.
|
|
1152
|
+
from .sd_playback import PlaybackClock
|
|
1153
|
+
|
|
1154
|
+
self.clock = PlaybackClock()
|
|
1155
|
+
|
|
1156
|
+
def _repeat(self, pkt: bytes, port: int) -> bool:
|
|
1157
|
+
"""Is ``pkt`` an RTP packet this hold has already passed on?"""
|
|
1158
|
+
if len(pkt) < 12 or pkt[0] >> 6 != 2 or 192 <= pkt[1] <= 223:
|
|
1159
|
+
return False # not RTP, or RTCP (payload types 192-223)
|
|
1160
|
+
ssrc = int.from_bytes(pkt[8:12], "big")
|
|
1161
|
+
seq = int.from_bytes(pkt[2:4], "big")
|
|
1162
|
+
seen = self._seen.get((port, ssrc))
|
|
1163
|
+
if seen is None:
|
|
1164
|
+
seen = self._seen[(port, ssrc)] = (set(), collections.deque())
|
|
1165
|
+
recent, order = seen
|
|
1166
|
+
if seq in recent:
|
|
1167
|
+
return True
|
|
1168
|
+
recent.add(seq)
|
|
1169
|
+
order.append(seq)
|
|
1170
|
+
if len(order) > _DUPLICATE_WINDOW:
|
|
1171
|
+
recent.discard(order.popleft())
|
|
1172
|
+
return False
|
|
1129
1173
|
|
|
1130
1174
|
def release(self) -> None:
|
|
1131
1175
|
self._released = True
|
|
@@ -1134,6 +1178,24 @@ class _MediaHold:
|
|
|
1134
1178
|
return self._released and self._held is not None
|
|
1135
1179
|
|
|
1136
1180
|
def send(self, sock, pkt: bytes, port: int, *, pace=None, log=None) -> None:
|
|
1181
|
+
was_ended = self.clock.ended
|
|
1182
|
+
if self.clock.observe(pkt):
|
|
1183
|
+
# After its last frame the camera marks packets as the end of the
|
|
1184
|
+
# recording; there is nothing more of it to serve.
|
|
1185
|
+
if not was_ended and log is not None:
|
|
1186
|
+
try:
|
|
1187
|
+
log("bridge: the camera marked the end of the recording")
|
|
1188
|
+
except Exception:
|
|
1189
|
+
pass
|
|
1190
|
+
return
|
|
1191
|
+
if self._repeat(pkt, port):
|
|
1192
|
+
self.duplicates += 1
|
|
1193
|
+
if self.duplicates == 1 and log is not None:
|
|
1194
|
+
try:
|
|
1195
|
+
log("bridge: dropping RTP packets that arrive twice")
|
|
1196
|
+
except Exception:
|
|
1197
|
+
pass
|
|
1198
|
+
return
|
|
1137
1199
|
if self._held is not None:
|
|
1138
1200
|
if not self._released:
|
|
1139
1201
|
if len(self._held) < self._limit:
|
|
@@ -1149,6 +1211,14 @@ class _MediaHold:
|
|
|
1149
1211
|
first keyframe alone is ~130 packets and one burst of all of them can
|
|
1150
1212
|
overrun the serve's socket. Returns how many were sent."""
|
|
1151
1213
|
held, self._held = self._held or [], None
|
|
1214
|
+
if self.dropped:
|
|
1215
|
+
_LOGGER.warning(
|
|
1216
|
+
"SD playback: %d packet(s) over the %d-packet hold were"
|
|
1217
|
+
" dropped before the serve listened; the clip has a gap"
|
|
1218
|
+
" after its opening",
|
|
1219
|
+
self.dropped,
|
|
1220
|
+
self._limit,
|
|
1221
|
+
)
|
|
1152
1222
|
sent = 0
|
|
1153
1223
|
for i, (sock, pkt, port) in enumerate(held):
|
|
1154
1224
|
try:
|
|
@@ -2046,6 +2116,11 @@ def turn_install_permissions(
|
|
|
2046
2116
|
return _perm_ok
|
|
2047
2117
|
|
|
2048
2118
|
|
|
2119
|
+
#: The extmap lines the vendor app's offer filter keeps (OfferUtils.processRtp):
|
|
2120
|
+
#: the two a playback offer adds (sd_playback.offer_with_frame_times).
|
|
2121
|
+
_KEPT_EXTMAPS = (EXT_TRANSPORT_CC, EXT_FRAME_TIME)
|
|
2122
|
+
|
|
2123
|
+
|
|
2049
2124
|
def compress_sdp_req(device_id, _sdp: str) -> str:
|
|
2050
2125
|
"""g.b() equivalent - selective SDP filter for wPayload."""
|
|
2051
2126
|
_out: list = []
|
|
@@ -2074,6 +2149,14 @@ def compress_sdp_req(device_id, _sdp: str) -> str:
|
|
|
2074
2149
|
if any(_d in _ln for _d in ("sendrecv", "recvonly", "sendonly")):
|
|
2075
2150
|
_k(_ln)
|
|
2076
2151
|
continue
|
|
2152
|
+
if _ln.startswith("a=extmap"):
|
|
2153
|
+
# OfferUtils.processRtp keeps these two, once each across the
|
|
2154
|
+
# SDP; firmware that reads wPayload sees no others. A live offer
|
|
2155
|
+
# names neither, so this changes nothing for it.
|
|
2156
|
+
for _uri in _KEPT_EXTMAPS:
|
|
2157
|
+
if _uri in _ln and _seen.get(_uri) is None:
|
|
2158
|
+
_k(_ln, _uri)
|
|
2159
|
+
continue
|
|
2077
2160
|
for _ak in (
|
|
2078
2161
|
"ice-ufrag",
|
|
2079
2162
|
"ice-pwd",
|
|
@@ -6645,6 +6728,13 @@ async def _sdes_open_build_offer(self, ps: _SdesOpenState) -> None:
|
|
|
6645
6728
|
)
|
|
6646
6729
|
if ps._status:
|
|
6647
6730
|
ps._status(f"SDES: offer pinned to video pt={_pin_video_pt}")
|
|
6731
|
+
if ps.sd_playback is not None:
|
|
6732
|
+
# The per-frame recording time the camera adds once the offer names
|
|
6733
|
+
# transport-cc and rtp-stream-id, as the vendor app's does: its RTP
|
|
6734
|
+
# clock runs at the live frame rate even for a slower recording.
|
|
6735
|
+
from .sd_playback import offer_with_frame_times
|
|
6736
|
+
|
|
6737
|
+
ps.sdes_offer_sdp = offer_with_frame_times(ps.sdes_offer_sdp)
|
|
6648
6738
|
|
|
6649
6739
|
_relay_str = (
|
|
6650
6740
|
f" relay-audio={ps._relay_addrs[ps._audio_sock][0]}:{ps._relay_addrs[ps._audio_sock][1]}"
|
|
@@ -9800,4 +9890,6 @@ class _SdesOpenMixin:
|
|
|
9800
9890
|
# holding the started stream back for it.
|
|
9801
9891
|
playback_waiter=_sd_start_waiter,
|
|
9802
9892
|
playback=ps.sd_playback,
|
|
9893
|
+
playback_clock=_media_hold.clock if _media_hold is not None else None,
|
|
9894
|
+
playback_output=output_path if ps.sd_playback is not None else None,
|
|
9803
9895
|
)
|
|
@@ -2458,6 +2458,18 @@ async def _dtls_open_sdes_branch(self, ps: _DtlsOpenState) -> _PhaseReturn | Non
|
|
|
2458
2458
|
await asyncio.wait_for(ps.mqtt_fut, timeout=5.0)
|
|
2459
2459
|
except Exception:
|
|
2460
2460
|
pass # MQTT thread exit errors don't affect the retry
|
|
2461
|
+
# A playback keeps what it asked for across the fallback: its own
|
|
2462
|
+
# peer id rebuilt for DTLS (the live default is class 2 and
|
|
2463
|
+
# PRE_LINK, which an H265-recording camera sends audio alone),
|
|
2464
|
+
# and rtsp_push_url, so the DTLS open refuses what it cannot
|
|
2465
|
+
# serve instead of dropping it. A live open mints a fresh id.
|
|
2466
|
+
_fallback_peer_id = None
|
|
2467
|
+
_fallback_push_url = None
|
|
2468
|
+
if ps._sd_playback is not None:
|
|
2469
|
+
from .sd_playback import playback_peer_id
|
|
2470
|
+
|
|
2471
|
+
_fallback_peer_id = playback_peer_id(self, sdes=False)
|
|
2472
|
+
_fallback_push_url = ps.rtsp_push_url
|
|
2461
2473
|
# Call the UNGATED impl, not the public async_open_webrtc_stream:
|
|
2462
2474
|
# we are already holding the global open-gate permit (acquired in
|
|
2463
2475
|
# async_open_webrtc_stream), and the gate's semaphore is not
|
|
@@ -2472,8 +2484,11 @@ async def _dtls_open_sdes_branch(self, ps: _DtlsOpenState) -> _PhaseReturn | Non
|
|
|
2472
2484
|
stream_id=ps.stream_id,
|
|
2473
2485
|
timeout=ps.timeout,
|
|
2474
2486
|
output_path=ps.output_path,
|
|
2487
|
+
max_seconds=ps.max_seconds,
|
|
2475
2488
|
status_callback=ps.status_callback,
|
|
2476
2489
|
force_sdes=False,
|
|
2490
|
+
rtsp_push_url=_fallback_push_url,
|
|
2491
|
+
reuse_peer_id=_fallback_peer_id,
|
|
2477
2492
|
_skip_ice_config=True,
|
|
2478
2493
|
_ice_config=(
|
|
2479
2494
|
ps.ice_config_fut.result() if ps.ice_config_fut.done() else None
|
|
@@ -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.0rc65"
|
|
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.0rc65
|
|
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
|
|
@@ -259,6 +259,7 @@ tests/test_sd_event_decode.py
|
|
|
259
259
|
tests/test_sd_event_probe_payloads.py
|
|
260
260
|
tests/test_sd_listing.py
|
|
261
261
|
tests/test_sd_playback.py
|
|
262
|
+
tests/test_sd_playback_clock.py
|
|
262
263
|
tests/test_sd_playback_controls.py
|
|
263
264
|
tests/test_sd_playback_dtls.py
|
|
264
265
|
tests/test_sd_playback_entry.py
|