python-aidot-cameras 1.0.0b31__tar.gz → 1.0.0b33__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.0b31/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b33}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/client.py +38 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sdes_open.py +270 -14
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/SOURCES.txt +3 -0
- python_aidot_cameras-1.0.0b33/tests/test_answer_pt_kinds.py +141 -0
- python_aidot_cameras-1.0.0b33/tests/test_serve_input_timeout.py +65 -0
- python_aidot_cameras-1.0.0b33/tests/test_stall_nudge_policy.py +51 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/README.md +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_offer_bandwidth_line.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_tmmbr_bridge.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/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.0b33
|
|
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.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/client.py
RENAMED
|
@@ -566,6 +566,36 @@ _SERVE_REORDER_QUEUE = int(os.environ.get("AIDOT_SERVE_REORDER_QUEUE", "500"))
|
|
|
566
566
|
#: failure this is meant to prevent.
|
|
567
567
|
_SERVE_MAX_DELAY_US = int(os.environ.get("AIDOT_SERVE_MAX_DELAY_US", "500000"))
|
|
568
568
|
|
|
569
|
+
#: How long (seconds) the serve ffmpeg waits with NO packets on any RTP socket
|
|
570
|
+
#: before its SDP input errors out ("Error during demuxing: Operation timed
|
|
571
|
+
#: out") and the serve dies. ffmpeg's own default is 10 s (READ_PACKET_TIMEOUT_S,
|
|
572
|
+
#: applied through the sdp demuxer's ``listen_timeout`` option). Measured on an
|
|
573
|
+
#: A001064 (2026-08-24/25): the camera episodically stops transmitting for
|
|
574
|
+
#: >=10 s mid-session, and every reopen afterwards succeeds immediately, so the
|
|
575
|
+
#: link recovers on a 10-20 s scale. A mains camera therefore gets 30 s to ride
|
|
576
|
+
#: the gap out; a battery camera keeps ffmpeg's 10 s default, because its stops
|
|
577
|
+
#: are real sleeps that only a reopen (with its wake) ends - waiting longer just
|
|
578
|
+
#: freezes the picture. ``AIDOT_SERVE_INPUT_TIMEOUT_S`` overrides both.
|
|
579
|
+
_SERVE_INPUT_TIMEOUT_MAINS_S = 30
|
|
580
|
+
_SERVE_INPUT_TIMEOUT_BATTERY_S = 10
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _resolve_serve_input_timeout_s(is_battery: bool) -> int:
|
|
584
|
+
"""Seconds the serve's SDP input tolerates total silence before erroring.
|
|
585
|
+
|
|
586
|
+
Pure policy (unit-testable): the env override wins for every camera; else
|
|
587
|
+
mains rides out a transmit gap and battery keeps the fast default.
|
|
588
|
+
"""
|
|
589
|
+
_env = os.environ.get("AIDOT_SERVE_INPUT_TIMEOUT_S")
|
|
590
|
+
if _env:
|
|
591
|
+
try:
|
|
592
|
+
return max(1, int(_env))
|
|
593
|
+
except ValueError:
|
|
594
|
+
pass
|
|
595
|
+
return (_SERVE_INPUT_TIMEOUT_BATTERY_S if is_battery
|
|
596
|
+
else _SERVE_INPUT_TIMEOUT_MAINS_S)
|
|
597
|
+
|
|
598
|
+
|
|
569
599
|
#: How long to wait after a camera answers -50002 / -50015 ("no free session").
|
|
570
600
|
#: Both loops used to wait _MAX_DELAY (300s) on the belief that the camera
|
|
571
601
|
#: "releases slowly". Measured 2026-08-07 on an A001064: reopening 2s after a
|
|
@@ -630,6 +660,7 @@ def _build_sdes_serve_cmd(
|
|
|
630
660
|
audio_gain_db: float = -8.0,
|
|
631
661
|
push_video_only: bool = False,
|
|
632
662
|
video_decoder: Optional[list] = None,
|
|
663
|
+
input_timeout_s: Optional[int] = None,
|
|
633
664
|
) -> list:
|
|
634
665
|
"""Build the ffmpeg argv for the SDES bridge serve.
|
|
635
666
|
|
|
@@ -778,6 +809,13 @@ def _build_sdes_serve_cmd(
|
|
|
778
809
|
# before giving up on a gap rather than stalling the pipeline.
|
|
779
810
|
"-reorder_queue_size", str(_SERVE_REORDER_QUEUE),
|
|
780
811
|
"-max_delay", str(_SERVE_MAX_DELAY_US),
|
|
812
|
+
# Input silence tolerance: the sdp demuxer's ``listen_timeout`` doubles
|
|
813
|
+
# as its packet-read timeout (READ_PACKET_TIMEOUT_S, ffmpeg default
|
|
814
|
+
# 10 s). See _resolve_serve_input_timeout_s for the mains/battery
|
|
815
|
+
# policy. Input option, so it must sit before -i; None keeps ffmpeg's
|
|
816
|
+
# default (no behavior change for callers that do not opt in).
|
|
817
|
+
*(["-listen_timeout", str(int(input_timeout_s))]
|
|
818
|
+
if input_timeout_s is not None else []),
|
|
781
819
|
"-i", sdp_path,
|
|
782
820
|
*dest_args,
|
|
783
821
|
]
|
{python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -746,6 +746,77 @@ def video_pt_from_answer_sdp(sdp_text: str) -> Optional[int]:
|
|
|
746
746
|
return None
|
|
747
747
|
|
|
748
748
|
|
|
749
|
+
def answer_pt_kinds(sdp_text: str) -> dict:
|
|
750
|
+
"""Map each payload type in the camera's answer to the kind that owns it.
|
|
751
|
+
|
|
752
|
+
The bridge demuxes a BUNDLEd stream by RTP payload type, and until this
|
|
753
|
+
existed it did so from a fixed table: 96/97/98 video, 0/8 audio. That
|
|
754
|
+
table is wrong for this fleet. Measured 2026-08-26 across 107 A001064
|
|
755
|
+
opens, **15 of them (14%) negotiated H265 on payload type 0** -- announced
|
|
756
|
+
correctly in the answer as ``m=video ... 0`` with ``a=rtpmap:0 H265/90000``
|
|
757
|
+
-- and then sent 2668 full-size (1222 B) video packets on pt=0 while audio
|
|
758
|
+
ran normally on pt=8. The fixed table posted every one of those video
|
|
759
|
+
packets to ffmpeg's AUDIO loopback, so no video was ever observed, the 75 s
|
|
760
|
+
first-media wait ran out, and the serve launched into an empty stream and
|
|
761
|
+
exited: 82 s wasted, then a full reopen. The other 92 opens answered
|
|
762
|
+
``m=video ... 96`` and worked. Perfect separation, both directions.
|
|
763
|
+
|
|
764
|
+
The answer is the authority on this and is already in hand: PT numbering is
|
|
765
|
+
per-m-section by definition, so a section's fmt list names the kind of every
|
|
766
|
+
payload type in it. ``tests/test_answer_section_selection.py`` records the
|
|
767
|
+
same renumbering on the A000088 (PT 0 carrying H265), and the DTLS path
|
|
768
|
+
already selects sections by content for it; the SDES bridge was the one
|
|
769
|
+
place still trusting a static tuple.
|
|
770
|
+
|
|
771
|
+
A payload type claimed by BOTH an audio and a video section is dropped from
|
|
772
|
+
the map rather than guessed at -- an ambiguous answer is not better evidence
|
|
773
|
+
than the fallback table, and silently preferring one section would be the
|
|
774
|
+
same class of assumption this function exists to remove. Malformed m= lines
|
|
775
|
+
are skipped, never raised on: this feeds a media path.
|
|
776
|
+
"""
|
|
777
|
+
kinds: dict = {}
|
|
778
|
+
dropped: set = set()
|
|
779
|
+
for raw in (sdp_text or "").splitlines():
|
|
780
|
+
line = raw.strip()
|
|
781
|
+
if not line.startswith("m=audio") and not line.startswith("m=video"):
|
|
782
|
+
continue
|
|
783
|
+
parts = line.split()
|
|
784
|
+
if len(parts) < 4:
|
|
785
|
+
continue
|
|
786
|
+
kind = "video" if line.startswith("m=video") else "audio"
|
|
787
|
+
for tok in parts[3:]:
|
|
788
|
+
try:
|
|
789
|
+
pt = int(tok)
|
|
790
|
+
except ValueError:
|
|
791
|
+
continue
|
|
792
|
+
if not 0 <= pt <= 127:
|
|
793
|
+
continue
|
|
794
|
+
if kinds.get(pt, kind) != kind:
|
|
795
|
+
dropped.add(pt)
|
|
796
|
+
kinds[pt] = kind
|
|
797
|
+
for pt in dropped:
|
|
798
|
+
kinds.pop(pt, None)
|
|
799
|
+
return kinds
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def rewrite_rtp_payload_type(pkt: bytes, pt: int) -> bytes:
|
|
803
|
+
"""Return ``pkt`` with its RTP payload type replaced, marker bit preserved.
|
|
804
|
+
|
|
805
|
+
Used only where the bridge itself decrypted the packet (``_use_plain_rtp``),
|
|
806
|
+
so what is being edited is plaintext on its way to our own loopback -- never
|
|
807
|
+
a packet ffmpeg will authenticate, where a changed byte would fail the SRTP
|
|
808
|
+
auth tag. RTP byte 1 is ``M(1) | PT(7)`` (RFC 3550), so only the low seven
|
|
809
|
+
bits move.
|
|
810
|
+
|
|
811
|
+
A packet too short to hold that byte is returned untouched rather than
|
|
812
|
+
rejected: this is the media path, and a malformed packet is ffmpeg's to
|
|
813
|
+
discard, not the bridge's to raise on.
|
|
814
|
+
"""
|
|
815
|
+
if len(pkt) < 2:
|
|
816
|
+
return pkt
|
|
817
|
+
return pkt[:1] + bytes([(pkt[1] & 0x80) | (pt & 0x7F)]) + pkt[2:]
|
|
818
|
+
|
|
819
|
+
|
|
749
820
|
#: Video payload types the SDES answer template advertises (H264, H265).
|
|
750
821
|
_SDES_ANSWER_VIDEO_PTS = (96, 97)
|
|
751
822
|
|
|
@@ -1218,6 +1289,42 @@ def _classify_ffmpeg_exit(rc: int, teardown_requested: bool) -> int:
|
|
|
1218
1289
|
return logging.WARNING
|
|
1219
1290
|
|
|
1220
1291
|
|
|
1292
|
+
#: Mid-session stall nudge (see _stall_nudge_due): enabled by default;
|
|
1293
|
+
#: ``AIDOT_SDES_STALL_NUDGE=0`` turns it off and
|
|
1294
|
+
#: ``AIDOT_SDES_STALL_NUDGE_AFTER_S`` moves the trigger point.
|
|
1295
|
+
_STALL_NUDGE_ENABLED = os.environ.get("AIDOT_SDES_STALL_NUDGE", "1") != "0"
|
|
1296
|
+
_STALL_NUDGE_AFTER_S = float(
|
|
1297
|
+
os.environ.get("AIDOT_SDES_STALL_NUDGE_AFTER_S", "2.5"))
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _stall_nudge_due(
|
|
1301
|
+
*,
|
|
1302
|
+
silence_s: float,
|
|
1303
|
+
nudges_sent: int,
|
|
1304
|
+
since_last_nudge_s: float,
|
|
1305
|
+
stall_after_s: float = 2.5,
|
|
1306
|
+
max_nudges: int = 3,
|
|
1307
|
+
interval_s: float = 2.0,
|
|
1308
|
+
) -> bool:
|
|
1309
|
+
"""Whether the bridge should re-send AVIO LIVING for a mid-session stall.
|
|
1310
|
+
|
|
1311
|
+
The A001064 episodically stops transmitting mid-session with no teardown
|
|
1312
|
+
signal; every reopen afterwards succeeds immediately, so the camera is
|
|
1313
|
+
awake and answering handshakes seconds after it stopped (measured
|
|
1314
|
+
2026-08-24/25). LIVING is the message that starts media on a fresh
|
|
1315
|
+
session, so re-sending it on the live session is the cheapest possible
|
|
1316
|
+
revive attempt. Bounded (a few sends, spaced out) so a camera that is
|
|
1317
|
+
truly asleep or out of range is not spammed - the serve's input timeout
|
|
1318
|
+
and the keepalive reopen remain the unchanged fallback. Pure function so
|
|
1319
|
+
the policy is unit-testable without a live bridge.
|
|
1320
|
+
"""
|
|
1321
|
+
if silence_s <= stall_after_s:
|
|
1322
|
+
return False
|
|
1323
|
+
if nudges_sent >= max_nudges:
|
|
1324
|
+
return False
|
|
1325
|
+
return since_last_nudge_s >= interval_s
|
|
1326
|
+
|
|
1327
|
+
|
|
1221
1328
|
def _bridge_should_break(rc, teardown_requested: bool) -> bool:
|
|
1222
1329
|
"""Whether the bridge observe loop should end on this poll() result.
|
|
1223
1330
|
|
|
@@ -1623,7 +1730,7 @@ class _SdesOpenMixin:
|
|
|
1623
1730
|
SDP answer, writes it to a temp file, and launches ffmpeg to receive and
|
|
1624
1731
|
record the SRTP stream.
|
|
1625
1732
|
"""
|
|
1626
|
-
from .client import CameraMixin, _build_sdes_serve_cmd, _ffmpeg_path, _spawn_bg # lazy: break client<->sdes_open cycle
|
|
1733
|
+
from .client import CameraMixin, _build_sdes_serve_cmd, _ffmpeg_path, _resolve_serve_input_timeout_s, _spawn_bg # lazy: break client<->sdes_open cycle
|
|
1627
1734
|
import base64
|
|
1628
1735
|
import subprocess
|
|
1629
1736
|
|
|
@@ -4002,6 +4109,10 @@ class _SdesOpenMixin:
|
|
|
4002
4109
|
# list for the same reason as _first_video_pt: the serve-restart path
|
|
4003
4110
|
# reads it from a nested scope.
|
|
4004
4111
|
_answer_video_pt: list = [None]
|
|
4112
|
+
# PT -> "video"/"audio" from the camera's answer, read by the
|
|
4113
|
+
# bridge demux. Empty until the answer lands (the bridge starts
|
|
4114
|
+
# first), which is exactly when the static-tuple fallback applies.
|
|
4115
|
+
_answer_pt_kinds: dict = {}
|
|
4005
4116
|
# Same for audio: the ffmpeg SDP advertises BOTH PCMU (0) and PCMA (8),
|
|
4006
4117
|
# and ffmpeg binds the depacketizer to the first one listed - so the line
|
|
4007
4118
|
# has to be narrowed to the payload type the camera actually sends, the
|
|
@@ -4099,6 +4210,9 @@ class _SdesOpenMixin:
|
|
|
4099
4210
|
_trigger_peer = None # camera's real addr when relayed
|
|
4100
4211
|
_sdes_probe_received = False # True after first 0xC8 probe from camera
|
|
4101
4212
|
_last_hb_ts = 0.0 # time of last AVIO HEARTBEAT send
|
|
4213
|
+
_stall_nudges_sent = 0 # LIVING re-sends for the CURRENT stall
|
|
4214
|
+
_stall_last_nudge = 0.0 # wall time of the last stall nudge
|
|
4215
|
+
_stall_active = False # media had started, then went silent
|
|
4102
4216
|
# One-shot guard for the "ffmpeg exited" log below: while the held
|
|
4103
4217
|
# proc keeps reporting the same stale exit code across a
|
|
4104
4218
|
# teardown-window skip (see _bridge_should_break), only the first
|
|
@@ -4537,12 +4651,17 @@ class _SdesOpenMixin:
|
|
|
4537
4651
|
except Exception as _dw_e:
|
|
4538
4652
|
_status(f"SDES DC: DCEP_WAIT LIVING err: {_dw_e}")
|
|
4539
4653
|
|
|
4540
|
-
#
|
|
4541
|
-
#
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4654
|
+
# One LIVING wire-build, shared by the pre-probe retrigger
|
|
4655
|
+
# below and the mid-session stall nudge: resend the AVIO
|
|
4656
|
+
# LIVING(5376) that starts media, encrypted then plaintext
|
|
4657
|
+
# exactly like the original trigger.
|
|
4658
|
+
# Default-arg binding (not closure capture): re-defined each
|
|
4659
|
+
# tick, so the CURRENT trigger socket/addrs are bound at
|
|
4660
|
+
# definition time and B023 cannot bite.
|
|
4661
|
+
def _resend_avio_living(
|
|
4662
|
+
_tb=_trigger_bs,
|
|
4663
|
+
_tsrc=_trigger_bsrc,
|
|
4664
|
+
_tpeer=_trigger_peer) -> None:
|
|
4546
4665
|
import struct as _st_re2
|
|
4547
4666
|
import random as _r_re2
|
|
4548
4667
|
_re_ts = int(_time_br.time() * 1000)
|
|
@@ -4568,15 +4687,63 @@ class _SdesOpenMixin:
|
|
|
4568
4687
|
_rsz = len(_rp)
|
|
4569
4688
|
try:
|
|
4570
4689
|
_br_send_to_cam(
|
|
4571
|
-
|
|
4690
|
+
_tb,
|
|
4572
4691
|
bytes([0xC8, 0x00, _rsz >> 8, _rsz & 0xFF]) + _rp,
|
|
4573
|
-
|
|
4574
|
-
|
|
4692
|
+
_tsrc,
|
|
4693
|
+
_tpeer,
|
|
4575
4694
|
)
|
|
4576
4695
|
except Exception:
|
|
4577
4696
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_bridge_fn', exc_info=True)
|
|
4697
|
+
|
|
4698
|
+
# Periodic retrigger: resend AVIO LIVING every 2s until probe
|
|
4699
|
+
# received (camera acknowledged our trigger).
|
|
4700
|
+
if (_avio_living_sent
|
|
4701
|
+
and not _sdes_probe_received
|
|
4702
|
+
and _trigger_bs is not None
|
|
4703
|
+
and _time_br.time() - _last_trigger_ts >= 2.0):
|
|
4704
|
+
_resend_avio_living()
|
|
4578
4705
|
_last_trigger_ts = _time_br.time()
|
|
4579
4706
|
|
|
4707
|
+
# Mid-session stall nudge: the camera episodically stops
|
|
4708
|
+
# transmitting with no teardown signal (A001064, measured
|
|
4709
|
+
# 2026-08-24/25), the serve's input then times out and the
|
|
4710
|
+
# session dies - yet the camera answers a FRESH handshake
|
|
4711
|
+
# immediately afterwards. So before letting it die, re-send
|
|
4712
|
+
# the LIVING that starts media. The AVIO HEARTBEAT alone
|
|
4713
|
+
# does not do this: heartbeats kept flowing right through
|
|
4714
|
+
# measured stalls while the media stayed silent. Bounded by
|
|
4715
|
+
# _stall_nudge_due; the input timeout and the keepalive
|
|
4716
|
+
# reopen remain the unchanged fallback.
|
|
4717
|
+
if (_STALL_NUDGE_ENABLED
|
|
4718
|
+
and _sdes_probe_received
|
|
4719
|
+
and _trigger_bs is not None
|
|
4720
|
+
and _media_progress[0] > 0.0):
|
|
4721
|
+
_stall_silence = (
|
|
4722
|
+
_time_br.monotonic() - _media_progress[0])
|
|
4723
|
+
if _stall_silence <= _STALL_NUDGE_AFTER_S:
|
|
4724
|
+
if _stall_active:
|
|
4725
|
+
_stall_active = False
|
|
4726
|
+
_stall_nudges_sent = 0
|
|
4727
|
+
# Ask for an immediate IDR so recovery is not
|
|
4728
|
+
# hostage to the 30s PLI cadence.
|
|
4729
|
+
_bridge_fn._last_pli_ts = 0.0
|
|
4730
|
+
_status(
|
|
4731
|
+
"SDES: media resumed after stall"
|
|
4732
|
+
" - PLI re-armed")
|
|
4733
|
+
elif _stall_nudge_due(
|
|
4734
|
+
silence_s=_stall_silence,
|
|
4735
|
+
nudges_sent=_stall_nudges_sent,
|
|
4736
|
+
since_last_nudge_s=(
|
|
4737
|
+
_time_br.time() - _stall_last_nudge),
|
|
4738
|
+
stall_after_s=_STALL_NUDGE_AFTER_S):
|
|
4739
|
+
_stall_active = True
|
|
4740
|
+
_stall_nudges_sent += 1
|
|
4741
|
+
_stall_last_nudge = _time_br.time()
|
|
4742
|
+
_status(
|
|
4743
|
+
f"SDES: no media for {_stall_silence:.1f}s"
|
|
4744
|
+
f" - LIVING nudge {_stall_nudges_sent}/3")
|
|
4745
|
+
_resend_avio_living()
|
|
4746
|
+
|
|
4580
4747
|
for _bs in _rl:
|
|
4581
4748
|
try:
|
|
4582
4749
|
_bpkt, _bsrc = _bs.recvfrom(4096)
|
|
@@ -5506,7 +5673,55 @@ class _SdesOpenMixin:
|
|
|
5506
5673
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_bridge_fn', exc_info=True)
|
|
5507
5674
|
continue
|
|
5508
5675
|
_pt = _pt_byte & 0x7F
|
|
5509
|
-
|
|
5676
|
+
# The ANSWER decides which payload type is which
|
|
5677
|
+
# kind; the tuples below are only the fallback for a
|
|
5678
|
+
# packet that beats the answer in. See
|
|
5679
|
+
# answer_pt_kinds: this camera negotiates H265 on
|
|
5680
|
+
# pt=0 in ~14% of sessions and the fixed tuples sent
|
|
5681
|
+
# every one of those video streams to the AUDIO
|
|
5682
|
+
# loopback.
|
|
5683
|
+
_kind_answered = _answer_pt_kinds.get(_pt)
|
|
5684
|
+
_pt_out = _pt
|
|
5685
|
+
if _kind_answered == "video":
|
|
5686
|
+
_btgt, _lo_target, _kind = (
|
|
5687
|
+
_lo_video_port, _lo_v, "video"
|
|
5688
|
+
)
|
|
5689
|
+
# The bridge starts before the answer lands, so
|
|
5690
|
+
# a video packet on an unexpected PT can beat the
|
|
5691
|
+
# map in by a few hundred ms and latch as the
|
|
5692
|
+
# session's "first audio". Left alone that
|
|
5693
|
+
# narrows the serve's audio line to a payload
|
|
5694
|
+
# type no audio uses, and the mpegts mux then
|
|
5695
|
+
# withholds PAT/PMT waiting for a stream that
|
|
5696
|
+
# never produces - costing the picture, not just
|
|
5697
|
+
# the sound. Undo it and let real audio latch.
|
|
5698
|
+
if _first_audio_pt[0] == _pt:
|
|
5699
|
+
_first_audio_pt[0] = None
|
|
5700
|
+
_br_first_audio_logged = False
|
|
5701
|
+
_status(
|
|
5702
|
+
"bridge: pt=%d re-classified as video"
|
|
5703
|
+
" from the answer - clearing the audio"
|
|
5704
|
+
" latch it took first" % _pt)
|
|
5705
|
+
# Our serve SDP describes video as 96=H264 /
|
|
5706
|
+
# 97=H265 only, so a camera-numbered payload
|
|
5707
|
+
# type we do not advertise must be translated
|
|
5708
|
+
# into our numbering before ffmpeg sees it -
|
|
5709
|
+
# the same translation video_pt_from_answer_sdp
|
|
5710
|
+
# already performs for the SDP side ("the
|
|
5711
|
+
# camera's own numbering need not agree").
|
|
5712
|
+
# Gated on _use_plain_rtp because only there is
|
|
5713
|
+
# the bridge the decryptor and the packet
|
|
5714
|
+
# reaching ffmpeg plaintext; rewriting a byte
|
|
5715
|
+
# ffmpeg will itself authenticate breaks SRTP.
|
|
5716
|
+
if (_pt not in _SDP_VIDEO_PTS
|
|
5717
|
+
and _use_plain_rtp
|
|
5718
|
+
and _answer_video_pt[0] is not None):
|
|
5719
|
+
_pt_out = int(_answer_video_pt[0])
|
|
5720
|
+
elif _kind_answered == "audio":
|
|
5721
|
+
_btgt, _lo_target, _kind = (
|
|
5722
|
+
_lo_audio_port, _lo_a, "audio"
|
|
5723
|
+
)
|
|
5724
|
+
elif _pt in (96, 97, 98):
|
|
5510
5725
|
_btgt, _lo_target, _kind = (
|
|
5511
5726
|
_lo_video_port, _lo_v, "video"
|
|
5512
5727
|
)
|
|
@@ -5566,15 +5781,22 @@ class _SdesOpenMixin:
|
|
|
5566
5781
|
)
|
|
5567
5782
|
elif _kind == "video" and not _br_first_video_logged:
|
|
5568
5783
|
_br_first_video_logged = True
|
|
5569
|
-
|
|
5570
|
-
|
|
5784
|
+
# _pt_out, not _pt: everything downstream (the
|
|
5785
|
+
# serve SDP narrowing, _serve_video_pt's
|
|
5786
|
+
# "observed beats pinned") speaks OUR numbering,
|
|
5787
|
+
# and the wire PT is preserved in the log line.
|
|
5788
|
+
_first_video_pt[0] = _pt_out
|
|
5789
|
+
_status(
|
|
5790
|
+
f"bridge: first video RTP pt={_pt_out}"
|
|
5791
|
+
+ (f" (camera numbered it {_pt})"
|
|
5792
|
+
if _pt_out != _pt else ""))
|
|
5571
5793
|
# At INFO, not DEBUG: this is the one line that
|
|
5572
5794
|
# makes a bitrate figure comparable to another
|
|
5573
5795
|
# one, and it is emitted once per session.
|
|
5574
5796
|
_LOGGER.info(
|
|
5575
5797
|
"camera %s: video profile %s",
|
|
5576
5798
|
getattr(self, "device_id", "?"),
|
|
5577
|
-
describe_video_profile(
|
|
5799
|
+
describe_video_profile(_pt_out),
|
|
5578
5800
|
)
|
|
5579
5801
|
# Camera answers BUNDLE (all media on one 5-tuple),
|
|
5580
5802
|
# so the talk destination is the same address as
|
|
@@ -5799,6 +6021,9 @@ class _SdesOpenMixin:
|
|
|
5799
6021
|
if _nk_late:
|
|
5800
6022
|
_bridge_fn._nack_late_drops = getattr(
|
|
5801
6023
|
_bridge_fn, '_nack_late_drops', 0) + 1
|
|
6024
|
+
if _pt_out != _pt and _decrypted:
|
|
6025
|
+
_fwd_pkt = rewrite_rtp_payload_type(
|
|
6026
|
+
_fwd_pkt, _pt_out)
|
|
5802
6027
|
try:
|
|
5803
6028
|
if not _nk_late:
|
|
5804
6029
|
_lo_target.sendto(
|
|
@@ -6085,6 +6310,7 @@ class _SdesOpenMixin:
|
|
|
6085
6310
|
_early_nominated = bool(_cam_ice_ufrag and _cam_ice_pwd and _cam_ice_cands)
|
|
6086
6311
|
_media_deadline = time.monotonic() + _FIRST_MEDIA_WAIT_S
|
|
6087
6312
|
_media_wait_started = time.monotonic()
|
|
6313
|
+
_pt_kinds_read = False
|
|
6088
6314
|
|
|
6089
6315
|
def _report_first_media_stall(_waited_s, _cancelled=False):
|
|
6090
6316
|
"""Emit the one line that says why nothing arrived.
|
|
@@ -6150,6 +6376,34 @@ class _SdesOpenMixin:
|
|
|
6150
6376
|
_status(f"camera refused: ack {_code} {_desc}"
|
|
6151
6377
|
" - terminal, abandoning the first-media wait")
|
|
6152
6378
|
raise AidotCameraBusy(_code, _desc)
|
|
6379
|
+
if (not _pt_kinds_read and answer_fut is not None
|
|
6380
|
+
and answer_fut.done() and not answer_fut.cancelled()):
|
|
6381
|
+
# One-shot, here rather than in the answer-logging block
|
|
6382
|
+
# further down: that block runs AFTER this wait, which is
|
|
6383
|
+
# 75 s too late for the demux that the map exists to fix.
|
|
6384
|
+
_pt_kinds_read = True
|
|
6385
|
+
try:
|
|
6386
|
+
if answer_fut.exception() is None:
|
|
6387
|
+
_pk_sdp = (
|
|
6388
|
+
answer_fut.result() or {}).get("sdp", "") or ""
|
|
6389
|
+
if _pk_sdp:
|
|
6390
|
+
_answer_pt_kinds.update(answer_pt_kinds(_pk_sdp))
|
|
6391
|
+
_answer_video_pt[0] = video_pt_from_answer_sdp(
|
|
6392
|
+
_pk_sdp)
|
|
6393
|
+
_vpts = sorted(
|
|
6394
|
+
pt for pt, k in _answer_pt_kinds.items()
|
|
6395
|
+
if k == "video")
|
|
6396
|
+
if any(pt not in _SDP_VIDEO_PTS for pt in _vpts):
|
|
6397
|
+
_status(
|
|
6398
|
+
"answer numbers video on payload"
|
|
6399
|
+
" type(s) %s - demuxing by the answer,"
|
|
6400
|
+
" serving as our pt=%s"
|
|
6401
|
+
% (_vpts, _answer_video_pt[0]))
|
|
6402
|
+
except Exception:
|
|
6403
|
+
_LOGGER.debug(
|
|
6404
|
+
"camera %s: swallowed exception in %s",
|
|
6405
|
+
getattr(self, "device_id", "?"),
|
|
6406
|
+
'answer_pt_kinds', exc_info=True)
|
|
6153
6407
|
if (not _early_nominated and answer_fut is not None
|
|
6154
6408
|
and answer_fut.done() and not answer_fut.cancelled()):
|
|
6155
6409
|
# Read-only peek: the real await below still consumes this
|
|
@@ -6278,6 +6532,8 @@ class _SdesOpenMixin:
|
|
|
6278
6532
|
audio_gain_db=self._resolve_sdes_audio_gain_db(),
|
|
6279
6533
|
push_video_only=_push_video_only,
|
|
6280
6534
|
video_decoder=_video_decoder,
|
|
6535
|
+
input_timeout_s=_resolve_serve_input_timeout_s(
|
|
6536
|
+
bool(getattr(self, "is_battery_camera", False))),
|
|
6281
6537
|
)
|
|
6282
6538
|
# Audio matters even more than video here. A multi-PT m-line makes ffmpeg
|
|
6283
6539
|
# bind the depacketizer to the FIRST payload type and silently discard the
|
|
@@ -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.0b33"
|
|
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.0b33
|
|
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
|
|
@@ -77,6 +77,7 @@ python_aidot_cameras.egg-info/requires.txt
|
|
|
77
77
|
python_aidot_cameras.egg-info/top_level.txt
|
|
78
78
|
tests/test_aioice_compat.py
|
|
79
79
|
tests/test_alarm_event.py
|
|
80
|
+
tests/test_answer_pt_kinds.py
|
|
80
81
|
tests/test_answer_sdp_video_pt.py
|
|
81
82
|
tests/test_answer_section_selection.py
|
|
82
83
|
tests/test_audio_mux_offline.py
|
|
@@ -218,6 +219,7 @@ tests/test_sdp_payload_type_narrowing.py
|
|
|
218
219
|
tests/test_security_hardening.py
|
|
219
220
|
tests/test_self_peer_is_an_address_not_an_ip.py
|
|
220
221
|
tests/test_serializable_login_info.py
|
|
222
|
+
tests/test_serve_input_timeout.py
|
|
221
223
|
tests/test_serve_loss_tolerance.py
|
|
222
224
|
tests/test_serve_pt_respects_the_pin.py
|
|
223
225
|
tests/test_serve_relay.py
|
|
@@ -232,6 +234,7 @@ tests/test_sprop_unstable_public.py
|
|
|
232
234
|
tests/test_sprop_write_is_reported_truthfully.py
|
|
233
235
|
tests/test_srtp_rx_rekey.py
|
|
234
236
|
tests/test_srtp_tx_key_note.py
|
|
237
|
+
tests/test_stall_nudge_policy.py
|
|
235
238
|
tests/test_stall_report_says_what_the_answer_carried.py
|
|
236
239
|
tests/test_stall_report_says_whether_the_trigger_was_answered.py
|
|
237
240
|
tests/test_stall_report_separates_silence_from_undecryptable.py
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Demuxing the camera's media by the ANSWER's payload numbering, not a table.
|
|
2
|
+
|
|
3
|
+
The bridge splits a BUNDLEd stream into ffmpeg's audio and video loopbacks by
|
|
4
|
+
RTP payload type. It used to do that from a fixed table -- 96/97/98 video,
|
|
5
|
+
0/8 audio -- and that table is wrong for this fleet.
|
|
6
|
+
|
|
7
|
+
Measured 2026-08-26 across 107 A001064 opens on the live camera: 15 of them
|
|
8
|
+
(14%) negotiated H265 on payload type 0, announced it correctly in the answer
|
|
9
|
+
as ``m=video ... 0`` with ``a=rtpmap:0 H265/90000``, and then sent 2668
|
|
10
|
+
full-size (1222 B) video packets on pt=0 while audio ran normally on pt=8
|
|
11
|
+
(verified on the wire, tshark by SSRC: two streams, 0x29d9/pt=0 video and
|
|
12
|
+
0x29d7/pt=8 audio). The fixed table posted every one of those video packets to
|
|
13
|
+
the AUDIO loopback, so no video was ever observed, the 75 s first-media wait
|
|
14
|
+
expired, and the serve launched into an empty stream and exited -- 82 s, then a
|
|
15
|
+
full reopen. The other 92 opens answered ``m=video ... 96`` and worked. Perfect
|
|
16
|
+
separation in both directions.
|
|
17
|
+
|
|
18
|
+
The same renumbering is already on record for the A000088 in
|
|
19
|
+
``test_answer_section_selection.py`` (PT 0 carrying H265), where the DTLS path
|
|
20
|
+
handles it by selecting sections by content. The SDES bridge was the one place
|
|
21
|
+
left trusting a static tuple.
|
|
22
|
+
|
|
23
|
+
The m-lines below are transcribed from the live log; the ``a=rtpmap:0
|
|
24
|
+
H265/90000`` line is inferred from ``video_pt_from_answer_sdp`` returning 97 on
|
|
25
|
+
every one of those 15 sessions (logged as "the camera's answer (97)"), which is
|
|
26
|
+
reachable only through the H265 branch of ``_SDP_VIDEO_PT_BY_CODEC``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
33
|
+
|
|
34
|
+
from aidot_cameras.camera.sdes_open import (
|
|
35
|
+
answer_pt_kinds,
|
|
36
|
+
rewrite_rtp_payload_type,
|
|
37
|
+
video_pt_from_answer_sdp,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# The 92-of-107 shape: ordinary 3-section answer, video on 96.
|
|
41
|
+
_HEALTHY = (
|
|
42
|
+
"v=0\r\n"
|
|
43
|
+
"m=audio 9 RTP/SAVPF 8\r\n"
|
|
44
|
+
"a=rtpmap:8 PCMA/8000\r\n"
|
|
45
|
+
"m=video 9 RTP/SAVPF 96\r\n"
|
|
46
|
+
"a=rtpmap:96 H264/90000\r\n"
|
|
47
|
+
"m=application 9 SCTP webrtc-datachannel\r\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# The 15-of-107 shape: a leading video section numbering H265 as payload type 0.
|
|
51
|
+
_PT0_VIDEO = (
|
|
52
|
+
"v=0\r\n"
|
|
53
|
+
"m=video 9 RTP/SAVPF 0\r\n"
|
|
54
|
+
"a=rtpmap:0 H265/90000\r\n"
|
|
55
|
+
"m=audio 9 RTP/SAVPF 8\r\n"
|
|
56
|
+
"a=rtpmap:8 PCMA/8000\r\n"
|
|
57
|
+
"m=video 9 RTP/SAVPF 96\r\n"
|
|
58
|
+
"a=rtpmap:96 H264/90000\r\n"
|
|
59
|
+
"m=application 9 SCTP webrtc-datachannel\r\n"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# A000088 H265-capable firmware: legitimately four sections, reordered. A
|
|
63
|
+
# predicate keyed on the section COUNT would break this model, which works.
|
|
64
|
+
_A000088_REORDERED = (
|
|
65
|
+
"v=0\r\n"
|
|
66
|
+
"m=video 9 UDP/TLS/RTP/SAVPF 0\r\n"
|
|
67
|
+
"a=rtpmap:0 H265/90000\r\n"
|
|
68
|
+
"m=audio 9 UDP/TLS/RTP/SAVPF 8\r\n"
|
|
69
|
+
"a=rtpmap:8 PCMA/8000\r\n"
|
|
70
|
+
"m=video 9 UDP/TLS/RTP/SAVPF 101 102\r\n"
|
|
71
|
+
"a=rtpmap:101 H264/90000\r\n"
|
|
72
|
+
"m=application 9 DTLS/SCTP 5000\r\n"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_healthy_answer_maps_kinds():
|
|
77
|
+
assert answer_pt_kinds(_HEALTHY) == {8: "audio", 96: "video"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_pt0_video_is_reported_as_video_not_audio():
|
|
81
|
+
# The whole defect in one assertion: without the answer, pt=0 reads as PCMU.
|
|
82
|
+
kinds = answer_pt_kinds(_PT0_VIDEO)
|
|
83
|
+
assert kinds[0] == "video"
|
|
84
|
+
assert kinds[8] == "audio"
|
|
85
|
+
assert kinds[96] == "video"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_pt0_video_answer_resolves_to_our_h265_payload_type():
|
|
89
|
+
# What the bridge translates the wire PT into before ffmpeg sees it.
|
|
90
|
+
assert video_pt_from_answer_sdp(_PT0_VIDEO) == 97
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_a000088_reordered_four_sections_still_map_correctly():
|
|
94
|
+
kinds = answer_pt_kinds(_A000088_REORDERED)
|
|
95
|
+
assert kinds[0] == "video"
|
|
96
|
+
assert kinds[8] == "audio"
|
|
97
|
+
assert kinds[101] == "video"
|
|
98
|
+
assert kinds[102] == "video"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_payload_type_claimed_by_both_kinds_is_dropped():
|
|
102
|
+
# Ambiguity is not better evidence than the fallback table, so the map
|
|
103
|
+
# declines to answer for that payload type rather than guessing.
|
|
104
|
+
ambiguous = (
|
|
105
|
+
"m=video 9 RTP/SAVPF 0\r\n"
|
|
106
|
+
"m=audio 9 RTP/SAVPF 0 8\r\n"
|
|
107
|
+
)
|
|
108
|
+
kinds = answer_pt_kinds(ambiguous)
|
|
109
|
+
assert 0 not in kinds
|
|
110
|
+
assert kinds[8] == "audio"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_malformed_and_empty_answers_do_not_raise():
|
|
114
|
+
assert answer_pt_kinds("") == {}
|
|
115
|
+
# Out of contract, but the media path must not raise on it either.
|
|
116
|
+
assert answer_pt_kinds(None) == {} # type: ignore[arg-type]
|
|
117
|
+
assert answer_pt_kinds("m=video\r\nm=audio 9\r\n") == {}
|
|
118
|
+
# Non-numeric and out-of-range payload tokens are skipped, not raised on.
|
|
119
|
+
assert answer_pt_kinds("m=video 9 RTP/SAVPF x 300 96\r\n") == {96: "video"}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_rewrite_payload_type_preserves_the_marker_bit():
|
|
123
|
+
# byte1 = M(1) | PT(7). 0x80 is marker set + pt 0; 0x00 is marker clear.
|
|
124
|
+
marked = bytes([0x80, 0x80]) + b"rest"
|
|
125
|
+
assert rewrite_rtp_payload_type(marked, 97) == bytes([0x80, 0xE1]) + b"rest"
|
|
126
|
+
unmarked = bytes([0x80, 0x00]) + b"rest"
|
|
127
|
+
assert rewrite_rtp_payload_type(unmarked, 97) == bytes([0x80, 0x61]) + b"rest"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_rewrite_payload_type_leaves_the_rest_of_the_packet_alone():
|
|
131
|
+
pkt = bytes([0x80, 0x00]) + bytes(range(2, 40))
|
|
132
|
+
out = rewrite_rtp_payload_type(pkt, 96)
|
|
133
|
+
assert out[0] == pkt[0]
|
|
134
|
+
assert out[2:] == pkt[2:]
|
|
135
|
+
assert out[1] & 0x7F == 96
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_rewrite_payload_type_ignores_a_runt_packet():
|
|
139
|
+
# Media path: a malformed packet is ffmpeg's to discard, not ours to raise.
|
|
140
|
+
assert rewrite_rtp_payload_type(b"", 97) == b""
|
|
141
|
+
assert rewrite_rtp_payload_type(b"\x80", 97) == b"\x80"
|