python-aidot-cameras 1.0.0b7__tar.gz → 1.0.0b8__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.0b7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b8}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/protocol.py +118 -0
- python_aidot_cameras-1.0.0b8/aidot_cameras/camera/sd_playback.py +112 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/webrtc_open.py +91 -14
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/SOURCES.txt +3 -0
- python_aidot_cameras-1.0.0b8/tests/test_answer_section_selection.py +189 -0
- python_aidot_cameras-1.0.0b8/tests/test_sd_playback.py +107 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/README.md +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/tests/test_webrtc_answer_race.py +0 -0
{python_aidot_cameras-1.0.0b7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b8}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b8
|
|
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.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -243,6 +243,124 @@ def _section_of(lines: list) -> str:
|
|
|
243
243
|
return ""
|
|
244
244
|
|
|
245
245
|
|
|
246
|
+
#: How long to wait for ICE when the camera's answer arrived with its mids
|
|
247
|
+
#: shifted. Measured over 63 opens on an A000088: every successful open
|
|
248
|
+
#: completed ICE between 8.7 s and 10.6 s, and every failure ran the full 45 s
|
|
249
|
+
#: deadline. There is no overlap between the two bands, so a cap sitting above
|
|
250
|
+
#: the success band cannot cost a session that was going to work.
|
|
251
|
+
SHIFTED_ANSWER_ICE_TIMEOUT_S = 15.0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def answer_inserted_a_section(*, shifted_sections: int,
|
|
255
|
+
unclaimed_answer_mids: int) -> bool:
|
|
256
|
+
"""Did the camera ADD an m-section, rather than drop or reorder one?
|
|
257
|
+
|
|
258
|
+
Both shapes shift our mids, and only one of them is the behaviour the ICE
|
|
259
|
+
cap was measured against. An A000088 that inserts its H265 video section
|
|
260
|
+
leaves that section unclaimed after the walk, because nothing in our offer
|
|
261
|
+
wanted it. An A001064 that drops a rejected section and renumbers the rest -
|
|
262
|
+
quirk 1 of this rebuild - shifts the mids just as much and leaves nothing
|
|
263
|
+
over.
|
|
264
|
+
|
|
265
|
+
The distinction is load-bearing: the cap comes from 63 opens on a mains
|
|
266
|
+
A000088 that connects in 9 s, and an SDES camera reaching this path through
|
|
267
|
+
the SDES->DTLS fallback can take 25-70 s to open cold. Cutting its deadline
|
|
268
|
+
to 15 s on a shape that was never measured would break exactly the streaming
|
|
269
|
+
this must not break.
|
|
270
|
+
"""
|
|
271
|
+
return shifted_sections > 0 and unclaimed_answer_mids > 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def ice_wait_timeout(*, shifted_sections: int, default_timeout: float) -> float:
|
|
275
|
+
"""How long to give ICE, given what the camera's answer looked like.
|
|
276
|
+
|
|
277
|
+
A shifted answer - one where the camera added its H265 video section and
|
|
278
|
+
pushed every later mid along - is a strong but NOT certain sign the session
|
|
279
|
+
is not coming up: 6 of 7 measured shifts failed at ICE, and the seventh
|
|
280
|
+
established in 8.8 s. So it must still be given room to succeed, just not
|
|
281
|
+
45 s of it.
|
|
282
|
+
|
|
283
|
+
45 s matters because it is past the ~30 s deadline Home Assistant's stream
|
|
284
|
+
worker allows, which is the failure this project already learned the hard
|
|
285
|
+
way. Capping at 15 s keeps the one-in-seven that works, fails the rest
|
|
286
|
+
faster than the old behaviour did, and stays inside that deadline either
|
|
287
|
+
way.
|
|
288
|
+
|
|
289
|
+
Never extends a caller's own timeout - a caller asking for 5 s means 5 s.
|
|
290
|
+
"""
|
|
291
|
+
if shifted_sections > 0:
|
|
292
|
+
return min(default_timeout, SHIFTED_ANSWER_ICE_TIMEOUT_S)
|
|
293
|
+
return default_timeout
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def select_answer_section(
|
|
297
|
+
offer_mid: str,
|
|
298
|
+
offer_kind: str,
|
|
299
|
+
ans_sections: dict,
|
|
300
|
+
offer_video_pts: set,
|
|
301
|
+
claimed: set,
|
|
302
|
+
) -> "Optional[tuple]":
|
|
303
|
+
"""Which answer section fills this offer m-section, or None to stub it.
|
|
304
|
+
|
|
305
|
+
``ans_sections`` maps ``a=mid`` -> ``(kind, lines)``. ``claimed`` holds the
|
|
306
|
+
answer mids already given to earlier offer sections, so one section is never
|
|
307
|
+
handed out twice. Returns ``(answer_mid, lines)``.
|
|
308
|
+
|
|
309
|
+
Same mid first: that is what a compliant answerer sends, and the A001064
|
|
310
|
+
quirks this rebuild exists for (dropped sections, swapped kinds) are all
|
|
311
|
+
expressed in mid terms.
|
|
312
|
+
|
|
313
|
+
The fallback is for the case the mid walk cannot express. An A000088 was
|
|
314
|
+
measured answering an SD-card offer with an extra ``m=video`` carrying
|
|
315
|
+
``a=rtpmap:0 H265/90000`` at mid 0, which shifts every later mid by one.
|
|
316
|
+
Matching by mid then puts H265 in our video slot - and aiortc has no H265
|
|
317
|
+
in its codec registry, so setRemoteDescription fails outright rather than
|
|
318
|
+
degrading. On a live offer the same shift makes every mid mismatch on kind
|
|
319
|
+
and the whole answer gets stubbed, which reads as a camera declining media.
|
|
320
|
+
The camera does not add that section every time, so both failures are
|
|
321
|
+
intermittent.
|
|
322
|
+
|
|
323
|
+
So when the same-mid section is the wrong kind, or is a video section
|
|
324
|
+
sharing no payload type with our offer, the other unclaimed sections of the
|
|
325
|
+
right kind are considered and the video one with the most payload-type
|
|
326
|
+
overlap wins. A video section with no overlap is still preferred over
|
|
327
|
+
stubbing: a section aiortc may reject beats one that cannot possibly work.
|
|
328
|
+
"""
|
|
329
|
+
def _pts(entry) -> set:
|
|
330
|
+
for ln in entry[1]:
|
|
331
|
+
if ln.startswith("m="):
|
|
332
|
+
return set(ln.split()[3:])
|
|
333
|
+
return set()
|
|
334
|
+
|
|
335
|
+
def _usable(entry) -> bool:
|
|
336
|
+
if entry[0] != offer_kind:
|
|
337
|
+
return False
|
|
338
|
+
# Payload types only discriminate video, and only when we know ours.
|
|
339
|
+
if offer_kind != "video" or not offer_video_pts:
|
|
340
|
+
return True
|
|
341
|
+
return bool(_pts(entry) & offer_video_pts)
|
|
342
|
+
|
|
343
|
+
same = ans_sections.get(offer_mid)
|
|
344
|
+
if same is not None and offer_mid not in claimed and _usable(same):
|
|
345
|
+
return (offer_mid, same[1])
|
|
346
|
+
|
|
347
|
+
candidates = [
|
|
348
|
+
(mid, entry) for mid, entry in ans_sections.items()
|
|
349
|
+
if mid not in claimed and entry[0] == offer_kind
|
|
350
|
+
]
|
|
351
|
+
if not candidates:
|
|
352
|
+
return None
|
|
353
|
+
if offer_kind == "video" and offer_video_pts:
|
|
354
|
+
mid, entry = max(
|
|
355
|
+
candidates, key=lambda c: (len(_pts(c[1]) & offer_video_pts), c[0])
|
|
356
|
+
)
|
|
357
|
+
return (mid, entry[1])
|
|
358
|
+
# Non-video, or nothing to score with: the earliest unclaimed one of the
|
|
359
|
+
# right kind, which for a shifted answer is the shifted section.
|
|
360
|
+
mid, entry = min(candidates, key=lambda c: c[0])
|
|
361
|
+
return (mid, entry[1])
|
|
362
|
+
|
|
363
|
+
|
|
246
364
|
def _compress_sdp_for_camera(sdp: str) -> str:
|
|
247
365
|
"""Selective SDP filter matching official Java client's g.b() behaviour.
|
|
248
366
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Ask a camera to play a recording off its own SD card.
|
|
2
|
+
|
|
3
|
+
The control surface only - the request bytes and the reply decode. Opening a
|
|
4
|
+
session and delivering frames is `_CameraSdPlaybackMixin`, added separately, so
|
|
5
|
+
this half can be tested with no camera in the way.
|
|
6
|
+
|
|
7
|
+
The request shape comes from the vendor app rather than from guesswork - but
|
|
8
|
+
from a specific overload of it. The app calls
|
|
9
|
+
`AVIOCTRLDEFs$SMsgAVIoctrlPlayRecord.parseContent(IIIIJII)`, not the
|
|
10
|
+
`(III[B)` overload this module first assumed, and the two disagree about the
|
|
11
|
+
last four bytes. `parseContent(IIIIJII)` allocates 0x18 bytes and fills:
|
|
12
|
+
command at offset 4 and an 8-byte STimeDay at offset 12, both as first
|
|
13
|
+
assumed, but offset 0 and offset 8 are ALWAYS 0 (not a caller-chosen channel),
|
|
14
|
+
and the trailing two bytes at offsets 20-21 carry the real channel and a
|
|
15
|
+
second caller value. The channel comes from `KVSWebRTCChannel.sdRecordPlay`,
|
|
16
|
+
which allocates it as `sdChannelId += 1` wrapping 26 -> 1 before every play -
|
|
17
|
+
so a real request never carries channel 0.
|
|
18
|
+
"""
|
|
19
|
+
import struct
|
|
20
|
+
from typing import Any, NamedTuple, Optional
|
|
21
|
+
|
|
22
|
+
RECORD_PLAYCONTROL_REQ_CMD = 0x31A
|
|
23
|
+
RECORD_PLAYCONTROL_RESP_CMD = 0x31B
|
|
24
|
+
|
|
25
|
+
# Only START and STOP have been sent to a camera and answered (measured
|
|
26
|
+
# 2026-08-13, A000088). PAUSE, STEPFORWARD, STEPBACKWARD, FORWARD, BACKWARD,
|
|
27
|
+
# SEEKTIME, END and RESUME are real values from the same vendor enum, kept
|
|
28
|
+
# here for completeness, but nobody should assume any of them are proven
|
|
29
|
+
# against hardware.
|
|
30
|
+
PLAY_PAUSE = 0x00
|
|
31
|
+
PLAY_STOP = 0x01
|
|
32
|
+
PLAY_STEPFORWARD = 0x02
|
|
33
|
+
PLAY_STEPBACKWARD = 0x03
|
|
34
|
+
PLAY_FORWARD = 0x04
|
|
35
|
+
PLAY_BACKWARD = 0x05
|
|
36
|
+
PLAY_SEEKTIME = 0x06
|
|
37
|
+
PLAY_END = 0x07
|
|
38
|
+
PLAY_START = 0x10
|
|
39
|
+
PLAY_RESUME = 0x11
|
|
40
|
+
|
|
41
|
+
_PAYLOAD_LEN = 0x18
|
|
42
|
+
# Unlike _PAYLOAD_LEN, this is not read off the vendor encoder - it rests on
|
|
43
|
+
# one live capture, on an A000088, 2026-08-13. Treat it as less certain than
|
|
44
|
+
# the request shape above.
|
|
45
|
+
_REPLY_LEN = 20
|
|
46
|
+
|
|
47
|
+
_MIN_SD_CHANNEL = 1
|
|
48
|
+
_MAX_SD_CHANNEL = 26
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PlaycontrolReply(NamedTuple):
|
|
52
|
+
"""What the camera said back.
|
|
53
|
+
|
|
54
|
+
``field1`` and ``field2`` are deliberately not named. Measured 2026-08-13:
|
|
55
|
+
``field1`` read 0 with the camera free and 1 while another viewer held a
|
|
56
|
+
live session, so it is NOT a success code; ``field2`` read 60 on every run,
|
|
57
|
+
but every run used the same record. Naming either would publish a guess.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
command: int
|
|
61
|
+
field1: int
|
|
62
|
+
field2: int
|
|
63
|
+
year: int
|
|
64
|
+
month: int
|
|
65
|
+
day: int
|
|
66
|
+
hour: int
|
|
67
|
+
minute: int
|
|
68
|
+
second: int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def playcontrol_payload(command: int, record: Any, *, sd_channel: int = 1,
|
|
72
|
+
param: int = 0) -> bytes:
|
|
73
|
+
"""The 24 bytes for one playback command against one record.
|
|
74
|
+
|
|
75
|
+
``sd_channel`` must be 1..26, the range `sdRecordPlay` actually allocates.
|
|
76
|
+
A value outside it is a caller bug, not a protocol surprise, and is
|
|
77
|
+
rejected rather than clamped: a silently changed channel would send a
|
|
78
|
+
request the camera accepts and answers while returning no media - the
|
|
79
|
+
exact symptom this module exists to avoid reproducing.
|
|
80
|
+
"""
|
|
81
|
+
if not _MIN_SD_CHANNEL <= sd_channel <= _MAX_SD_CHANNEL:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"sd_channel must be {_MIN_SD_CHANNEL}..{_MAX_SD_CHANNEL}, "
|
|
84
|
+
f"got {sd_channel}")
|
|
85
|
+
# Offsets 0 and 8 are always 0 in the vendor app's own call - neither is a
|
|
86
|
+
# caller-chosen channel or param, whatever the (III[B) overload implies.
|
|
87
|
+
head = struct.pack("<III", 0, command, 0)
|
|
88
|
+
when = struct.pack(
|
|
89
|
+
"<HBBBBBB",
|
|
90
|
+
record.year, record.month, record.day,
|
|
91
|
+
0, # wday - the camera fills its own
|
|
92
|
+
record.hour, record.minute, record.second,
|
|
93
|
+
)
|
|
94
|
+
tail = struct.pack("<BB", sd_channel, param)
|
|
95
|
+
return (head + when + tail
|
|
96
|
+
+ b"\x00" * (_PAYLOAD_LEN - len(head) - len(when) - len(tail)))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def decode_playcontrol_reply(data: Optional[bytes]) -> Optional[PlaycontrolReply]:
|
|
100
|
+
"""Read a 0x31b reply, or None if it cannot be read.
|
|
101
|
+
|
|
102
|
+
None rather than an exception, for the same reason the listing decode does
|
|
103
|
+
it: a reply with an unexpected shape is entirely ordinary and must not
|
|
104
|
+
surface as a traceback in a media browser.
|
|
105
|
+
"""
|
|
106
|
+
if not data or len(data) < _REPLY_LEN:
|
|
107
|
+
return None
|
|
108
|
+
command, field1, field2 = struct.unpack("<III", data[:12])
|
|
109
|
+
year, month, day, _wday, hour, minute, second = struct.unpack(
|
|
110
|
+
"<HBBBBBB", data[12:20])
|
|
111
|
+
return PlaycontrolReply(command, field1, field2,
|
|
112
|
+
year, month, day, hour, minute, second)
|
{python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
@@ -34,6 +34,9 @@ from .protocol import (
|
|
|
34
34
|
_normalize_bundle_ice_credentials,
|
|
35
35
|
_reorder_m_section_ice_attrs,
|
|
36
36
|
_sdp_transport,
|
|
37
|
+
answer_inserted_a_section,
|
|
38
|
+
ice_wait_timeout,
|
|
39
|
+
select_answer_section,
|
|
37
40
|
_terminal_webrtc_ack,
|
|
38
41
|
_upgrade_sctp,
|
|
39
42
|
_webrtc_consume_video,
|
|
@@ -2558,6 +2561,13 @@ class _WebRTCOpenMixin:
|
|
|
2558
2561
|
_rr_webrtc_resp_topic: str = ""
|
|
2559
2562
|
_rr_webrtc_resp_payload: str = ""
|
|
2560
2563
|
_rr_ice_payloads: list = [] # list of (topic, json_str) tuples
|
|
2564
|
+
# How many answer sections came from a mid other than the offer's. Set
|
|
2565
|
+
# by the normal path's rebuild below; declared here so the ICE wait can
|
|
2566
|
+
# read it whichever path ran.
|
|
2567
|
+
_shifted_picks = 0
|
|
2568
|
+
# True only when the camera ADDED a section, which is the shape the
|
|
2569
|
+
# ICE cap was measured against.
|
|
2570
|
+
_answer_inserted = False
|
|
2561
2571
|
|
|
2562
2572
|
if camera_offer_fut in _rr_done and answer_fut not in _rr_done:
|
|
2563
2573
|
# ---- ROLE REVERSAL path (e.g. LK.IPC.A001064) ---------------------- #
|
|
@@ -3037,14 +3047,52 @@ class _WebRTCOpenMixin:
|
|
|
3037
3047
|
# 4-section BUNDLE is aligned) but we only decode H264 and audio.
|
|
3038
3048
|
_h265_offer_pts: set = set() # H265 no longer offered; camera won't answer H265 PTs
|
|
3039
3049
|
|
|
3050
|
+
# Our offer's video payload types, used both here (to tell the
|
|
3051
|
+
# camera's real video answer from an H265 section it added) and
|
|
3052
|
+
# again by _aiortc_answer below.
|
|
3053
|
+
_offer_video_pts: set = set()
|
|
3054
|
+
for _ol in _offer_sdp.splitlines():
|
|
3055
|
+
if _ol.startswith('m=video'):
|
|
3056
|
+
_offer_video_pts = set(_ol.split()[3:])
|
|
3057
|
+
break
|
|
3058
|
+
|
|
3040
3059
|
_rebuilt: list = list(_ans_header)
|
|
3041
3060
|
_kept_count = 0
|
|
3042
3061
|
_stub_count = 0
|
|
3043
3062
|
_dropped_mids: list = []
|
|
3063
|
+
_claimed_ans_mids: set = set()
|
|
3044
3064
|
_rejected_mids: set = set() # mids whose stub has port=0 (excluded from BUNDLE)
|
|
3045
3065
|
for _m in _offer_mids:
|
|
3046
3066
|
_expected_kind = _offer_kinds[_m]
|
|
3047
|
-
|
|
3067
|
+
# Same mid first, then kind + payload-type overlap. This camera
|
|
3068
|
+
# sometimes adds an H265 video section at mid 0 and shifts every
|
|
3069
|
+
# later mid by one; matching on mid alone then puts H265 in our
|
|
3070
|
+
# video slot (hard setRemoteDescription failure) or mismatches
|
|
3071
|
+
# every kind at once (reads as declined media). See
|
|
3072
|
+
# select_answer_section.
|
|
3073
|
+
_pick = select_answer_section(
|
|
3074
|
+
_m, _expected_kind, _ans_sections,
|
|
3075
|
+
_offer_video_pts, _claimed_ans_mids,
|
|
3076
|
+
)
|
|
3077
|
+
if _pick is not None:
|
|
3078
|
+
_claimed_ans_mids.add(_pick[0])
|
|
3079
|
+
if _pick[0] != _m:
|
|
3080
|
+
_shifted_picks += 1
|
|
3081
|
+
_status(
|
|
3082
|
+
f"answer mid {_pick[0]} taken for offer mid {_m}"
|
|
3083
|
+
f" ({_expected_kind}): the camera shifted its mids"
|
|
3084
|
+
)
|
|
3085
|
+
_ans_kind = _expected_kind if _pick is not None else ""
|
|
3086
|
+
_ans_block = list(_pick[1]) if _pick is not None else []
|
|
3087
|
+
if _pick is not None and _pick[0] != _m:
|
|
3088
|
+
# Renumber to the slot it now occupies. _aiortc_answer
|
|
3089
|
+
# renumbers again downstream, but leaving a section
|
|
3090
|
+
# carrying another slot's a=mid inside _rebuilt would make
|
|
3091
|
+
# the intermediate SDP disagree with its own BUNDLE line.
|
|
3092
|
+
_ans_block = [
|
|
3093
|
+
f'a=mid:{_m}' if _bl.startswith('a=mid:') else _bl
|
|
3094
|
+
for _bl in _ans_block
|
|
3095
|
+
]
|
|
3048
3096
|
# Force-stub H265 sections: detect by camera's answer m-line
|
|
3049
3097
|
# using only our H265 PTs (103/104).
|
|
3050
3098
|
_ans_h265 = False
|
|
@@ -3070,9 +3118,19 @@ class _WebRTCOpenMixin:
|
|
|
3070
3118
|
_stub_count += 1
|
|
3071
3119
|
if _s and _s[0].split()[1] == '0':
|
|
3072
3120
|
_rejected_mids.add(_m)
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3121
|
+
# Sections the walk never took. An INSERTED section (the camera's
|
|
3122
|
+
# H265 one) ends up here; a dropped-and-renumbered answer leaves
|
|
3123
|
+
# nothing. That is what separates the shape the ICE cap was
|
|
3124
|
+
# measured on from the one it was not - see
|
|
3125
|
+
# answer_inserted_a_section.
|
|
3126
|
+
_unclaimed_ans_mids = [
|
|
3127
|
+
_am for _am in _ans_sections if _am not in _claimed_ans_mids
|
|
3128
|
+
]
|
|
3129
|
+
_answer_inserted = answer_inserted_a_section(
|
|
3130
|
+
shifted_sections=_shifted_picks,
|
|
3131
|
+
unclaimed_answer_mids=len(_unclaimed_ans_mids),
|
|
3132
|
+
)
|
|
3133
|
+
_dropped_mids.extend(_unclaimed_ans_mids)
|
|
3076
3134
|
|
|
3077
3135
|
if _stub_count or _dropped_mids:
|
|
3078
3136
|
_status(
|
|
@@ -3196,13 +3254,8 @@ class _WebRTCOpenMixin:
|
|
|
3196
3254
|
# bogus single-PT video stub - all of which break aiortc's positional
|
|
3197
3255
|
# matching. _aiortc_answer() below selects by content and re-emits.
|
|
3198
3256
|
import re as _re_ans # module has no top-level `import re`
|
|
3199
|
-
#
|
|
3200
|
-
#
|
|
3201
|
-
_offer_video_pts: set = set()
|
|
3202
|
-
for _ol in _offer_sdp.splitlines():
|
|
3203
|
-
if _ol.startswith('m=video'):
|
|
3204
|
-
_offer_video_pts = set(_ol.split()[3:])
|
|
3205
|
-
break
|
|
3257
|
+
# _offer_video_pts (our H264 + RTX payload types) is computed above,
|
|
3258
|
+
# where the mid walk needs it, and reused here.
|
|
3206
3259
|
|
|
3207
3260
|
# Set by _aiortc_answer when the camera returns a DC-only answer
|
|
3208
3261
|
# (both audio and video rejected = media declined, encoder cold).
|
|
@@ -3618,7 +3671,19 @@ class _WebRTCOpenMixin:
|
|
|
3618
3671
|
async def _on_ice_gather() -> None:
|
|
3619
3672
|
_LOGGER.info("webrtc: ICE gatheringState -> %s", pc.iceGatheringState)
|
|
3620
3673
|
|
|
3621
|
-
deadline
|
|
3674
|
+
# A shifted answer gets a shorter ICE deadline, not a refusal: 6 of 7
|
|
3675
|
+
# measured shifts never connected, but the seventh came up in 8.8 s.
|
|
3676
|
+
# See ice_wait_timeout for the measurement behind the cap.
|
|
3677
|
+
_ice_timeout = ice_wait_timeout(
|
|
3678
|
+
shifted_sections=1 if _answer_inserted else 0,
|
|
3679
|
+
default_timeout=timeout,
|
|
3680
|
+
)
|
|
3681
|
+
if _ice_timeout != timeout:
|
|
3682
|
+
_status(
|
|
3683
|
+
f"answer shifted mids: ICE deadline capped to {_ice_timeout:.0f}s"
|
|
3684
|
+
f" (from {timeout:.0f}s)"
|
|
3685
|
+
)
|
|
3686
|
+
deadline = time.monotonic() + _ice_timeout
|
|
3622
3687
|
_last_ice_log = time.monotonic()
|
|
3623
3688
|
_userconnect_midloop_sent = False # guard: re-send user/connect once at half-timeout
|
|
3624
3689
|
_second_ans_processed = False # guard: process second_answer_fut candidates once
|
|
@@ -3719,11 +3784,15 @@ class _WebRTCOpenMixin:
|
|
|
3719
3784
|
# Mid-loop user/connect retry: if no camera IP yet and ~half of the
|
|
3720
3785
|
# ICE timeout has elapsed, re-announce user presence so the camera
|
|
3721
3786
|
# pushes setDevAttrNotif again. Replaces the invented getDevAttrReq.
|
|
3787
|
+
# Halve the deadline this loop is actually running to (_ice_timeout),
|
|
3788
|
+
# not the caller's: a capped wait compared against the uncapped
|
|
3789
|
+
# timeout is already past half on its first tick, which fires the
|
|
3790
|
+
# one-shot re-announce at t=0 and leaves nothing for mid-loop.
|
|
3722
3791
|
_remaining = deadline - time.monotonic()
|
|
3723
3792
|
if (not _userconnect_midloop_sent
|
|
3724
3793
|
and cam_ip_q.empty()
|
|
3725
3794
|
and _remaining > 0
|
|
3726
|
-
and _remaining <=
|
|
3795
|
+
and _remaining <= _ice_timeout / 2.0):
|
|
3727
3796
|
outgoing_q.put_nowait((
|
|
3728
3797
|
f"iot/v1/cb/{user_id}/user/connect",
|
|
3729
3798
|
json.dumps({
|
|
@@ -3767,10 +3836,18 @@ class _WebRTCOpenMixin:
|
|
|
3767
3836
|
outgoing_q.put_nowait(None)
|
|
3768
3837
|
_cancel_track_tasks()
|
|
3769
3838
|
await pc.close()
|
|
3839
|
+
if _answer_inserted:
|
|
3840
|
+
# Before the answer sections were matched by content, a shifted
|
|
3841
|
+
# answer on the live path stubbed every section and raised this
|
|
3842
|
+
# same error, which the caller retries in a fast bounded burst.
|
|
3843
|
+
# Keep that: a shifted answer that did not connect inside the
|
|
3844
|
+
# cap is the camera declining, not a transport fault, and the
|
|
3845
|
+
# generic 15s-gated retry is the wrong response to it.
|
|
3846
|
+
raise AidotCameraNotReady(self.device_id)
|
|
3770
3847
|
raise RuntimeError(
|
|
3771
3848
|
f"async_open_webrtc_stream: ICE connection not established "
|
|
3772
3849
|
f"(connState={pc.connectionState}"
|
|
3773
|
-
f" iceState={pc.iceConnectionState}) within {
|
|
3850
|
+
f" iceState={pc.iceConnectionState}) within {_ice_timeout}s"
|
|
3774
3851
|
)
|
|
3775
3852
|
|
|
3776
3853
|
if recorder:
|
|
@@ -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.0b8"
|
|
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"
|
{python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b8/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b8
|
|
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
|
|
@@ -61,6 +61,7 @@ aidot_cameras/camera/playback.py
|
|
|
61
61
|
aidot_cameras/camera/protocol.py
|
|
62
62
|
aidot_cameras/camera/sd_events.py
|
|
63
63
|
aidot_cameras/camera/sd_listing.py
|
|
64
|
+
aidot_cameras/camera/sd_playback.py
|
|
64
65
|
aidot_cameras/camera/sdes.py
|
|
65
66
|
aidot_cameras/camera/sdes_open.py
|
|
66
67
|
aidot_cameras/camera/tutk.py
|
|
@@ -75,6 +76,7 @@ python_aidot_cameras.egg-info/top_level.txt
|
|
|
75
76
|
tests/test_aioice_compat.py
|
|
76
77
|
tests/test_alarm_event.py
|
|
77
78
|
tests/test_answer_sdp_video_pt.py
|
|
79
|
+
tests/test_answer_section_selection.py
|
|
78
80
|
tests/test_audio_mux_offline.py
|
|
79
81
|
tests/test_avio_request.py
|
|
80
82
|
tests/test_avio_response_parse.py
|
|
@@ -164,6 +166,7 @@ tests/test_sd_card_presence.py
|
|
|
164
166
|
tests/test_sd_event_decode.py
|
|
165
167
|
tests/test_sd_event_probe_payloads.py
|
|
166
168
|
tests/test_sd_listing.py
|
|
169
|
+
tests/test_sd_playback.py
|
|
167
170
|
tests/test_sdes_adaptive.py
|
|
168
171
|
tests/test_sdes_avio_dispatch.py
|
|
169
172
|
tests/test_sdes_bridge_break_gate.py
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Matching the camera's answer sections to the offer's m-sections.
|
|
2
|
+
|
|
3
|
+
The rebuild that feeds aiortc used to match purely by ``a=mid``. That holds
|
|
4
|
+
right up until the camera adds a section the offer never had, which this
|
|
5
|
+
firmware family does: an A000088 was measured on 2026-08-13 answering an
|
|
6
|
+
SD-card offer with
|
|
7
|
+
|
|
8
|
+
m=video ... 0 a=mid:0 a=rtpmap:0 H265/90000
|
|
9
|
+
m=video ... 101 102 a=mid:1 (the H264 section we negotiated)
|
|
10
|
+
m=application ... a=mid:2
|
|
11
|
+
|
|
12
|
+
Every mid is shifted by one. Matching by mid then hands our video slot the
|
|
13
|
+
H265 section and drops the H264 one, and aiortc rejects the answer outright
|
|
14
|
+
("Failed to set remote video description send parameters") because H265 is not
|
|
15
|
+
in its codec registry. On a live offer the same shift makes every mid mismatch
|
|
16
|
+
on kind, all sections get stubbed, and the open fails as a cold camera would.
|
|
17
|
+
|
|
18
|
+
Mid order is still the right first guess - it is what a compliant answerer
|
|
19
|
+
sends, and the A001064 quirks the mid-walk was written for depend on it. So the
|
|
20
|
+
mid match stays, and the fallback only runs when it produces something
|
|
21
|
+
unusable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
28
|
+
|
|
29
|
+
from aidot_cameras.camera.protocol import select_answer_section
|
|
30
|
+
|
|
31
|
+
_OFFER_VIDEO_PTS = {"97", "98", "99", "100", "101", "102"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sec(kind: str, *pts: str) -> tuple:
|
|
35
|
+
"""One answer section: (kind, lines), as the rebuild stores them."""
|
|
36
|
+
line = f"m={kind} 9 UDP/TLS/RTP/SAVPF " + " ".join(pts)
|
|
37
|
+
return (kind, [line, "a=rtcp-mux", "a=sendonly"])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# The shapes below are transcribed from measured answers, not invented.
|
|
41
|
+
_SD_SHIFTED = {
|
|
42
|
+
"0": _sec("video", "0"), # H265 stub the camera added
|
|
43
|
+
"1": _sec("video", "101", "102"), # the H264 answer we want
|
|
44
|
+
"2": _sec("application"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_LIVE_SHIFTED = {
|
|
48
|
+
"0": _sec("video", "0"),
|
|
49
|
+
"1": _sec("audio", "8"),
|
|
50
|
+
"2": _sec("video", "101", "102"),
|
|
51
|
+
"3": _sec("application"),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_LIVE_CLEAN = {
|
|
55
|
+
"0": _sec("audio", "8"),
|
|
56
|
+
"1": _sec("video", "101", "102"),
|
|
57
|
+
"2": _sec("application"),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_clean_answer_is_matched_by_mid_unchanged():
|
|
62
|
+
"""The common case must not move: mid 1 is the video, and stays it."""
|
|
63
|
+
for mid, kind in (("0", "audio"), ("1", "video"), ("2", "application")):
|
|
64
|
+
got = select_answer_section(mid, kind, _LIVE_CLEAN, _OFFER_VIDEO_PTS, set())
|
|
65
|
+
assert got == (mid, _LIVE_CLEAN[mid][1])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_sd_offer_skips_the_h265_stub_sharing_its_mid():
|
|
69
|
+
"""Our only video mid is 0, and the camera's mid 0 is its H265 section."""
|
|
70
|
+
got = select_answer_section("0", "video", _SD_SHIFTED, _OFFER_VIDEO_PTS, set())
|
|
71
|
+
assert got is not None
|
|
72
|
+
assert got[0] == "1", "should take the H264 section, not the H265 one"
|
|
73
|
+
assert "101 102" in got[1][0]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_sd_datachannel_finds_the_application_section_a_mid_away():
|
|
77
|
+
got = select_answer_section("1", "application", _SD_SHIFTED,
|
|
78
|
+
_OFFER_VIDEO_PTS, {"0"})
|
|
79
|
+
assert got is not None and got[0] == "2"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_live_offer_survives_a_one_mid_shift():
|
|
83
|
+
"""audio/video/application must each find their section despite the shift."""
|
|
84
|
+
claimed: set = set()
|
|
85
|
+
picks = {}
|
|
86
|
+
for mid, kind in (("0", "audio"), ("1", "video"), ("2", "application")):
|
|
87
|
+
got = select_answer_section(mid, kind, _LIVE_SHIFTED,
|
|
88
|
+
_OFFER_VIDEO_PTS, claimed)
|
|
89
|
+
assert got is not None, f"offer mid {mid} ({kind}) found nothing"
|
|
90
|
+
claimed.add(got[0])
|
|
91
|
+
picks[kind] = got[0]
|
|
92
|
+
assert picks == {"audio": "1", "video": "2", "application": "3"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_a_section_is_never_handed_to_two_offer_mids():
|
|
96
|
+
claimed = {"1"}
|
|
97
|
+
got = select_answer_section("0", "video", _SD_SHIFTED,
|
|
98
|
+
_OFFER_VIDEO_PTS, claimed)
|
|
99
|
+
assert got is None or got[0] != "1"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_missing_kind_returns_none_so_the_caller_stubs():
|
|
103
|
+
"""A001064 drops sections outright; the caller must still get to stub."""
|
|
104
|
+
answer = {"0": _sec("video", "101", "102")}
|
|
105
|
+
assert select_answer_section("1", "audio", answer,
|
|
106
|
+
_OFFER_VIDEO_PTS, set()) is None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_video_with_no_overlap_is_still_taken_when_it_is_all_there_is():
|
|
110
|
+
"""Better a section aiortc may reject than a stub that guarantees failure.
|
|
111
|
+
|
|
112
|
+
Losing the real section would turn a recoverable answer into a declined one,
|
|
113
|
+
so a no-overlap video section is preferred over nothing at all.
|
|
114
|
+
"""
|
|
115
|
+
answer = {"0": _sec("video", "0")}
|
|
116
|
+
got = select_answer_section("0", "video", answer, _OFFER_VIDEO_PTS, set())
|
|
117
|
+
assert got is not None and got[0] == "0"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_no_offer_pts_falls_back_to_plain_mid_matching():
|
|
121
|
+
"""With nothing to score against, the mid is the only signal left."""
|
|
122
|
+
got = select_answer_section("0", "video", _SD_SHIFTED, set(), set())
|
|
123
|
+
assert got is not None and got[0] == "0"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --- how long to wait for ICE, given what the answer looked like -------------
|
|
127
|
+
|
|
128
|
+
from aidot_cameras.camera.protocol import ice_wait_timeout
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_a_clean_answer_gets_the_full_timeout():
|
|
132
|
+
assert ice_wait_timeout(shifted_sections=0, default_timeout=45.0) == 45.0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_a_shifted_answer_is_capped_but_not_refused():
|
|
136
|
+
"""One shifted answer in seven DID open - at 8.8 s. The cap must clear it.
|
|
137
|
+
|
|
138
|
+
Measured over 63 opens: every successful open finished ICE between 8.7 s and
|
|
139
|
+
10.6 s and every failure ran the full 45 s. There is no overlap, so a cap
|
|
140
|
+
above the success band and far below the failure band loses nothing and
|
|
141
|
+
saves 30 s on the 86% that were never going to connect.
|
|
142
|
+
"""
|
|
143
|
+
capped = ice_wait_timeout(shifted_sections=3, default_timeout=45.0)
|
|
144
|
+
assert capped < 45.0
|
|
145
|
+
assert capped > 10.6, "must still clear the slowest observed success"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_the_cap_never_extends_a_shorter_caller_timeout():
|
|
149
|
+
"""A caller asking for 5 s means 5 s, shifted answer or not."""
|
|
150
|
+
assert ice_wait_timeout(shifted_sections=3, default_timeout=5.0) == 5.0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# --- telling an ADDED section from a dropped or reordered one ----------------
|
|
154
|
+
|
|
155
|
+
from aidot_cameras.camera.protocol import answer_inserted_a_section
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_an_added_section_leaves_one_unclaimed():
|
|
159
|
+
"""The measured shape: the camera adds H265 and every later mid moves on.
|
|
160
|
+
|
|
161
|
+
The SD offer has two mids (video 0, application 1); the camera answers with
|
|
162
|
+
three, and the walk claims its video and application sections, leaving the
|
|
163
|
+
H265 one spare. That spare is what says a section was INSERTED.
|
|
164
|
+
"""
|
|
165
|
+
assert answer_inserted_a_section(shifted_sections=2, unclaimed_answer_mids=1)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_a_dropped_section_is_not_an_insert():
|
|
169
|
+
"""A001064 drops rejected sections and renumbers - quirk 1 of the rebuild.
|
|
170
|
+
|
|
171
|
+
Its mids shift too, but nothing is left over. Treating that as the H265
|
|
172
|
+
shape would cut the ICE deadline of a camera whose cold open is 25-70 s,
|
|
173
|
+
on the SDES->DTLS fallback path, using a cap measured on a mains A000088
|
|
174
|
+
that opens in 9 s.
|
|
175
|
+
"""
|
|
176
|
+
assert not answer_inserted_a_section(shifted_sections=2,
|
|
177
|
+
unclaimed_answer_mids=0)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_a_clean_answer_is_not_an_insert():
|
|
181
|
+
assert not answer_inserted_a_section(shifted_sections=0,
|
|
182
|
+
unclaimed_answer_mids=0)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_a_spare_section_without_a_shift_is_not_an_insert():
|
|
186
|
+
"""An extra section nobody wanted, with our mids still lining up, is the
|
|
187
|
+
camera padding the answer - not the shift the cap was measured against."""
|
|
188
|
+
assert not answer_inserted_a_section(shifted_sections=0,
|
|
189
|
+
unclaimed_answer_mids=1)
|