python-aidot-cameras 1.0.0b6__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.0b6/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b8}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/models.py +42 -0
- {python_aidot_cameras-1.0.0b6 → 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.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/webrtc_open.py +91 -14
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/SOURCES.txt +4 -0
- python_aidot_cameras-1.0.0b8/tests/test_answer_section_selection.py +189 -0
- python_aidot_cameras-1.0.0b8/tests/test_sd_card_presence.py +94 -0
- python_aidot_cameras-1.0.0b8/tests/test_sd_playback.py +107 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/README.md +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_bitrate_campaign_arms.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_live_validate_policy.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_first_media_stall_report.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b6 → python_aidot_cameras-1.0.0b8}/tests/test_webrtc_answer_race.py +0 -0
{python_aidot_cameras-1.0.0b6/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.0b6 → python_aidot_cameras-1.0.0b8}/aidot_cameras/camera/models.py
RENAMED
|
@@ -48,6 +48,38 @@ def _as_bool(v: Any) -> Optional[bool]:
|
|
|
48
48
|
return None if i is None else bool(i)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _card_present(attr: dict) -> Optional[bool]:
|
|
52
|
+
"""Is an SD card in the slot? None means nobody said.
|
|
53
|
+
|
|
54
|
+
Three states, and the third is the point. Of seven cameras measured
|
|
55
|
+
2026-08-12, one reported ``SDcardExistFlag: true``, two reported ``false``,
|
|
56
|
+
and four carried neither key - including an A000088, the same model as all
|
|
57
|
+
three that did report. So a missing key cannot mean "this model cannot say";
|
|
58
|
+
it means we do not know, and anything that reads it as "no card" would tell
|
|
59
|
+
someone with a working camera that their slot is empty.
|
|
60
|
+
|
|
61
|
+
``SDcardExistFlag`` wins where it exists. ``SDcardBaseInfo`` is the fallback
|
|
62
|
+
and arrives as a STRING holding a JSON array - element 0 agreed with the
|
|
63
|
+
flag on every camera carrying both. The other four positions are not
|
|
64
|
+
decoded: ``30432`` is plausibly a 32 GB card's total MB and plausibly
|
|
65
|
+
something else, and a guess does not belong in a public data model.
|
|
66
|
+
"""
|
|
67
|
+
if "SDcardExistFlag" in attr:
|
|
68
|
+
flag = _as_bool(attr.get("SDcardExistFlag"))
|
|
69
|
+
if flag is not None:
|
|
70
|
+
return flag
|
|
71
|
+
|
|
72
|
+
raw = attr.get("SDcardBaseInfo")
|
|
73
|
+
if isinstance(raw, str):
|
|
74
|
+
try:
|
|
75
|
+
raw = json.loads(raw)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return None
|
|
78
|
+
if isinstance(raw, (list, tuple)) and raw:
|
|
79
|
+
return _as_bool(raw[0])
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
51
83
|
class CameraStatusData(DeviceStatusData):
|
|
52
84
|
"""Core status plus camera fields; accepts both typed-model and dict updates."""
|
|
53
85
|
|
|
@@ -74,6 +106,11 @@ class CameraStatusData(DeviceStatusData):
|
|
|
74
106
|
battery_remaining: Optional[int] = None # Battery_remaining 0-100 (%)
|
|
75
107
|
occupancy: Optional[bool] = None # Occupancy live presence
|
|
76
108
|
sd_card_status: Optional[str] = None # SDcardStatus
|
|
109
|
+
# Tri-state: True in the slot, False slot empty, None nobody said. Reads
|
|
110
|
+
# INVERTED on A000088 relative to sd_card_status ("0" with a card, "1"
|
|
111
|
+
# without, 3/3) - which is why sd_card_status is not used for this and the
|
|
112
|
+
# two are kept apart. That inversion has no corroboration off A000088.
|
|
113
|
+
sd_card_present: Optional[bool] = None # SDcardExistFlag/SDcardBaseInfo[0]
|
|
77
114
|
wifi_rssi: Optional[int] = None # networkRssi (dBm), cloud property
|
|
78
115
|
|
|
79
116
|
def update(self, attr) -> None:
|
|
@@ -125,6 +162,11 @@ class CameraStatusData(DeviceStatusData):
|
|
|
125
162
|
self.occupancy = b
|
|
126
163
|
if (v := attr.get("SDcardStatus")) is not None:
|
|
127
164
|
self.sd_card_status = str(v)
|
|
165
|
+
# Only overwrite on an actual reading. Attribute pushes are partial - a
|
|
166
|
+
# motion notif carries no card keys - so a silent update must leave what
|
|
167
|
+
# the last full poll established alone.
|
|
168
|
+
if (b := _card_present(attr)) is not None:
|
|
169
|
+
self.sd_card_present = b
|
|
128
170
|
if (i := _as_int(attr.get("networkRssi"))) is not None:
|
|
129
171
|
self.wifi_rssi = i
|
|
130
172
|
|
{python_aidot_cameras-1.0.0b6 → 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.0b6 → 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.0b6 → 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
|
|
@@ -160,9 +162,11 @@ tests/test_restart_cycle.py
|
|
|
160
162
|
tests/test_retry_policy.py
|
|
161
163
|
tests/test_scripts_importable.py
|
|
162
164
|
tests/test_sctp_init_ack_tsn.py
|
|
165
|
+
tests/test_sd_card_presence.py
|
|
163
166
|
tests/test_sd_event_decode.py
|
|
164
167
|
tests/test_sd_event_probe_payloads.py
|
|
165
168
|
tests/test_sd_listing.py
|
|
169
|
+
tests/test_sd_playback.py
|
|
166
170
|
tests/test_sdes_adaptive.py
|
|
167
171
|
tests/test_sdes_avio_dispatch.py
|
|
168
172
|
tests/test_sdes_bridge_break_gate.py
|