python-aidot-cameras 1.0.0rc2__tar.gz → 1.0.0rc3__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.0rc2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc3}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/README.md +1 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/protocol.py +71 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-1.0.0rc3/tests/test_direct_ts_keyframe_signal.py +140 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_direct_ts_serve.py +11 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/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.0rc3
|
|
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
|
|
@@ -418,6 +418,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
418
418
|
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
419
419
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
420
420
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
421
|
+
| `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
|
|
421
422
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
422
423
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
423
424
|
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
@@ -378,6 +378,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
378
378
|
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
379
379
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
380
380
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
381
|
+
| `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
|
|
381
382
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
382
383
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
383
384
|
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -1058,6 +1058,11 @@ class _DirectTsServer:
|
|
|
1058
1058
|
self._pmt: "Optional[bytes]" = None
|
|
1059
1059
|
self._pmt_pid: "Optional[int]" = None
|
|
1060
1060
|
self._synced = False # has the current consumer been given a start
|
|
1061
|
+
# Set by the mux immediately before it writes a video keyframe, so a new
|
|
1062
|
+
# consumer can be spliced where the picture is actually decodable.
|
|
1063
|
+
self._kf_pending = False
|
|
1064
|
+
self._kf_ever = False
|
|
1065
|
+
self._waiting_since: "Optional[float]" = None
|
|
1061
1066
|
|
|
1062
1067
|
@property
|
|
1063
1068
|
def port(self) -> int:
|
|
@@ -1141,6 +1146,51 @@ class _DirectTsServer:
|
|
|
1141
1146
|
except (IndexError, ValueError):
|
|
1142
1147
|
return
|
|
1143
1148
|
|
|
1149
|
+
#: How long a new consumer may wait for a keyframe signal before the old
|
|
1150
|
+
#: PID-blind random-access test is allowed to start it anyway. The signal is
|
|
1151
|
+
#: the correct trigger, but a stream that never sends one must not hang
|
|
1152
|
+
#: forever - gating solely on a video-pid flag served ZERO bytes when tried
|
|
1153
|
+
#: (2026-08-29), because the muxer does not set it.
|
|
1154
|
+
_LEGACY_SYNC_AFTER_S = 5.0
|
|
1155
|
+
|
|
1156
|
+
def mark_keyframe(self) -> None:
|
|
1157
|
+
"""Tell the server the next bytes written begin a video keyframe.
|
|
1158
|
+
|
|
1159
|
+
The mux knows this and used to throw it away: it pulls ``(data, ts, kf)``
|
|
1160
|
+
off the queue and ``kf`` came from ``_h264_has_keyframe`` at the tap.
|
|
1161
|
+
Everything downstream then tried to rediscover it from the container,
|
|
1162
|
+
which does not carry it - PyAV's mpegts muxer does not set
|
|
1163
|
+
``random_access_indicator`` on the video PID in this stream, so a sync
|
|
1164
|
+
gated on that flag never fires at all.
|
|
1165
|
+
|
|
1166
|
+
Splicing a consumer anywhere else hands it video mid-GOP referencing an
|
|
1167
|
+
SPS/PPS it never received, which decodes as "non-existing PPS 0
|
|
1168
|
+
referenced": a video track that never produces a frame.
|
|
1169
|
+
"""
|
|
1170
|
+
if not self._kf_ever:
|
|
1171
|
+
_LOGGER.debug("direct-ts: first keyframe signal reached the server")
|
|
1172
|
+
self._kf_ever = True
|
|
1173
|
+
self._kf_pending = True
|
|
1174
|
+
|
|
1175
|
+
def _legacy_sync_ok(self, pkt: bytes) -> bool:
|
|
1176
|
+
"""The pre-signal behaviour, allowed only after a grace period.
|
|
1177
|
+
|
|
1178
|
+
Kept because a source that never calls ``mark_keyframe`` must still be
|
|
1179
|
+
servable. Deliberately NOT the first choice: this test accepts a
|
|
1180
|
+
random-access point on ANY pid, and AAC audio sets that flag ~50x/s, so
|
|
1181
|
+
before the signal existed a late joiner was spliced onto audio and never
|
|
1182
|
+
received a decodable picture.
|
|
1183
|
+
"""
|
|
1184
|
+
if self._kf_ever:
|
|
1185
|
+
return False # this source signals; wait for the real thing
|
|
1186
|
+
if not self._is_random_access(pkt):
|
|
1187
|
+
return False
|
|
1188
|
+
now = time.monotonic()
|
|
1189
|
+
if self._waiting_since is None:
|
|
1190
|
+
self._waiting_since = now
|
|
1191
|
+
return False
|
|
1192
|
+
return (now - self._waiting_since) >= self._LEGACY_SYNC_AFTER_S
|
|
1193
|
+
|
|
1144
1194
|
def write(self, b: bytes) -> int:
|
|
1145
1195
|
data = self._tail + b if self._tail else b
|
|
1146
1196
|
out = bytearray()
|
|
@@ -1164,7 +1214,11 @@ class _DirectTsServer:
|
|
|
1164
1214
|
self._pmt = pkt
|
|
1165
1215
|
if self._synced:
|
|
1166
1216
|
out += pkt
|
|
1167
|
-
elif self.
|
|
1217
|
+
elif self._kf_pending or self._legacy_sync_ok(pkt):
|
|
1218
|
+
_LOGGER.debug(
|
|
1219
|
+
"direct-ts: spliced a consumer via=%s signals=%s pid=%s",
|
|
1220
|
+
"keyframe-signal" if self._kf_pending else "legacy-rai",
|
|
1221
|
+
self._kf_ever, pid)
|
|
1168
1222
|
# Start this consumer here: tables first, then the keyframe.
|
|
1169
1223
|
if self._pat is not None:
|
|
1170
1224
|
out += self._pat
|
|
@@ -1172,6 +1226,8 @@ class _DirectTsServer:
|
|
|
1172
1226
|
out += self._pmt
|
|
1173
1227
|
out += pkt
|
|
1174
1228
|
self._synced = True
|
|
1229
|
+
self._kf_pending = False
|
|
1230
|
+
self._waiting_since = None
|
|
1175
1231
|
self._tail = data[i:] if i < n else b""
|
|
1176
1232
|
|
|
1177
1233
|
with self._lock:
|
|
@@ -1568,6 +1624,16 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1568
1624
|
def close(self):
|
|
1569
1625
|
self._f.close()
|
|
1570
1626
|
|
|
1627
|
+
def mark_keyframe(self):
|
|
1628
|
+
"""Forward the keyframe signal when the sink understands it.
|
|
1629
|
+
|
|
1630
|
+
The sink is a _DirectTsServer on the direct path and an os pipe to
|
|
1631
|
+
ffmpeg otherwise; only the former can use this.
|
|
1632
|
+
"""
|
|
1633
|
+
fn = getattr(self._f, "mark_keyframe", None)
|
|
1634
|
+
if fn is not None:
|
|
1635
|
+
fn()
|
|
1636
|
+
|
|
1571
1637
|
wsink = _FailFlagWriter(out_fileobj)
|
|
1572
1638
|
try:
|
|
1573
1639
|
# max_interleave_delta (default 10s of stream time): with an AAC stream
|
|
@@ -1659,6 +1725,10 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
|
1659
1725
|
pkt.stream = vs
|
|
1660
1726
|
pkt.pts, pkt.dts = video_pts_dts(_ts_state, _rel, _slack)
|
|
1661
1727
|
pkt.time_base = Fraction(1, 90000)
|
|
1728
|
+
if kf:
|
|
1729
|
+
# Announce it BEFORE muxing: the bytes this produces are where a
|
|
1730
|
+
# new consumer can safely be spliced, and only we know that.
|
|
1731
|
+
wsink.mark_keyframe()
|
|
1662
1732
|
try:
|
|
1663
1733
|
out.mux(pkt)
|
|
1664
1734
|
progress[0] = _t.monotonic()
|
|
@@ -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.0rc3"
|
|
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.0rc3
|
|
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
|
|
@@ -418,6 +418,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
418
418
|
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
419
419
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
420
420
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
421
|
+
| `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
|
|
421
422
|
| `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
422
423
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
|
|
423
424
|
| `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
|
|
@@ -102,6 +102,7 @@ tests/test_deferred_hardening.py
|
|
|
102
102
|
tests/test_device_cmd_seq_match.py
|
|
103
103
|
tests/test_device_login_guard.py
|
|
104
104
|
tests/test_device_user_info_cache.py
|
|
105
|
+
tests/test_direct_ts_keyframe_signal.py
|
|
105
106
|
tests/test_direct_ts_serve.py
|
|
106
107
|
tests/test_dormant_camera_cleanup.py
|
|
107
108
|
tests/test_drain_is_cancellable.py
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""A late joiner is spliced where the mux says a keyframe starts.
|
|
2
|
+
|
|
3
|
+
`_DirectTsServer` holds a new consumer until it can hand over a decodable start.
|
|
4
|
+
It used to decide that from the container: any TS packet with the adaptation
|
|
5
|
+
field's random_access_indicator set. That flag is per-PID, and AAC audio sets it
|
|
6
|
+
roughly 50x/s against a video keyframe every ~2 s, so a late joiner was spliced
|
|
7
|
+
onto audio and received video mid-GOP against an SPS/PPS it never got -
|
|
8
|
+
"non-existing PPS 0 referenced", a video track that never produces a frame.
|
|
9
|
+
Only a consumer present from byte 0 escaped, and go2rtc pulls lazily when a
|
|
10
|
+
viewer connects, so it is always late.
|
|
11
|
+
|
|
12
|
+
Gating on the flag being set on the VIDEO pid instead was tried on hardware
|
|
13
|
+
(2026-08-29) and served ZERO bytes: PyAV's mpegts muxer does not set it on video
|
|
14
|
+
in this stream, so the condition never became true. That attempt was reverted.
|
|
15
|
+
|
|
16
|
+
The mux already knows. It pulls ``(data, ts, kf)`` off the queue, where ``kf``
|
|
17
|
+
came from ``_h264_has_keyframe`` at the tap, and then threw it away. It now
|
|
18
|
+
announces it, and the legacy container test survives only as a bounded fallback
|
|
19
|
+
for a source that never signals - so a stream that does not use the signal can
|
|
20
|
+
still be served rather than hanging forever.
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
27
|
+
|
|
28
|
+
from aidot_cameras.camera.protocol import _DirectTsServer
|
|
29
|
+
|
|
30
|
+
VIDEO_PID = 0x0100
|
|
31
|
+
AUDIO_PID = 0x0101
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ts(pid: int, *, rai: bool = False) -> bytes:
|
|
35
|
+
"""One 188-byte TS packet, optionally flagging a random-access point."""
|
|
36
|
+
hdr = bytearray(4)
|
|
37
|
+
hdr[0] = 0x47
|
|
38
|
+
hdr[1] = (pid >> 8) & 0x1F
|
|
39
|
+
hdr[2] = pid & 0xFF
|
|
40
|
+
hdr[3] = 0x30 if rai else 0x10 # adaptation+payload, or payload only
|
|
41
|
+
body = bytes([1, 0x40]) if rai else b"" # adaptation_field_length=1, RAI
|
|
42
|
+
pkt = bytes(hdr) + body + b"\xde\xad"
|
|
43
|
+
return pkt + b"\xff" * (188 - len(pkt))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _server():
|
|
47
|
+
srv = _DirectTsServer.__new__(_DirectTsServer)
|
|
48
|
+
srv._tail = b""
|
|
49
|
+
srv._pat = b"P" * 188
|
|
50
|
+
srv._pmt = b"M" * 188
|
|
51
|
+
srv._pmt_pid = 0x1000
|
|
52
|
+
srv._synced = False
|
|
53
|
+
srv._kf_pending = False
|
|
54
|
+
srv._kf_ever = False
|
|
55
|
+
srv._waiting_since = None
|
|
56
|
+
srv._lock = threading.Lock()
|
|
57
|
+
sent = bytearray()
|
|
58
|
+
|
|
59
|
+
class _Sock:
|
|
60
|
+
def sendall(self, b):
|
|
61
|
+
sent.extend(b)
|
|
62
|
+
|
|
63
|
+
srv._client = _Sock()
|
|
64
|
+
return srv, sent
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_audio_random_access_does_not_splice_a_signalling_source():
|
|
68
|
+
"""The defect: audio RAI used to start the consumer mid-video-GOP."""
|
|
69
|
+
srv, sent = _server()
|
|
70
|
+
srv.mark_keyframe() # source signals, so it is trusted
|
|
71
|
+
srv._kf_pending = False # ...but no keyframe pending yet
|
|
72
|
+
srv.write(_ts(AUDIO_PID, rai=True) * 20)
|
|
73
|
+
assert not srv._synced
|
|
74
|
+
assert bytes(sent) == b""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_keyframe_signal_splices_with_tables_first():
|
|
78
|
+
srv, sent = _server()
|
|
79
|
+
srv.write(_ts(AUDIO_PID, rai=True) * 3)
|
|
80
|
+
assert not srv._synced
|
|
81
|
+
srv.mark_keyframe()
|
|
82
|
+
srv.write(_ts(VIDEO_PID))
|
|
83
|
+
assert srv._synced
|
|
84
|
+
out = bytes(sent)
|
|
85
|
+
assert out[:188] == b"P" * 188, "PAT first"
|
|
86
|
+
assert out[188:376] == b"M" * 188, "then PMT"
|
|
87
|
+
assert len(out) == 3 * 188, "then the keyframe's packet"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_pending_flag_is_consumed_so_a_later_consumer_waits_again():
|
|
91
|
+
srv, _ = _server()
|
|
92
|
+
srv.mark_keyframe()
|
|
93
|
+
srv.write(_ts(VIDEO_PID))
|
|
94
|
+
assert srv._synced and not srv._kf_pending
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_everything_flows_once_synced():
|
|
98
|
+
srv, sent = _server()
|
|
99
|
+
srv.mark_keyframe()
|
|
100
|
+
srv.write(_ts(VIDEO_PID))
|
|
101
|
+
before = len(bytes(sent))
|
|
102
|
+
srv.write(_ts(AUDIO_PID) + _ts(VIDEO_PID))
|
|
103
|
+
assert len(bytes(sent)) == before + 2 * 188
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_non_signalling_source_still_starts_after_the_grace_period():
|
|
107
|
+
"""A writer that never calls mark_keyframe must not hang forever.
|
|
108
|
+
|
|
109
|
+
This is the regression that gating solely on a video-pid flag caused:
|
|
110
|
+
zero bytes, which is worse than the bug it was fixing.
|
|
111
|
+
"""
|
|
112
|
+
srv, sent = _server()
|
|
113
|
+
srv.write(_ts(AUDIO_PID, rai=True))
|
|
114
|
+
assert not srv._synced, "must wait out the grace period first"
|
|
115
|
+
srv._waiting_since -= _DirectTsServer._LEGACY_SYNC_AFTER_S + 1
|
|
116
|
+
srv.write(_ts(AUDIO_PID, rai=True))
|
|
117
|
+
assert srv._synced, "a non-signalling source must still be servable"
|
|
118
|
+
assert bytes(sent), "and must actually receive bytes"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_a_signalling_source_never_falls_back():
|
|
122
|
+
"""Once a source has signalled, the container test must never win."""
|
|
123
|
+
srv, _ = _server()
|
|
124
|
+
srv.mark_keyframe()
|
|
125
|
+
srv._kf_pending = False
|
|
126
|
+
srv._waiting_since = 0.0 # grace long expired
|
|
127
|
+
assert srv._legacy_sync_ok(_ts(AUDIO_PID, rai=True)) is False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_mux_forwards_the_signal_only_to_sinks_that_understand_it():
|
|
131
|
+
"""The fail-flag wrapper must not raise on an os pipe, which has no hook."""
|
|
132
|
+
import inspect
|
|
133
|
+
|
|
134
|
+
from aidot_cameras.camera import protocol
|
|
135
|
+
|
|
136
|
+
src = inspect.getsource(protocol._dtls_av_mux_run)
|
|
137
|
+
assert "wsink.mark_keyframe()" in src, "the mux must announce keyframes"
|
|
138
|
+
assert 'getattr(self._f, "mark_keyframe", None)' in src, (
|
|
139
|
+
"forwarding must be optional - the ffmpeg path's sink is a pipe"
|
|
140
|
+
)
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_direct_ts_serve.py
RENAMED
|
@@ -39,6 +39,13 @@ this server or in go2rtc: a `proc.returncode` dereference when there is no
|
|
|
39
39
|
ffmpeg process, binding an ephemeral port instead of the advertised one when no
|
|
40
40
|
relay holds it, and the missing join-awareness below. The server itself
|
|
41
41
|
describes cleanly against real go2rtc (`rc: 0, h264,video`).
|
|
42
|
+
|
|
43
|
+
These drive the server directly, so they must announce keyframes the way the
|
|
44
|
+
mux does (`mark_keyframe`). The container's random_access_indicator is no longer
|
|
45
|
+
the primary trigger: it is per-PID, AAC audio sets it ~50x/s, and syncing on it
|
|
46
|
+
spliced a late joiner onto audio with video mid-GOP. It survives only as a
|
|
47
|
+
bounded fallback for a writer that never signals - covered in
|
|
48
|
+
test_direct_ts_keyframe_signal.py.
|
|
42
49
|
"""
|
|
43
50
|
import os
|
|
44
51
|
import socket
|
|
@@ -133,6 +140,7 @@ def test_a_consumer_gets_an_http_response_then_the_media():
|
|
|
133
140
|
if srv.has_consumer():
|
|
134
141
|
break
|
|
135
142
|
time.sleep(0.05)
|
|
143
|
+
srv.mark_keyframe() # the mux announces it
|
|
136
144
|
srv.write(_ts(0x100, pusi=1, rai=1)) # keyframe: releases the join
|
|
137
145
|
for _ in range(10):
|
|
138
146
|
srv.write(_ts(0x100, pusi=1))
|
|
@@ -255,6 +263,7 @@ def test_a_late_consumer_gets_tables_then_a_keyframe():
|
|
|
255
263
|
sock, head = _get(srv.port, read_bytes=1)
|
|
256
264
|
assert _wait_consumer(srv)
|
|
257
265
|
body = head.partition(b"\r\n\r\n")[2]
|
|
266
|
+
srv.mark_keyframe() # the mux announces it
|
|
258
267
|
srv.write(_ts(0x100, pusi=1, rai=1)) # the next keyframe
|
|
259
268
|
srv.write(_ts(0x100, pusi=1))
|
|
260
269
|
body += _drain(sock, n=4)
|
|
@@ -276,6 +285,7 @@ def test_once_synced_it_keeps_forwarding():
|
|
|
276
285
|
srv.write(_pat()); srv.write(_ts(0x1000, pusi=1))
|
|
277
286
|
sock, head = _get(srv.port, read_bytes=1)
|
|
278
287
|
assert _wait_consumer(srv)
|
|
288
|
+
srv.mark_keyframe()
|
|
279
289
|
srv.write(_ts(0x100, pusi=1, rai=1))
|
|
280
290
|
for _ in range(6):
|
|
281
291
|
srv.write(_ts(0x100, pusi=1))
|
|
@@ -295,6 +305,7 @@ def test_a_reconnecting_consumer_resyncs():
|
|
|
295
305
|
srv.write(_pat()); srv.write(_ts(0x1000, pusi=1))
|
|
296
306
|
s1, _ = _get(srv.port, read_bytes=1)
|
|
297
307
|
assert _wait_consumer(srv)
|
|
308
|
+
srv.mark_keyframe()
|
|
298
309
|
srv.write(_ts(0x100, pusi=1, rai=1))
|
|
299
310
|
s1.close()
|
|
300
311
|
time.sleep(0.3)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/ble_gateway.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/client.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/go2rtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/hwaccel.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/models.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_events.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_listing.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sdes.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sdes_open.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/tutk.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/webrtc.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/credentials.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/device_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_pt_kinds.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_sdp_video_pt.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_audio_mux_offline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_response_parse.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_response_router.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_detection.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_skip_turn.py
RENAMED
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_bitrate_campaign_arms.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_bridge_state_sentinels.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|