python-aidot-cameras 1.0.0rc64__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.0rc64/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc65}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/client.py +5 -1
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_playback.py +169 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes.py +74 -1
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes_open.py +38 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0rc64 → 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.0rc64 → python_aidot_cameras-1.0.0rc65}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/README.md +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_log_redaction.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/agc.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/health.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/hls_ts.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/recording.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/settings.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_arrival_timestamps.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_auth_error_routing.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_auto_alarm.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_auto_light_settings.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_header_single_layout.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_cap_spans_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_battery_wake_settles_before_the_command.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ble_gateway_is_gone.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_close_mid_ice_leaves_nothing_pending.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_error_log_names_the_exception.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_error_severity.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_declared_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_detection_types.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_direct_ts_serve_only_for_http.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_open_abort_covers_every_exit.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_recorder_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_refusal_after_the_echo.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_tap_states_no_reorder.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_ts_copy_recording.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_every_exception_is_exported.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ffmpeg_rx_sdp_single_template.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_first_media_abandon_rules.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_health_line.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_health_line_counts_pictures.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_hls_ts_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_hls_ts_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_credentials_match_the_app.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ipc_envelope_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_login_accessors.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_short_sessions.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_silences_upstream_over_ceiling.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_retry_stops_on_close.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_lan_sweep_only_with_lights.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_live_validate_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_log_hygiene.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_loop_break_ends_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_mypy_new_findings.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_no_unbounded_hex_dumps.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_open_errors_are_aidot_errors.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_phase_returns_reach_the_caller.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_phase_state_is_complete.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_plaintext_mqtt_refused.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_region_allowlist.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_review_lan_retry_and_graces.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_rtsp_publish_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_dtls.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_entry.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_media_hold.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sd_playback_open.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_open_yields_the_loop.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_pinned_codec_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_loop_releases_on_error.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_session_mode_seq.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_session_stop_twice.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_settings_registry.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_skip_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_status_and_trace_channels.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_stun_attr_single_builder.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_swallowed_exception_names.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_transport_failure_reporting.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ts_fanout.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_ts_tee.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_turn_target_shared.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_seam_adapter.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_upstream_secrets_redacted.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_vendored_aiortc_integrity.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_video_presence_needs_a_keyframe.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc64 → 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.0rc64 → 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
|
|
@@ -305,6 +305,175 @@ def playback_peer_id(camera: Any, *, sdes: bool) -> str:
|
|
|
305
305
|
)
|
|
306
306
|
|
|
307
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
|
+
|
|
308
477
|
# --- DTLS (A000088): the recording's H265 is taken raw ----------------------
|
|
309
478
|
#
|
|
310
479
|
# aiortc has no H265, and the camera sends a recording it holds as H265 on a
|
{python_aidot_cameras-1.0.0rc64 → 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.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -36,6 +36,7 @@ from .constants import (
|
|
|
36
36
|
)
|
|
37
37
|
from .models import VideoFrame # noqa: F401 - forward-ref annotation
|
|
38
38
|
from .sdes import SdesSession
|
|
39
|
+
from .sd_playback import EXT_FRAME_TIME, EXT_TRANSPORT_CC
|
|
39
40
|
from .rtsp_publish import (
|
|
40
41
|
redact_url,
|
|
41
42
|
LoopbackRtpPublisher,
|
|
@@ -1146,6 +1147,11 @@ class _MediaHold:
|
|
|
1146
1147
|
self.dropped = 0
|
|
1147
1148
|
self.duplicates = 0
|
|
1148
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()
|
|
1149
1155
|
|
|
1150
1156
|
def _repeat(self, pkt: bytes, port: int) -> bool:
|
|
1151
1157
|
"""Is ``pkt`` an RTP packet this hold has already passed on?"""
|
|
@@ -1172,6 +1178,16 @@ class _MediaHold:
|
|
|
1172
1178
|
return self._released and self._held is not None
|
|
1173
1179
|
|
|
1174
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
|
|
1175
1191
|
if self._repeat(pkt, port):
|
|
1176
1192
|
self.duplicates += 1
|
|
1177
1193
|
if self.duplicates == 1 and log is not None:
|
|
@@ -2100,6 +2116,11 @@ def turn_install_permissions(
|
|
|
2100
2116
|
return _perm_ok
|
|
2101
2117
|
|
|
2102
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
|
+
|
|
2103
2124
|
def compress_sdp_req(device_id, _sdp: str) -> str:
|
|
2104
2125
|
"""g.b() equivalent - selective SDP filter for wPayload."""
|
|
2105
2126
|
_out: list = []
|
|
@@ -2128,6 +2149,14 @@ def compress_sdp_req(device_id, _sdp: str) -> str:
|
|
|
2128
2149
|
if any(_d in _ln for _d in ("sendrecv", "recvonly", "sendonly")):
|
|
2129
2150
|
_k(_ln)
|
|
2130
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
|
|
2131
2160
|
for _ak in (
|
|
2132
2161
|
"ice-ufrag",
|
|
2133
2162
|
"ice-pwd",
|
|
@@ -6699,6 +6728,13 @@ async def _sdes_open_build_offer(self, ps: _SdesOpenState) -> None:
|
|
|
6699
6728
|
)
|
|
6700
6729
|
if ps._status:
|
|
6701
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)
|
|
6702
6738
|
|
|
6703
6739
|
_relay_str = (
|
|
6704
6740
|
f" relay-audio={ps._relay_addrs[ps._audio_sock][0]}:{ps._relay_addrs[ps._audio_sock][1]}"
|
|
@@ -9854,4 +9890,6 @@ class _SdesOpenMixin:
|
|
|
9854
9890
|
# holding the started stream back for it.
|
|
9855
9891
|
playback_waiter=_sd_start_waiter,
|
|
9856
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,
|
|
9857
9895
|
)
|
|
@@ -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
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""SD playback puts a recording on its own clock.
|
|
2
|
+
|
|
3
|
+
An A001064 night recording made at 5 fps went out stamped and paced at the
|
|
4
|
+
live 20 fps (2026-10-29): 60 s of video in 15 s, its sound sent in real time
|
|
5
|
+
and cut off when the video ran out. Every packet carries the frame's time in
|
|
6
|
+
the recording in an RTP header extension once the offer names transport-cc
|
|
7
|
+
and rtp-stream-id, as the vendor app's does; the library offers both, keeps
|
|
8
|
+
them through the wPayload filter, reads the time off each packet and moves
|
|
9
|
+
the written file onto it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from aidot_cameras.camera.sd_playback import (
|
|
19
|
+
EXT_FRAME_TIME,
|
|
20
|
+
EXT_FRAME_TIME_ID,
|
|
21
|
+
EXT_TRANSPORT_CC,
|
|
22
|
+
EXT_TRANSPORT_CC_ID,
|
|
23
|
+
FRAME_TIME_END,
|
|
24
|
+
PlaybackClock,
|
|
25
|
+
frame_time_ms,
|
|
26
|
+
offer_with_frame_times,
|
|
27
|
+
retime_argv,
|
|
28
|
+
rtp_extension,
|
|
29
|
+
)
|
|
30
|
+
from aidot_cameras.camera.sdes_open import _MediaHold, compress_sdp_req
|
|
31
|
+
|
|
32
|
+
T0 = 1_792_988_619_319 # 2026-10-26T04:23:39.319Z, a played night clip
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _value(ms, channel=0x14D4):
|
|
36
|
+
return channel.to_bytes(2, "big") + b"\xf0" + ms.to_bytes(8, "big")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _rtp(pt, seq, ts, ms=None, *, two_byte=False):
|
|
40
|
+
hdr = bytes([0x80, pt]) + seq.to_bytes(2, "big") + ts.to_bytes(4, "big")
|
|
41
|
+
hdr += (0x1259).to_bytes(4, "big")
|
|
42
|
+
if ms is None:
|
|
43
|
+
return hdr + b"payload"
|
|
44
|
+
if two_byte:
|
|
45
|
+
ext = bytes([EXT_TRANSPORT_CC_ID, 2]) + seq.to_bytes(2, "big")
|
|
46
|
+
ext += bytes([EXT_FRAME_TIME_ID, 11]) + _value(ms)
|
|
47
|
+
profile = b"\x10\x00"
|
|
48
|
+
else:
|
|
49
|
+
ext = bytes([(EXT_TRANSPORT_CC_ID << 4) | 1]) + seq.to_bytes(2, "big")
|
|
50
|
+
ext += bytes([(EXT_FRAME_TIME_ID << 4) | 10]) + _value(ms)
|
|
51
|
+
profile = b"\xbe\xde"
|
|
52
|
+
ext += b"\x00" * (-len(ext) % 4)
|
|
53
|
+
hdr = bytes([0x90]) + hdr[1:]
|
|
54
|
+
return hdr + profile + (len(ext) // 4).to_bytes(2, "big") + ext + b"payload"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_the_frame_time_is_read_in_either_extension_form():
|
|
58
|
+
for two in (False, True):
|
|
59
|
+
pkt = _rtp(97, 7, 1000, T0, two_byte=two)
|
|
60
|
+
assert frame_time_ms(rtp_extension(pkt, EXT_FRAME_TIME_ID)) == T0
|
|
61
|
+
assert rtp_extension(pkt, EXT_TRANSPORT_CC_ID) == (7).to_bytes(2, "big")
|
|
62
|
+
assert rtp_extension(_rtp(97, 7, 1000), EXT_FRAME_TIME_ID) is None
|
|
63
|
+
assert frame_time_ms(None) is None
|
|
64
|
+
assert frame_time_ms(b"short") is None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_night_clip_is_stretched_to_the_recording_and_its_sound_placed():
|
|
68
|
+
# Measured: 200 ms of recording per frame, 50 ms of RTP (4500 ticks);
|
|
69
|
+
# the sound starts 6 s into the clip and runs in real time.
|
|
70
|
+
clock = PlaybackClock()
|
|
71
|
+
for i in range(301):
|
|
72
|
+
clock.observe(_rtp(97, i, 1000 + i * 4500, T0 + i * 200))
|
|
73
|
+
for i in range(200):
|
|
74
|
+
clock.observe(_rtp(8, i, 50 + i * 512, T0 + 6002 + i * 64))
|
|
75
|
+
timing = clock.timing()
|
|
76
|
+
assert timing["video"] == (pytest.approx(4.0), 0.0)
|
|
77
|
+
assert timing["audio"] == (pytest.approx(1.0), pytest.approx(6.002))
|
|
78
|
+
assert not clock.ended
|
|
79
|
+
assert clock.observe(_rtp(97, 999, 0, FRAME_TIME_END)) is True
|
|
80
|
+
assert clock.ended
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_no_frame_times_means_nothing_to_correct():
|
|
84
|
+
clock = PlaybackClock()
|
|
85
|
+
clock.observe(_rtp(97, 1, 1000))
|
|
86
|
+
assert clock.timing() == {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_the_playback_offer_names_both_extensions_in_each_media_section():
|
|
90
|
+
sdp = (
|
|
91
|
+
"v=0\r\nm=audio 9 RTP/SAVPF 8\r\na=mid:0\r\na=rtpmap:8 PCMA/8000\r\n"
|
|
92
|
+
"m=video 9 RTP/SAVPF 96 97\r\na=mid:1\r\n"
|
|
93
|
+
"m=application 9 SCTP webrtc-datachannel\r\na=mid:2\r\n"
|
|
94
|
+
)
|
|
95
|
+
out = offer_with_frame_times(sdp)
|
|
96
|
+
for mid in ("a=mid:0", "a=mid:1"):
|
|
97
|
+
after = out.split(mid + "\r\n")[1]
|
|
98
|
+
assert after.startswith(
|
|
99
|
+
f"a=extmap:{EXT_TRANSPORT_CC_ID} {EXT_TRANSPORT_CC}\r\n"
|
|
100
|
+
f"a=extmap:{EXT_FRAME_TIME_ID} {EXT_FRAME_TIME}\r\n"
|
|
101
|
+
)
|
|
102
|
+
assert "a=rtcp-fb:97 transport-cc" in out
|
|
103
|
+
assert out.split("a=mid:2")[1].count("extmap") == 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_the_wpayload_filter_keeps_the_two_extmaps_once_as_the_app_does():
|
|
107
|
+
sdp = offer_with_frame_times(
|
|
108
|
+
"v=0\r\ns=-\r\nm=audio 9 RTP/SAVPF 8\r\na=mid:0\r\na=rtpmap:8 PCMA/8000\r\n"
|
|
109
|
+
"a=extmap:9 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n"
|
|
110
|
+
"m=video 9 RTP/SAVPF 97\r\na=mid:1\r\na=rtpmap:97 H265/90000\r\n"
|
|
111
|
+
)
|
|
112
|
+
out = compress_sdp_req("dev", sdp)
|
|
113
|
+
assert out.count(EXT_TRANSPORT_CC) == 1 and out.count(EXT_FRAME_TIME) == 1
|
|
114
|
+
assert "ssrc-audio-level" not in out
|
|
115
|
+
# A live offer names neither, and comes out as it always did.
|
|
116
|
+
assert "extmap" not in compress_sdp_req("dev", sdp.replace("a=extmap", "a=x"))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _Sock:
|
|
120
|
+
def __init__(self):
|
|
121
|
+
self.sent = []
|
|
122
|
+
|
|
123
|
+
def sendto(self, pkt, addr):
|
|
124
|
+
self.sent.append(pkt)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_the_hold_reads_the_clock_and_forwards_nothing_after_the_end():
|
|
128
|
+
sock, lines = _Sock(), []
|
|
129
|
+
hold = _MediaHold()
|
|
130
|
+
hold.release()
|
|
131
|
+
hold.send(sock, _rtp(97, 1, 1000, T0), 5000, log=lines.append)
|
|
132
|
+
hold.send(sock, _rtp(97, 2, 5500, T0 + 200), 5000, log=lines.append)
|
|
133
|
+
for seq in (3, 4):
|
|
134
|
+
hold.send(sock, _rtp(97, seq, 0, FRAME_TIME_END), 5000, log=lines.append)
|
|
135
|
+
assert len(sock.sent) == 2
|
|
136
|
+
assert hold.clock.ended
|
|
137
|
+
assert lines.count("bridge: the camera marked the end of the recording") == 1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_a_playback_serve_keeps_the_camera_s_timestamps():
|
|
141
|
+
from aidot_cameras.camera import client
|
|
142
|
+
|
|
143
|
+
def argv(**kw):
|
|
144
|
+
return client._build_sdes_serve_cmd(
|
|
145
|
+
sdp_path="in.sdp", output_path="out.ts", **kw
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not client._SERVE_ARRIVAL_TS:
|
|
149
|
+
pytest.skip("arrival stamping is off in this environment")
|
|
150
|
+
assert "-use_wallclock_as_timestamps" in argv()
|
|
151
|
+
assert "-use_wallclock_as_timestamps" not in argv(keep_probe_packets=True)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="needs ffmpeg")
|
|
155
|
+
def test_the_retime_command_stretches_video_and_places_the_data_stream(tmp_path):
|
|
156
|
+
src, dst = tmp_path / "in.ts", tmp_path / "out.ts"
|
|
157
|
+
subprocess.run(
|
|
158
|
+
[
|
|
159
|
+
"ffmpeg",
|
|
160
|
+
"-loglevel",
|
|
161
|
+
"error",
|
|
162
|
+
"-y",
|
|
163
|
+
"-f",
|
|
164
|
+
"lavfi",
|
|
165
|
+
"-i",
|
|
166
|
+
"testsrc=size=64x64:rate=20:duration=3",
|
|
167
|
+
"-f",
|
|
168
|
+
"lavfi",
|
|
169
|
+
"-i",
|
|
170
|
+
"sine=frequency=440:sample_rate=8000:duration=3",
|
|
171
|
+
"-c:v",
|
|
172
|
+
"libx264",
|
|
173
|
+
"-c:a",
|
|
174
|
+
"pcm_alaw",
|
|
175
|
+
"-f",
|
|
176
|
+
"mpegts",
|
|
177
|
+
str(src),
|
|
178
|
+
],
|
|
179
|
+
check=True,
|
|
180
|
+
)
|
|
181
|
+
timing = {"video": (4.0, 0.0), "audio": (1.0, 2.0)}
|
|
182
|
+
subprocess.run(retime_argv("ffmpeg", str(src), str(dst), timing), check=True)
|
|
183
|
+
out = subprocess.run(
|
|
184
|
+
[
|
|
185
|
+
"ffprobe",
|
|
186
|
+
"-v",
|
|
187
|
+
"error",
|
|
188
|
+
"-show_entries",
|
|
189
|
+
"stream=codec_type,start_time,duration",
|
|
190
|
+
"-of",
|
|
191
|
+
"csv=p=0",
|
|
192
|
+
str(dst),
|
|
193
|
+
],
|
|
194
|
+
capture_output=True,
|
|
195
|
+
text=True,
|
|
196
|
+
check=True,
|
|
197
|
+
).stdout.split()
|
|
198
|
+
rows = {r.split(",")[0]: [float(x) for x in r.split(",")[1:]] for r in out}
|
|
199
|
+
assert rows["video"][1] == pytest.approx(12.0, abs=0.3) # 3 s x4
|
|
200
|
+
# MPEG-TS has no type for A-law: like an SDES capture, it is a data stream.
|
|
201
|
+
sound = rows.get("audio") or rows["data"]
|
|
202
|
+
assert sound[0] - rows["video"][0] == pytest.approx(2.0, abs=0.1)
|
|
203
|
+
assert os.path.getsize(dst) > 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_a_stray_second_video_type_does_not_decide_the_timing():
|
|
207
|
+
clock = PlaybackClock()
|
|
208
|
+
for i in range(50):
|
|
209
|
+
clock.observe(_rtp(97, i, 1000 + i * 4500, T0 + i * 200))
|
|
210
|
+
for i in range(3): # a few packets of another type, its own pace
|
|
211
|
+
clock.observe(_rtp(96, i, 5000 + i * 90000, T0 + i * 1000))
|
|
212
|
+
assert clock.timing()["video"][0] == pytest.approx(4.0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_the_offer_finds_sections_by_media_type_not_by_mid():
|
|
216
|
+
sdp = (
|
|
217
|
+
"v=0\r\nm=video 9 RTP/SAVPF 97\r\na=mid:20\r\n"
|
|
218
|
+
"m=audio 9 RTP/SAVPF 8\r\na=mid:video\r\n"
|
|
219
|
+
"m=application 9 SCTP webrtc-datachannel\r\na=mid:1\r\n"
|
|
220
|
+
)
|
|
221
|
+
out = offer_with_frame_times(sdp)
|
|
222
|
+
assert out.count(f"a=extmap:{EXT_FRAME_TIME_ID}") == 2
|
|
223
|
+
assert "a=rtcp-fb:97 transport-cc" in out
|
|
224
|
+
assert "a=rtcp-fb:8 transport-cc" not in out
|
|
225
|
+
assert out.split("m=application")[1].count("extmap") == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_log_redaction.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_upstream.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc64 → python_aidot_cameras-1.0.0rc65}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|