python-aidot-cameras 1.0.0rc44__tar.gz → 1.0.0rc45__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.0rc44/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc45}/PKG-INFO +47 -41
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/README.md +46 -40
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/client.py +88 -16
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/protocol.py +95 -9
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/rtsp_publish.py +2 -1
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sdes_open.py +178 -28
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/ts_fanout.py +23 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/ts_tee.py +11 -3
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/webrtc_open.py +5 -16
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/lan_retry.py +89 -5
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45/python_aidot_cameras.egg-info}/PKG-INFO +47 -41
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/SOURCES.txt +3 -0
- python_aidot_cameras-1.0.0rc45/tests/test_first_media_abandon_rules.py +116 -0
- python_aidot_cameras-1.0.0rc45/tests/test_idle_release_both_loops.py +112 -0
- python_aidot_cameras-1.0.0rc45/tests/test_lan_login_accessors.py +85 -0
- python_aidot_cameras-1.0.0rc45/tests/test_lan_retry_silences_upstream_over_ceiling.py +186 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_validate_policy.py +3 -3
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_first_media_stall_report.py +43 -0
- python_aidot_cameras-1.0.0rc45/tests/test_sdes_idle_release.py +351 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ts_fanout.py +50 -2
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ts_tee.py +137 -1
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_viewer_presence.py +14 -0
- python_aidot_cameras-1.0.0rc44/tests/test_idle_release_both_loops.py +0 -49
- python_aidot_cameras-1.0.0rc44/tests/test_sdes_idle_release.py +0 -157
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_log_redaction.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/hls_ts.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/recording.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sd_events.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/cloud_auth.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_aac_track.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_answer_pt_kinds.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_answer_section_selection.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_arrival_timestamps.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_auth_error_routing.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_auto_alarm.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_auto_light_settings.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_wake_settles_before_the_command.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ble_gateway_is_gone.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_camera_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_canary_log_names_the_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_count.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_error_log_names_the_exception.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_error_severity.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_plan.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_declared_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_detection_types.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_ts_keyframe_signal.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_ts_serve.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_ts_serve_only_for_http.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_drain_is_cancellable.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_recorder_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_serve_ice_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_tap_states_no_reorder.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_ts_copy_recording.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_video_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_feature_probe_needs_a_live_session.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_h264_sps.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_hls_ts_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_hls_ts_wiring.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ice_credentials_match_the_app.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_lan_sweep_only_with_lights.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_late_repeats_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_session_carries_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_validate_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_video_canary.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_liveplay_echo_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_local_only_controls.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_log_hygiene.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_path_is_reported.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_client_id_is_exact.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_thread_not_orphaned.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_nack.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_unbounded_hex_dumps.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_off_subnet_candidate.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_on_frame_is_an_av_frame.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_peer_id_class_env.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_peer_id_client_type.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_public_signature_matches_impl.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_publish_target_readiness.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_quality_campaign_in_session.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_reboot_action.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_recent_recordings.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_resent_frames_are_dropped.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_rtp_h264.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_rtsp_publish.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_rtsp_publish_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sctp_reassembly.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sctp_sack.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_card_presence.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_event_decode.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_event_probe_payloads.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_listing.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_playback.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_connection_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_ice_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_nack_bridge.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_pinned_codec_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_session_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_input_timeout.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_pt_respects_the_pin.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_session_stop_twice.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_skip_guard.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_unstable_public.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stable_terminal_id.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_nudge_policy.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_reports_reach_the_artifact.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_status_and_trace_channels.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_tap_to_mux_end_to_end.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_tmmbr.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_transport_failure_reporting.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_upstream_seam_adapter.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_upstream_secrets_redacted.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_validator_shares_one_token.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_vendored_aiortc_integrity.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_presence_needs_a_keyframe.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_pts_dts.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_ts_unwrap.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_webrtc_answer_race.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0rc45
|
|
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
|
|
@@ -100,11 +100,11 @@ not going to change in this release. Neither is a fault to report.
|
|
|
100
100
|
records to the cloud or to its own SD card is per-camera, not per-model, and the
|
|
101
101
|
device reports it as `IsSupportPlayback`. This library lists cloud recordings
|
|
102
102
|
(`async_get_cloud_recordings`) and resolves a playable HLS URL for one
|
|
103
|
-
(`async_get_event_video_media`);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
(`async_get_event_video_media`); for a camera storing to a card it lists the
|
|
104
|
+
on-device recordings (`async_get_sd_recordings`) but cannot play one back, so
|
|
105
|
+
such a camera records normally and nothing here can show you the video. On the
|
|
106
|
+
reference fleet that is four of seven cameras. Playback from the card is closed
|
|
107
|
+
as not feasible; see `docs/CAMERAS.md`, "On-device (SD card) recordings".
|
|
108
108
|
|
|
109
109
|
**The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
|
|
110
110
|
app.** Measured from a capture of the app on the same camera: it takes about
|
|
@@ -268,9 +268,9 @@ decides it:
|
|
|
268
268
|
|
|
269
269
|
| your camera | use | why |
|
|
270
270
|
| --- | --- | --- |
|
|
271
|
-
| **DTLS** (mains, e.g. A000088) | `-` |
|
|
271
|
+
| **DTLS** (mains, e.g. A000088) | `'{output}'` or `-` | `{output}` is published by the library itself (direct publish, on by default): the camera's 8 kHz A-law plus an in-process AAC track, no ffmpeg. `-` writes the muxed MPEG-TS with its 48 kHz AAC to stdout instead |
|
|
272
272
|
| **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
|
|
273
|
-
| either, with `AIDOT_DIRECT_PUBLISH=
|
|
273
|
+
| either, with `AIDOT_DIRECT_PUBLISH=0` | `'{output}'` | the push goes through ffmpeg instead, which has to transcode audio down to 8 kHz G.711. A session that negotiates H.265 takes this path whatever the setting |
|
|
274
274
|
| either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
|
|
275
275
|
|
|
276
276
|
`'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
|
|
@@ -301,12 +301,13 @@ go2rtc itself gets its environment: export them in the shell before starting it,
|
|
|
301
301
|
or set them in its service unit or container definition. If a stream fails
|
|
302
302
|
immediately with an authentication error, that is the thing to check first.
|
|
303
303
|
|
|
304
|
-
**ffmpeg must be on PATH.**
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
**ffmpeg must be on PATH.** pip cannot install a system binary, and the
|
|
305
|
+
library still needs one: recordings, snapshots, the `-` stdout form, `http://`
|
|
306
|
+
pull serves and any session that negotiates H.265 run through an ffmpeg
|
|
307
|
+
subprocess, and it is the DTLS fallback when a serve port cannot be bound. A
|
|
308
|
+
live `{output}` push does not use it (next paragraph), and a DTLS `-` serve
|
|
309
|
+
sends the muxed MPEG-TS straight to the consumer, because that hop was the only
|
|
310
|
+
component in the chain that lost timestamps.
|
|
310
311
|
|
|
311
312
|
**Live pushes skip ffmpeg: direct publish is on by default** (since 1.0.0rc44;
|
|
312
313
|
`AIDOT_DIRECT_PUBLISH=0` restores the ffmpeg path). An `rtsp://` / `{output}`
|
|
@@ -316,8 +317,8 @@ camera can use `{output}` too. It takes H.264 sessions only; a session that
|
|
|
316
317
|
negotiates H.265 keeps the ffmpeg serve. Audio goes out as the camera's own
|
|
317
318
|
G.711 A-law plus an in-process AAC track (see "Audio" under
|
|
318
319
|
[Getting an RTSP URL](#getting-an-rtsp-url)).
|
|
319
|
-
ffmpeg is still used for recordings and snapshots
|
|
320
|
-
|
|
320
|
+
ffmpeg is still used for recordings and snapshots; see
|
|
321
|
+
[`docs/DESIGN-direct-publish.md`](docs/DESIGN-direct-publish.md).
|
|
321
322
|
|
|
322
323
|
```bash
|
|
323
324
|
sudo apt install ffmpeg # Debian/Ubuntu
|
|
@@ -503,14 +504,16 @@ the last one leaves - nothing talks to your cameras while nobody is watching:
|
|
|
503
504
|
|
|
504
505
|
```yaml
|
|
505
506
|
streams:
|
|
506
|
-
driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
|
|
507
|
+
driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera (stdout form; {output} works too)
|
|
507
508
|
frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
|
|
508
509
|
```
|
|
509
510
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
`{output}` works for both transports: the library publishes the stream into
|
|
512
|
+
go2rtc itself (direct publish, on by default since 1.0.0rc44). A DTLS camera
|
|
513
|
+
can also take `-`, which hands go2rtc the muxed MPEG-TS on stdout; an SDES
|
|
514
|
+
camera cannot, and `--list` told you which is which. (`{output}` is a
|
|
515
|
+
placeholder go2rtc fills in with its own publish URL. It is not something you
|
|
516
|
+
type at a shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
|
|
514
517
|
variables the CLI does, so set them wherever go2rtc starts.
|
|
515
518
|
|
|
516
519
|
**3. Use the URL.** go2rtc serves every stream you named at:
|
|
@@ -532,20 +535,20 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
|
532
535
|
- **Which camera is which.** `--list` reports the transport per device. DTLS is
|
|
533
536
|
the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
|
|
534
537
|
form is not silent - the CLI tells you which one to use and exits.
|
|
535
|
-
- **Audio.**
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
source is needed for
|
|
538
|
+
- **Audio.** A `{output}` push (direct publish, the default) carries the
|
|
539
|
+
camera's own **PCMA (G.711 A-law, 8 kHz)** first - WebRTC viewers play it
|
|
540
|
+
natively and nothing is transcoded - and, unless `AIDOT_PUBLISH_AAC=0`, an
|
|
541
|
+
**AAC-LC 48 kHz mono** track encoded in-process; an AAC-only consumer (for
|
|
542
|
+
example Home Assistant's HLS player) selects it with `?audio=aac`. The `-`
|
|
543
|
+
form carries AAC at 48 kHz mono, resampled from the camera's 8 kHz A-law,
|
|
544
|
+
because 8 kHz AAC plays silent in a lot of browsers and browsers on the MSE
|
|
545
|
+
path have no mapping for G.711 at all. Audio is on by default; see
|
|
546
|
+
`AIDOT_SDES_SERVE_AUDIO` to turn it off. No ffmpeg transcode
|
|
547
|
+
source is needed for the AAC - see
|
|
545
548
|
[`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#an-aac-transcoding-source-makes-every-player-crawl)
|
|
546
549
|
for why the old `ffmpeg:<stream>#audio=aac` advice made playback crawl.
|
|
547
|
-
The AAC track is
|
|
548
|
-
gets no AAC track either way.
|
|
550
|
+
The AAC track is on by default since 1.0.0rc44. A camera whose audio is
|
|
551
|
+
mu-law (PCMU) gets no AAC track either way.
|
|
549
552
|
- **A stream that starts with no video** and picks it up a few seconds later is
|
|
550
553
|
normal: the mux waits for a keyframe so the first GOP is decodable. A stream
|
|
551
554
|
that stays audio-only is a camera that never sent one - retry the view.
|
|
@@ -554,9 +557,11 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
|
554
557
|
spawns. That process has to stay running to keep the port bound, so it holds
|
|
555
558
|
a camera session whether or not anyone is watching. Prefer the `exec:` forms.
|
|
556
559
|
- **Pushing to your own RTSP server** rather than go2rtc: `rtsp_push_url=`
|
|
557
|
-
from Python, or an `rtsp://` output on the CLI. Both transports support it
|
|
558
|
-
|
|
559
|
-
|
|
560
|
+
from Python, or an `rtsp://` output on the CLI. Both transports support it,
|
|
561
|
+
and by default the library publishes it directly, with the audio described
|
|
562
|
+
above. Only on the `AIDOT_DIRECT_PUBLISH=0` ffmpeg path does a DTLS push
|
|
563
|
+
transcode audio to G.711 A-law - ffmpeg's RTSP muxer will not accept the AAC
|
|
564
|
+
that MPEG-TS carries.
|
|
560
565
|
`output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
|
|
561
566
|
address above, and `ensure_stream(name, source)` registers a stream with a
|
|
562
567
|
running go2rtc if you would rather not edit YAML.
|
|
@@ -583,7 +588,7 @@ The library reads the following environment variables.
|
|
|
583
588
|
|
|
584
589
|
### Credentials
|
|
585
590
|
|
|
586
|
-
Used by the credential helper (`
|
|
591
|
+
Used by the credential helper (`aidot_cameras.credentials`); they take priority over any
|
|
587
592
|
stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
|
|
588
593
|
|
|
589
594
|
| Variable | Purpose | Default |
|
|
@@ -604,13 +609,13 @@ knob is in
|
|
|
604
609
|
| `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
|
|
605
610
|
| `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
|
|
606
611
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
607
|
-
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all
|
|
612
|
+
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all, though battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
608
613
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | `96` (H.264; `none` leaves the camera its choice) |
|
|
609
614
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
610
|
-
| `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve
|
|
611
|
-
| `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless.
|
|
615
|
+
| `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve. Live-validated with H.264 on A000088, A001064 and A001513 (see `docs/DESIGN-direct-publish.md`). Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
|
|
616
|
+
| `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless. **On by default** (since 1.0.0rc44); set `0`/`false`/`no`/`off` to turn it off. | `1` (on) |
|
|
612
617
|
| `AIDOT_HLS_DIRECT_TS` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Give each DTLS camera a library-muxed MPEG-TS on a loopback listener (`CameraMixin.hls_ts_url()`), for Home Assistant's HLS view and recordings to read instead of go2rtc's RTSP. go2rtc re-bases each track for each consumer, so a reader that joins a running stream had its sound 0.1-0.75 s late, differently each time; this TS carries the publisher's video and AAC on one clock, so every reader gets them in the same step, whenever it joins. Needs `AIDOT_DIRECT_PUBLISH` and `AIDOT_PUBLISH_AAC`. An SDES camera gets a URL only on a model whose media the bridge decrypts (A001064, A001513) with its offer pinned to H.264 (`AIDOT_SDES_VIDEO_PT=96`), so that every session feeds it; pulled (non-publishing) sessions get none. When a camera's SPS changes (the A001064 changes it between sessions), the TS's readers are disconnected so they rebuild their decoder. Each camera's URL carries a secret made per listener, as `?auth=<secret>` (Home Assistant masks that query parameter when it logs a stream's URL); a request without it gets 404. For the owner's own tools, the listener's URL for any camera (`http://127.0.0.1:<port>/{name}.ts?auth=<secret>`, where `{name}` is `aidot_<first 12 of the device id>`) is left in `hls-ts-url` in the library's state directory (`AIDOT_SPROP_DIR`), owner-readable only, while the listener runs. A reader of the TS counts as a viewer. Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
|
|
613
|
-
| `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone
|
|
618
|
+
| `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone: discovery and the library's own retry loop stop kicking it, and when something else still asks for a login (Home Assistant's light poll does, every five minutes) the attempt is made quietly - upstream's per-attempt warning is dropped - until the device's address changes or a login gets through. One warning marks the moment. Applies to every device, camera or not - the devices that hit this are lights on another subnet. | `6` |
|
|
614
619
|
|
|
615
620
|
### Security hardening
|
|
616
621
|
|
|
@@ -623,3 +628,4 @@ the permissive default.
|
|
|
623
628
|
| --- | --- | --- |
|
|
624
629
|
| `AIDOT_ALLOW_LAN_SERVE` | Silences the warning emitted when decrypted media is served on a non-loopback bind (e.g. `0.0.0.0`), where any host on the LAN can read the unencrypted stream. Set when an exposed bind is intentional. | unset (warn on non-loopback) |
|
|
625
630
|
| `AIDOT_CRED_KEY_FILE` | Path to the Fernet key file for stored credentials. Point it outside the config dir (ideally a separate secret store) so the key isn't co-located with the ciphertext. Applies to the default credentials path only (ignored when an explicit `creds_path` is passed). | `$XDG_CONFIG_HOME/aidot/.key` (falls back to `~/.config/aidot/.key`) |
|
|
631
|
+
| `AIDOT_PLAYBACK_TLS_VERIFY` | Set to `1` to require full certificate and hostname verification on the TLS connection to a camera's playback server. The camera presents a self-signed certificate, so this needs a trust anchor it chains to; unset, verification is skipped and a one-time warning is logged. | unset (skip verification, warn once) |
|
|
@@ -60,11 +60,11 @@ not going to change in this release. Neither is a fault to report.
|
|
|
60
60
|
records to the cloud or to its own SD card is per-camera, not per-model, and the
|
|
61
61
|
device reports it as `IsSupportPlayback`. This library lists cloud recordings
|
|
62
62
|
(`async_get_cloud_recordings`) and resolves a playable HLS URL for one
|
|
63
|
-
(`async_get_event_video_media`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
(`async_get_event_video_media`); for a camera storing to a card it lists the
|
|
64
|
+
on-device recordings (`async_get_sd_recordings`) but cannot play one back, so
|
|
65
|
+
such a camera records normally and nothing here can show you the video. On the
|
|
66
|
+
reference fleet that is four of seven cameras. Playback from the card is closed
|
|
67
|
+
as not feasible; see `docs/CAMERAS.md`, "On-device (SD card) recordings".
|
|
68
68
|
|
|
69
69
|
**The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
|
|
70
70
|
app.** Measured from a capture of the app on the same camera: it takes about
|
|
@@ -228,9 +228,9 @@ decides it:
|
|
|
228
228
|
|
|
229
229
|
| your camera | use | why |
|
|
230
230
|
| --- | --- | --- |
|
|
231
|
-
| **DTLS** (mains, e.g. A000088) | `-` |
|
|
231
|
+
| **DTLS** (mains, e.g. A000088) | `'{output}'` or `-` | `{output}` is published by the library itself (direct publish, on by default): the camera's 8 kHz A-law plus an in-process AAC track, no ffmpeg. `-` writes the muxed MPEG-TS with its 48 kHz AAC to stdout instead |
|
|
232
232
|
| **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
|
|
233
|
-
| either, with `AIDOT_DIRECT_PUBLISH=
|
|
233
|
+
| either, with `AIDOT_DIRECT_PUBLISH=0` | `'{output}'` | the push goes through ffmpeg instead, which has to transcode audio down to 8 kHz G.711. A session that negotiates H.265 takes this path whatever the setting |
|
|
234
234
|
| either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
|
|
235
235
|
|
|
236
236
|
`'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
|
|
@@ -261,12 +261,13 @@ go2rtc itself gets its environment: export them in the shell before starting it,
|
|
|
261
261
|
or set them in its service unit or container definition. If a stream fails
|
|
262
262
|
immediately with an authentication error, that is the thing to check first.
|
|
263
263
|
|
|
264
|
-
**ffmpeg must be on PATH.**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
264
|
+
**ffmpeg must be on PATH.** pip cannot install a system binary, and the
|
|
265
|
+
library still needs one: recordings, snapshots, the `-` stdout form, `http://`
|
|
266
|
+
pull serves and any session that negotiates H.265 run through an ffmpeg
|
|
267
|
+
subprocess, and it is the DTLS fallback when a serve port cannot be bound. A
|
|
268
|
+
live `{output}` push does not use it (next paragraph), and a DTLS `-` serve
|
|
269
|
+
sends the muxed MPEG-TS straight to the consumer, because that hop was the only
|
|
270
|
+
component in the chain that lost timestamps.
|
|
270
271
|
|
|
271
272
|
**Live pushes skip ffmpeg: direct publish is on by default** (since 1.0.0rc44;
|
|
272
273
|
`AIDOT_DIRECT_PUBLISH=0` restores the ffmpeg path). An `rtsp://` / `{output}`
|
|
@@ -276,8 +277,8 @@ camera can use `{output}` too. It takes H.264 sessions only; a session that
|
|
|
276
277
|
negotiates H.265 keeps the ffmpeg serve. Audio goes out as the camera's own
|
|
277
278
|
G.711 A-law plus an in-process AAC track (see "Audio" under
|
|
278
279
|
[Getting an RTSP URL](#getting-an-rtsp-url)).
|
|
279
|
-
ffmpeg is still used for recordings and snapshots
|
|
280
|
-
|
|
280
|
+
ffmpeg is still used for recordings and snapshots; see
|
|
281
|
+
[`docs/DESIGN-direct-publish.md`](docs/DESIGN-direct-publish.md).
|
|
281
282
|
|
|
282
283
|
```bash
|
|
283
284
|
sudo apt install ffmpeg # Debian/Ubuntu
|
|
@@ -463,14 +464,16 @@ the last one leaves - nothing talks to your cameras while nobody is watching:
|
|
|
463
464
|
|
|
464
465
|
```yaml
|
|
465
466
|
streams:
|
|
466
|
-
driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
|
|
467
|
+
driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera (stdout form; {output} works too)
|
|
467
468
|
frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
|
|
468
469
|
```
|
|
469
470
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
471
|
+
`{output}` works for both transports: the library publishes the stream into
|
|
472
|
+
go2rtc itself (direct publish, on by default since 1.0.0rc44). A DTLS camera
|
|
473
|
+
can also take `-`, which hands go2rtc the muxed MPEG-TS on stdout; an SDES
|
|
474
|
+
camera cannot, and `--list` told you which is which. (`{output}` is a
|
|
475
|
+
placeholder go2rtc fills in with its own publish URL. It is not something you
|
|
476
|
+
type at a shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
|
|
474
477
|
variables the CLI does, so set them wherever go2rtc starts.
|
|
475
478
|
|
|
476
479
|
**3. Use the URL.** go2rtc serves every stream you named at:
|
|
@@ -492,20 +495,20 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
|
492
495
|
- **Which camera is which.** `--list` reports the transport per device. DTLS is
|
|
493
496
|
the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
|
|
494
497
|
form is not silent - the CLI tells you which one to use and exits.
|
|
495
|
-
- **Audio.**
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
source is needed for
|
|
498
|
+
- **Audio.** A `{output}` push (direct publish, the default) carries the
|
|
499
|
+
camera's own **PCMA (G.711 A-law, 8 kHz)** first - WebRTC viewers play it
|
|
500
|
+
natively and nothing is transcoded - and, unless `AIDOT_PUBLISH_AAC=0`, an
|
|
501
|
+
**AAC-LC 48 kHz mono** track encoded in-process; an AAC-only consumer (for
|
|
502
|
+
example Home Assistant's HLS player) selects it with `?audio=aac`. The `-`
|
|
503
|
+
form carries AAC at 48 kHz mono, resampled from the camera's 8 kHz A-law,
|
|
504
|
+
because 8 kHz AAC plays silent in a lot of browsers and browsers on the MSE
|
|
505
|
+
path have no mapping for G.711 at all. Audio is on by default; see
|
|
506
|
+
`AIDOT_SDES_SERVE_AUDIO` to turn it off. No ffmpeg transcode
|
|
507
|
+
source is needed for the AAC - see
|
|
505
508
|
[`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#an-aac-transcoding-source-makes-every-player-crawl)
|
|
506
509
|
for why the old `ffmpeg:<stream>#audio=aac` advice made playback crawl.
|
|
507
|
-
The AAC track is
|
|
508
|
-
gets no AAC track either way.
|
|
510
|
+
The AAC track is on by default since 1.0.0rc44. A camera whose audio is
|
|
511
|
+
mu-law (PCMU) gets no AAC track either way.
|
|
509
512
|
- **A stream that starts with no video** and picks it up a few seconds later is
|
|
510
513
|
normal: the mux waits for a keyframe so the first GOP is decodable. A stream
|
|
511
514
|
that stays audio-only is a camera that never sent one - retry the view.
|
|
@@ -514,9 +517,11 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
|
|
|
514
517
|
spawns. That process has to stay running to keep the port bound, so it holds
|
|
515
518
|
a camera session whether or not anyone is watching. Prefer the `exec:` forms.
|
|
516
519
|
- **Pushing to your own RTSP server** rather than go2rtc: `rtsp_push_url=`
|
|
517
|
-
from Python, or an `rtsp://` output on the CLI. Both transports support it
|
|
518
|
-
|
|
519
|
-
|
|
520
|
+
from Python, or an `rtsp://` output on the CLI. Both transports support it,
|
|
521
|
+
and by default the library publishes it directly, with the audio described
|
|
522
|
+
above. Only on the `AIDOT_DIRECT_PUBLISH=0` ffmpeg path does a DTLS push
|
|
523
|
+
transcode audio to G.711 A-law - ffmpeg's RTSP muxer will not accept the AAC
|
|
524
|
+
that MPEG-TS carries.
|
|
520
525
|
`output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
|
|
521
526
|
address above, and `ensure_stream(name, source)` registers a stream with a
|
|
522
527
|
running go2rtc if you would rather not edit YAML.
|
|
@@ -543,7 +548,7 @@ The library reads the following environment variables.
|
|
|
543
548
|
|
|
544
549
|
### Credentials
|
|
545
550
|
|
|
546
|
-
Used by the credential helper (`
|
|
551
|
+
Used by the credential helper (`aidot_cameras.credentials`); they take priority over any
|
|
547
552
|
stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
|
|
548
553
|
|
|
549
554
|
| Variable | Purpose | Default |
|
|
@@ -564,13 +569,13 @@ knob is in
|
|
|
564
569
|
| `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
|
|
565
570
|
| `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
|
|
566
571
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
567
|
-
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all
|
|
572
|
+
| `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all, though battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
|
|
568
573
|
| `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | `96` (H.264; `none` leaves the camera its choice) |
|
|
569
574
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
570
|
-
| `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve
|
|
571
|
-
| `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless.
|
|
575
|
+
| `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve. Live-validated with H.264 on A000088, A001064 and A001513 (see `docs/DESIGN-direct-publish.md`). Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
|
|
576
|
+
| `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless. **On by default** (since 1.0.0rc44); set `0`/`false`/`no`/`off` to turn it off. | `1` (on) |
|
|
572
577
|
| `AIDOT_HLS_DIRECT_TS` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Give each DTLS camera a library-muxed MPEG-TS on a loopback listener (`CameraMixin.hls_ts_url()`), for Home Assistant's HLS view and recordings to read instead of go2rtc's RTSP. go2rtc re-bases each track for each consumer, so a reader that joins a running stream had its sound 0.1-0.75 s late, differently each time; this TS carries the publisher's video and AAC on one clock, so every reader gets them in the same step, whenever it joins. Needs `AIDOT_DIRECT_PUBLISH` and `AIDOT_PUBLISH_AAC`. An SDES camera gets a URL only on a model whose media the bridge decrypts (A001064, A001513) with its offer pinned to H.264 (`AIDOT_SDES_VIDEO_PT=96`), so that every session feeds it; pulled (non-publishing) sessions get none. When a camera's SPS changes (the A001064 changes it between sessions), the TS's readers are disconnected so they rebuild their decoder. Each camera's URL carries a secret made per listener, as `?auth=<secret>` (Home Assistant masks that query parameter when it logs a stream's URL); a request without it gets 404. For the owner's own tools, the listener's URL for any camera (`http://127.0.0.1:<port>/{name}.ts?auth=<secret>`, where `{name}` is `aidot_<first 12 of the device id>`) is left in `hls-ts-url` in the library's state directory (`AIDOT_SPROP_DIR`), owner-readable only, while the listener runs. A reader of the TS counts as a viewer. Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
|
|
573
|
-
| `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone
|
|
578
|
+
| `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone: discovery and the library's own retry loop stop kicking it, and when something else still asks for a login (Home Assistant's light poll does, every five minutes) the attempt is made quietly - upstream's per-attempt warning is dropped - until the device's address changes or a login gets through. One warning marks the moment. Applies to every device, camera or not - the devices that hit this are lights on another subnet. | `6` |
|
|
574
579
|
|
|
575
580
|
### Security hardening
|
|
576
581
|
|
|
@@ -583,3 +588,4 @@ the permissive default.
|
|
|
583
588
|
| --- | --- | --- |
|
|
584
589
|
| `AIDOT_ALLOW_LAN_SERVE` | Silences the warning emitted when decrypted media is served on a non-loopback bind (e.g. `0.0.0.0`), where any host on the LAN can read the unencrypted stream. Set when an exposed bind is intentional. | unset (warn on non-loopback) |
|
|
585
590
|
| `AIDOT_CRED_KEY_FILE` | Path to the Fernet key file for stored credentials. Point it outside the config dir (ideally a separate secret store) so the key isn't co-located with the ciphertext. Applies to the default credentials path only (ignored when an explicit `creds_path` is passed). | `$XDG_CONFIG_HOME/aidot/.key` (falls back to `~/.config/aidot/.key`) |
|
|
591
|
+
| `AIDOT_PLAYBACK_TLS_VERIFY` | Set to `1` to require full certificate and hostname verification on the TLS connection to a camera's playback server. The camera presents a self-signed certificate, so this needs a trust anchor it chains to; unset, verification is skipped and a one-time warning is logged. | unset (skip verification, warn once) |
|
{python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/client.py
RENAMED
|
@@ -97,6 +97,8 @@ from .protocol import ( # noqa: F401 - used here and/or by the webrtc_open mixi
|
|
|
97
97
|
_grab_free_port,
|
|
98
98
|
_tcp_table_has_established_on_port,
|
|
99
99
|
_idle_release_due,
|
|
100
|
+
_battery_unknown_release_s,
|
|
101
|
+
_stream_startup_grace_s,
|
|
100
102
|
_DirectTsServer,
|
|
101
103
|
_serve_port,
|
|
102
104
|
_dtls_av_mux_run,
|
|
@@ -4549,8 +4551,10 @@ class CameraMixin(
|
|
|
4549
4551
|
is (the serve port is go2rtc's shared RTSP port, where every camera's own
|
|
4550
4552
|
publisher is connected, so a socket check would report a viewer for every
|
|
4551
4553
|
camera forever). With no signal ``_viewer_present`` answers "unknown",
|
|
4552
|
-
``_idle_release_due``
|
|
4553
|
-
camera
|
|
4554
|
+
and ``_idle_release_due`` never releases a mains camera on unknown; a
|
|
4555
|
+
battery camera is released only after
|
|
4556
|
+
``AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S`` (300 s) without a known
|
|
4557
|
+
viewer, so every view of it ends at that cap.
|
|
4554
4558
|
|
|
4555
4559
|
``stream_idle_s`` sets the no-viewer idle-release window in seconds,
|
|
4556
4560
|
overriding the ``AIDOT_STREAM_IDLE_S`` env default (120). ``0`` (or
|
|
@@ -4619,10 +4623,10 @@ class CameraMixin(
|
|
|
4619
4623
|
# mid-flight, which was observed leaving DTLS cameras producing nothing
|
|
4620
4624
|
# at all. Such a consumer previously had to withhold go2rtc_url
|
|
4621
4625
|
# entirely, which silently disabled viewer detection - and in SDES push
|
|
4622
|
-
# mode that means
|
|
4623
|
-
# ``
|
|
4624
|
-
#
|
|
4625
|
-
#
|
|
4626
|
+
# mode that means ``_viewer_present`` answers "unknown", on which
|
|
4627
|
+
# ``_idle_release_due`` never releases a mains camera and releases a
|
|
4628
|
+
# battery camera only after AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S
|
|
4629
|
+
# without a known viewer - cutting every view of it short at that cap.
|
|
4626
4630
|
if go2rtc_url and go2rtc_register:
|
|
4627
4631
|
self._go2rtc_task = asyncio.ensure_future(self._register_with_go2rtc())
|
|
4628
4632
|
|
|
@@ -4963,7 +4967,8 @@ class CameraMixin(
|
|
|
4963
4967
|
against a 5 minute idle window.
|
|
4964
4968
|
|
|
4965
4969
|
Falls back to the TCP-table check when go2rtc is not in use, and returns
|
|
4966
|
-
None (never
|
|
4970
|
+
None (unknown: never releases a mains camera; a battery camera after
|
|
4971
|
+
AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S) if neither can answer.
|
|
4967
4972
|
"""
|
|
4968
4973
|
# A consumer of the camera's in-sync HLS TS is a viewer that go2rtc
|
|
4969
4974
|
# never sees (it reads from the library, not from go2rtc). Local and
|
|
@@ -5002,7 +5007,8 @@ class CameraMixin(
|
|
|
5002
5007
|
# push mode, which is why a consumer that registers its own stream
|
|
5003
5008
|
# should still pass go2rtc_url with go2rtc_register=False rather than
|
|
5004
5009
|
# withholding it (withholding it silently pins push-mode cameras to
|
|
5005
|
-
# "unknown", and unknown never releases
|
|
5010
|
+
# "unknown", and unknown never releases a mains camera and cuts a
|
|
5011
|
+
# battery camera's views short at AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S).
|
|
5006
5012
|
if base and name:
|
|
5007
5013
|
try:
|
|
5008
5014
|
import aiohttp
|
|
@@ -5262,7 +5268,9 @@ class CameraMixin(
|
|
|
5262
5268
|
# pipe to watch, so detect "nobody is pulling" via an ESTABLISHED TCP
|
|
5263
5269
|
# connection on the -listen serve port and release after the same idle
|
|
5264
5270
|
# window as DTLS; the next view re-runs camera.stream_source(). Fail
|
|
5265
|
-
# safe: unknown (non-Linux /proc) never releases
|
|
5271
|
+
# safe: unknown (non-Linux /proc) never releases a mains camera; a
|
|
5272
|
+
# battery camera releases after AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S
|
|
5273
|
+
# without a known viewer. Escape hatch:
|
|
5266
5274
|
# AIDOT_SDES_IDLE_RELEASE=0.
|
|
5267
5275
|
# stream_idle_s / AIDOT_STREAM_IDLE_S override; <= 0 = never release.
|
|
5268
5276
|
_idle_secs = self._resolve_idle_secs()
|
|
@@ -5271,6 +5279,17 @@ class CameraMixin(
|
|
|
5271
5279
|
)
|
|
5272
5280
|
_serve_port = _sdes_serve_port(self._keepalive_rtsp_url)
|
|
5273
5281
|
_last_consumer = _started_at # grace: count idle from session open
|
|
5282
|
+
# Battery exception to "unknown never releases": a battery camera
|
|
5283
|
+
# whose viewers cannot be counted is released this long after the
|
|
5284
|
+
# last KNOWN viewer, so it cannot stream (and drain) until restart.
|
|
5285
|
+
_battery = bool(getattr(self, "is_battery_camera", False))
|
|
5286
|
+
_unknown_cap = _battery_unknown_release_s()
|
|
5287
|
+
# Start-up grace: until a viewer has been seen, the idle window is
|
|
5288
|
+
# at least AIDOT_STREAM_STARTUP_GRACE_S from the open, so a short
|
|
5289
|
+
# window cannot end a view whose first viewer is still connecting
|
|
5290
|
+
# (a stream worker or recording that retries late on a slow wake).
|
|
5291
|
+
_viewer_seen = False
|
|
5292
|
+
_startup_grace = _stream_startup_grace_s()
|
|
5274
5293
|
try:
|
|
5275
5294
|
while True:
|
|
5276
5295
|
_fin, _ = await asyncio.wait({_done}, timeout=5.0)
|
|
@@ -5309,12 +5328,28 @@ class CameraMixin(
|
|
|
5309
5328
|
_present = await self._viewer_present(_serve_port)
|
|
5310
5329
|
if _present: # True -> a viewer is pulling; stay alive
|
|
5311
5330
|
_last_consumer = time.monotonic()
|
|
5331
|
+
_viewer_seen = True
|
|
5312
5332
|
elif _idle_release_due(
|
|
5313
|
-
_present,
|
|
5333
|
+
_present,
|
|
5334
|
+
_last_consumer,
|
|
5335
|
+
time.monotonic(),
|
|
5336
|
+
_idle_secs,
|
|
5337
|
+
battery=_battery,
|
|
5338
|
+
unknown_cap_s=_unknown_cap,
|
|
5339
|
+
viewer_seen=_viewer_seen,
|
|
5340
|
+
startup_grace_s=_startup_grace,
|
|
5314
5341
|
):
|
|
5342
|
+
if _present is None:
|
|
5343
|
+
_LOGGER.info(
|
|
5344
|
+
"SDES serve: %s released - viewer state "
|
|
5345
|
+
"unknown for %.0f s on a battery camera",
|
|
5346
|
+
self.device_id,
|
|
5347
|
+
time.monotonic() - _last_consumer,
|
|
5348
|
+
)
|
|
5315
5349
|
_idle_release = True
|
|
5316
5350
|
break
|
|
5317
|
-
# _present is None (
|
|
5351
|
+
# _present is None (unknown) -> a mains camera never
|
|
5352
|
+
# releases; a battery camera releases after the cap.
|
|
5318
5353
|
except asyncio.CancelledError:
|
|
5319
5354
|
_done.cancel()
|
|
5320
5355
|
self._stream_session = None
|
|
@@ -6010,8 +6045,8 @@ class CameraMixin(
|
|
|
6010
6045
|
if getattr(self, "is_battery_camera", False):
|
|
6011
6046
|
return False
|
|
6012
6047
|
# The connection mode composes here rather than in a parallel path, so
|
|
6013
|
-
# everything hanging off this resolver (instrumentation
|
|
6014
|
-
#
|
|
6048
|
+
# everything hanging off this resolver (instrumentation included) sees
|
|
6049
|
+
# one answer. relay force-keeps the pre-allocation -
|
|
6015
6050
|
# forcing the relay while skipping its allocation would offer nothing.
|
|
6016
6051
|
return self._resolve_sdes_connection_mode() == "lan"
|
|
6017
6052
|
|
|
@@ -6437,6 +6472,13 @@ class CameraMixin(
|
|
|
6437
6472
|
proc = wfile = stop_flag = mux_thread = None
|
|
6438
6473
|
cancelled = idle_release = False
|
|
6439
6474
|
_pub_fail_n = 0
|
|
6475
|
+
# Start-up grace (see the SDES loop): before the first viewer of
|
|
6476
|
+
# this session, every release comparison in the serve cycle uses at
|
|
6477
|
+
# least AIDOT_STREAM_STARTUP_GRACE_S instead of the idle window.
|
|
6478
|
+
# Once per PC session, not per serve cycle: a cycle restarted after
|
|
6479
|
+
# a viewer left must not get the full grace again.
|
|
6480
|
+
_viewer_seen_dtls = False
|
|
6481
|
+
_grace_dtls = _stream_startup_grace_s()
|
|
6440
6482
|
try:
|
|
6441
6483
|
# (Re)start the ffmpeg serve whenever go2rtc (re)connects, while
|
|
6442
6484
|
# the warm WebRTC session keeps delivering encoded frames.
|
|
@@ -6459,7 +6501,8 @@ class CameraMixin(
|
|
|
6459
6501
|
else:
|
|
6460
6502
|
_ff_port = None
|
|
6461
6503
|
_ff_url = serve_url
|
|
6462
|
-
#
|
|
6504
|
+
# Serve the muxed TS straight to the consumer on any http://
|
|
6505
|
+
# serve URL that direct publish does not take.
|
|
6463
6506
|
# The `-c copy` ffmpeg hop re-packetizes MPEG-TS this mux
|
|
6464
6507
|
# already wrote in the form go2rtc wants, and measured
|
|
6465
6508
|
# 2026-08-20 it is the one component that LOSES timestamps
|
|
@@ -6579,6 +6622,8 @@ class CameraMixin(
|
|
|
6579
6622
|
_stall_pli_armed = True
|
|
6580
6623
|
_last_viewer_dtls = loop.time()
|
|
6581
6624
|
_serve_port_dtls = _sdes_serve_port(serve_url) or 0
|
|
6625
|
+
_battery_dtls = bool(getattr(self, "is_battery_camera", False))
|
|
6626
|
+
_unknown_cap_dtls = _battery_unknown_release_s()
|
|
6582
6627
|
# Video-presence check. The only other liveness test here is
|
|
6583
6628
|
# _pc_dead(), which reads the ICE/PC state - and a session
|
|
6584
6629
|
# receiving audio and no video passes it forever (measured
|
|
@@ -6660,15 +6705,42 @@ class CameraMixin(
|
|
|
6660
6705
|
# cannot answer.
|
|
6661
6706
|
if idle_secs > 0:
|
|
6662
6707
|
_watching = await self._viewer_present(_serve_port_dtls)
|
|
6708
|
+
_window_dtls = (
|
|
6709
|
+
idle_secs
|
|
6710
|
+
if _viewer_seen_dtls
|
|
6711
|
+
else max(idle_secs, _grace_dtls)
|
|
6712
|
+
)
|
|
6663
6713
|
if _watching:
|
|
6664
6714
|
_last_viewer_dtls = _now
|
|
6715
|
+
_viewer_seen_dtls = True
|
|
6665
6716
|
elif _watching is False:
|
|
6666
|
-
if _now - _last_viewer_dtls >
|
|
6717
|
+
if _now - _last_viewer_dtls > _window_dtls:
|
|
6667
6718
|
idle_release = True
|
|
6668
|
-
elif _now - progress[0] >
|
|
6719
|
+
elif _now - progress[0] > _window_dtls:
|
|
6669
6720
|
# Unknown: keep the old staleness heuristic rather
|
|
6670
6721
|
# than holding the stream open forever.
|
|
6671
6722
|
idle_release = True
|
|
6723
|
+
elif _battery_dtls and _idle_release_due(
|
|
6724
|
+
None,
|
|
6725
|
+
_last_viewer_dtls,
|
|
6726
|
+
_now,
|
|
6727
|
+
idle_secs,
|
|
6728
|
+
battery=True,
|
|
6729
|
+
unknown_cap_s=_unknown_cap_dtls,
|
|
6730
|
+
viewer_seen=_viewer_seen_dtls,
|
|
6731
|
+
startup_grace_s=_grace_dtls,
|
|
6732
|
+
):
|
|
6733
|
+
# Unknown on a battery camera, and go2rtc keeps
|
|
6734
|
+
# draining the pipe so staleness never fires:
|
|
6735
|
+
# release after the cap since the last known
|
|
6736
|
+
# viewer rather than streaming until restart.
|
|
6737
|
+
_LOGGER.info(
|
|
6738
|
+
"DTLS serve: %s released - viewer state "
|
|
6739
|
+
"unknown for %.0f s on a battery camera",
|
|
6740
|
+
self.device_id,
|
|
6741
|
+
_now - _last_viewer_dtls,
|
|
6742
|
+
)
|
|
6743
|
+
idle_release = True
|
|
6672
6744
|
# Only an actual idle release ends this serve cycle.
|
|
6673
6745
|
# (This break was unconditional - any idle_secs > 0
|
|
6674
6746
|
# tore down a HEALTHY ffmpeg after one 0.5s tick,
|