python-aidot-cameras 0.15.7__tar.gz → 0.16.0__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.
Files changed (182) hide show
  1. {python_aidot_cameras-0.15.7/python_aidot_cameras.egg-info → python_aidot_cameras-0.16.0}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/client.py +30 -1
  3. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/protocol.py +232 -1
  4. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/sdes.py +8 -1
  5. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/sdes_open.py +60 -0
  6. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/webrtc.py +10 -1
  7. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/webrtc_open.py +13 -0
  8. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/pyproject.toml +1 -1
  9. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  10. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/SOURCES.txt +5 -0
  11. python_aidot_cameras-0.16.0/tests/test_avio_request.py +221 -0
  12. python_aidot_cameras-0.16.0/tests/test_avio_response_parse.py +91 -0
  13. python_aidot_cameras-0.16.0/tests/test_avio_response_router.py +207 -0
  14. python_aidot_cameras-0.16.0/tests/test_sdes_avio_dispatch.py +117 -0
  15. python_aidot_cameras-0.16.0/tests/test_serve_loss_tolerance.py +70 -0
  16. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/LICENSE +0 -0
  17. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/MANIFEST.in +0 -0
  18. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/README.md +0 -0
  19. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/__init__.py +0 -0
  20. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/__main__.py +0 -0
  21. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_upstream.py +0 -0
  22. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/__init__.py +0 -0
  23. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  24. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  25. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  26. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  27. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  28. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  29. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  30. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  31. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  32. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  33. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  34. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  35. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  36. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  37. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  38. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  39. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  40. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  41. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  42. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  43. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  44. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  45. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  46. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  47. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  48. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  49. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  50. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  51. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  52. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  53. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  54. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  55. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/ble_gateway.py +0 -0
  56. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/__init__.py +0 -0
  57. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/constants.py +0 -0
  58. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/controls.py +0 -0
  59. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/go2rtc.py +0 -0
  60. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/hwaccel.py +0 -0
  61. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/lan_control.py +0 -0
  62. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/models.py +0 -0
  63. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/playback.py +0 -0
  64. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/camera/tutk.py +0 -0
  65. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/client.py +0 -0
  66. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/const.py +0 -0
  67. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/credentials.py +0 -0
  68. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/crypto.py +0 -0
  69. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/device_client.py +0 -0
  70. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/discover.py +0 -0
  71. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/exceptions.py +0 -0
  72. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/g711.py +0 -0
  73. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/aidot_cameras/py.typed +0 -0
  74. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  75. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  76. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/requires.txt +0 -0
  77. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  78. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/setup.cfg +0 -0
  79. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_aioice_compat.py +0 -0
  80. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_alarm_event.py +0 -0
  81. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_answer_sdp_video_pt.py +0 -0
  82. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_audio_mux_offline.py +0 -0
  83. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_backoff.py +0 -0
  84. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_battery_detection.py +0 -0
  85. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_battery_evidence_corroboration.py +0 -0
  86. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_battery_skip_turn.py +0 -0
  87. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_ble_gateway.py +0 -0
  88. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_bridge_state_sentinels.py +0 -0
  89. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_constants_imports_resolve.py +0 -0
  90. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_deferred_hardening.py +0 -0
  91. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_device_login_guard.py +0 -0
  92. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_device_user_info_cache.py +0 -0
  93. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dormant_camera_cleanup.py +0 -0
  94. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_mux_terminal_write.py +0 -0
  95. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_not_ready_burst.py +0 -0
  96. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_pinning.py +0 -0
  97. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_serve_decode_skip.py +0 -0
  98. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_serve_open_timeout.py +0 -0
  99. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_skip_signaling_wait.py +0 -0
  100. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_slow_probe.py +0 -0
  101. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_dtls_stdout_producer.py +0 -0
  102. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_egress_guard.py +0 -0
  103. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_env_seams.py +0 -0
  104. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_go2rtc.py +0 -0
  105. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_go2rtc_cli.py +0 -0
  106. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_h264_decode_rate_limit.py +0 -0
  107. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_highport_nomination.py +0 -0
  108. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_hwaccel_probe.py +0 -0
  109. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_ice_config_cache.py +0 -0
  110. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_ice_peer_reflexive.py +0 -0
  111. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_idle_release_both_loops.py +0 -0
  112. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_keepalive_renew.py +0 -0
  113. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_key_restart_sdp.py +0 -0
  114. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_keyframe_prompter.py +0 -0
  115. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_lan_control.py +0 -0
  116. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_light_active_color_mode.py +0 -0
  117. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_live_stream_param.py +0 -0
  118. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_live_validate_policy.py +0 -0
  119. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_logging_caps.py +0 -0
  120. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_media_socket_rcvbuf.py +0 -0
  121. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_media_wait_floor.py +0 -0
  122. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_motion_poll.py +0 -0
  123. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_credential_refetch.py +0 -0
  124. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_credential_self_heal.py +0 -0
  125. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_password_not_persisted.py +0 -0
  126. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_mqtt_session_reconnect.py +0 -0
  127. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_narrow_pc_ice.py +0 -0
  128. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_no_self_referential_go2rtc.py +0 -0
  129. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_no_undefined_names.py +0 -0
  130. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_offline_keepalive_pause.py +0 -0
  131. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_open_fail_logger.py +0 -0
  132. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_open_gate_delay.py +0 -0
  133. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_peerid_reuse.py +0 -0
  134. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_persistent_mqtt.py +0 -0
  135. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_persistent_mqtt_robustness.py +0 -0
  136. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_playback_tls.py +0 -0
  137. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_post_merge_hardening.py +0 -0
  138. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_resolution_persists.py +0 -0
  139. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_restart_cycle.py +0 -0
  140. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_retry_policy.py +0 -0
  141. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_scripts_importable.py +0 -0
  142. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_adaptive.py +0 -0
  143. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_bridge_break_gate.py +0 -0
  144. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_echo_wait_timeout.py +0 -0
  145. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_fast_liveplay.py +0 -0
  146. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  147. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_idle_release.py +0 -0
  148. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_open_cleanup.py +0 -0
  149. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_serve_audio.py +0 -0
  150. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_serve_cmd.py +0 -0
  151. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_serve_stderr_drain.py +0 -0
  152. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_sprop.py +0 -0
  153. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_talk.py +0 -0
  154. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_teardown_exit_log.py +0 -0
  155. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_terminal_ack.py +0 -0
  156. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_terminal_ack_latency.py +0 -0
  157. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdes_watchdog.py +0 -0
  158. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdp_narrowing_properties.py +0 -0
  159. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sdp_payload_type_narrowing.py +0 -0
  160. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_security_hardening.py +0 -0
  161. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_serializable_login_info.py +0 -0
  162. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_serve_relay.py +0 -0
  163. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_session_stats.py +0 -0
  164. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_shared_house_seam.py +0 -0
  165. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_speak.py +0 -0
  166. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_sprop_staleness.py +0 -0
  167. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_cap.py +0 -0
  168. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_drain_ownership.py +0 -0
  169. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_idle.py +0 -0
  170. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_slot_cap.py +0 -0
  171. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_stream_teardown.py +0 -0
  172. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_talk.py +0 -0
  173. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_terminal_ack.py +0 -0
  174. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_token_file_robustness.py +0 -0
  175. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_token_refresh.py +0 -0
  176. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_turn_create_permission.py +0 -0
  177. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_upstream_compat.py +0 -0
  178. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_viewer_check_throttle.py +0 -0
  179. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_viewer_presence.py +0 -0
  180. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_viewer_present_push_mode.py +0 -0
  181. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_wait_or_event.py +0 -0
  182. {python_aidot_cameras-0.15.7 → python_aidot_cameras-0.16.0}/tests/test_wake_readiness_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.15.7
3
+ Version: 0.16.0
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
@@ -365,6 +365,18 @@ def _is_self_referential_source(source: str, stream_name: str) -> bool:
365
365
  return path == stream_name
366
366
 
367
367
 
368
+ #: Packets the serve's demuxer may hold while it waits for a gap to fill. A
369
+ #: keyframe on an A001064 is 146-190 KB - roughly 130 packets - so anything
370
+ #: smaller than a few hundred cannot ride out a burst that arrives out of order.
371
+ #: Env-overridable because the right value depends on the link, not on us.
372
+ _SERVE_REORDER_QUEUE = int(os.environ.get("AIDOT_SERVE_REORDER_QUEUE", "500"))
373
+
374
+ #: How long (microseconds) the demuxer waits for a missing packet before moving
375
+ #: on. Bounded on purpose: waiting forever turns loss into a stall, which is the
376
+ #: failure this is meant to prevent.
377
+ _SERVE_MAX_DELAY_US = int(os.environ.get("AIDOT_SERVE_MAX_DELAY_US", "500000"))
378
+
379
+
368
380
  def _build_sdes_serve_cmd(
369
381
  *,
370
382
  sdp_path: str,
@@ -470,9 +482,26 @@ def _build_sdes_serve_cmd(
470
482
  # 2 s analyzeduration: the camera sends SPS+PPS+IDR in the first burst;
471
483
  # 15 s consumed nearly all packets during analysis. PLI re-arms an IDR
472
484
  # every 5 s so parameters always re-arrive.
473
- "-fflags", "+nobuffer+genpts",
485
+ #
486
+ # ``discardcorrupt`` is the loss half of this: both ends of this path are
487
+ # wireless (the camera, and typically the host), so RTP arrives with real
488
+ # gaps. Measured on the reference install: `RTP: missed N packets` with
489
+ # ZERO drops on every socket in the path, which means the packets were
490
+ # lost before they ever reached the box. Without this ffmpeg passes the
491
+ # damaged frame downstream and then backs its input queue up behind the
492
+ # reordering it thinks it needs - "max delay reached. need to consume
493
+ # packet" - and the serve eventually dies, taking every viewer with it.
494
+ # A brief artefact is a far better outcome than a dropped stream.
495
+ "-fflags", "+nobuffer+genpts+discardcorrupt",
474
496
  "-analyzeduration", "2000000",
475
497
  "-probesize", "500000",
498
+ # Reordering headroom. The default (-1) leaves the demuxer with almost
499
+ # none, so a burst that arrives out of order reads as loss. A keyframe
500
+ # here is 146-190 KB - about 130 packets - so the queue has to be able to
501
+ # hold more than one of them, and max_delay bounds how long it will wait
502
+ # before giving up on a gap rather than stalling the pipeline.
503
+ "-reorder_queue_size", str(_SERVE_REORDER_QUEUE),
504
+ "-max_delay", str(_SERVE_MAX_DELAY_US),
476
505
  "-i", sdp_path,
477
506
  *dest_args,
478
507
  ]
@@ -8,6 +8,7 @@ imports client.py -- the import edge is one-way (client -> protocol).
8
8
  """
9
9
 
10
10
  import asyncio
11
+ import concurrent.futures as _cfutures
11
12
  import hashlib
12
13
  import ipaddress
13
14
  import json
@@ -22,7 +23,7 @@ import tempfile
22
23
  import threading
23
24
  import time
24
25
  import zlib
25
- from typing import Any, Callable, Optional
26
+ from typing import Any, Callable, Optional, NamedTuple
26
27
 
27
28
  from .constants import TALK_PCM_FRAME_BYTES, TALK_PCM_RATE
28
29
 
@@ -2176,3 +2177,233 @@ def _dedup_bundle_candidates(sdp: str) -> str:
2176
2177
  else:
2177
2178
  result.extend(ln for ln in sec if not ln.startswith('a=candidate:'))
2178
2179
  return '\r\n'.join(result)
2180
+
2181
+
2182
+ #: Header of an AVIO control frame: seq, command, timestamp(ms), payload length,
2183
+ #: reserved, then 4 bytes of padding. Same layout we already build when sending.
2184
+ _AVIO_RESP_HDR = struct.Struct("<IIqII4x")
2185
+
2186
+ #: Bytes of header before an AVIO payload begins. Exported because the SDES
2187
+ #: receive path has to walk past one frame to reach the next in the same chunk.
2188
+ AVIO_HDR_LEN = _AVIO_RESP_HDR.size
2189
+
2190
+
2191
+ class AvioResponse(NamedTuple):
2192
+ """A decoded AVIO control response."""
2193
+
2194
+ seq: int
2195
+ command: int
2196
+ payload: bytes
2197
+
2198
+
2199
+ def parse_avio_response(frame: bytes) -> "Optional[AvioResponse]":
2200
+ """Decode an AVIO control response, or None if this is not a valid one.
2201
+
2202
+ Returns None rather than raising: this runs on the bridge's receive path,
2203
+ where anything can arrive, and a malformed frame must be ignored rather than
2204
+ kill the loop. A declared length that overruns the buffer is rejected too -
2205
+ trusting it would hand the caller a truncated payload and, for example, read
2206
+ a stream profile out of bytes the camera never sent.
2207
+ """
2208
+ if not frame or len(frame) < _AVIO_RESP_HDR.size:
2209
+ return None
2210
+ seq, command, _ts, length, _reserved = _AVIO_RESP_HDR.unpack_from(frame, 0)
2211
+ body = frame[_AVIO_RESP_HDR.size:]
2212
+ if len(body) < length:
2213
+ return None
2214
+ return AvioResponse(seq=seq, command=command, payload=body[:length])
2215
+
2216
+
2217
+ class _AvioWaiter:
2218
+ """A single outstanding question, handed back by ``AvioResponseRouter.expect``.
2219
+
2220
+ Built on a ``concurrent.futures.Future`` rather than an ``asyncio`` one on
2221
+ purpose: the reply is dispatched from the SDES bridge thread while the
2222
+ caller awaits on the event loop, and setting an asyncio future's result from
2223
+ a foreign thread does not wake the awaiting coroutine - the camera would
2224
+ look silent.
2225
+ """
2226
+
2227
+ def __init__(self, router: "AvioResponseRouter", command: int) -> None:
2228
+ self._router = router
2229
+ self._command = command
2230
+ self._future = _cfutures.Future()
2231
+
2232
+ def cancel(self) -> None:
2233
+ """Withdraw the question - used when the command could not be sent."""
2234
+ self._router._forget(self)
2235
+
2236
+ async def result(self, timeout: float) -> "Optional[AvioResponse]":
2237
+ """The camera's reply, or None if it did not answer within ``timeout``.
2238
+
2239
+ None is a real answer, not an error: this firmware may not implement a
2240
+ response for every command we send, and a control call must never block
2241
+ a view while we find out.
2242
+ """
2243
+ if self._future.done():
2244
+ # A wait that timed out cancels its own wrapper, and that
2245
+ # cancellation can propagate down to this future - asking again
2246
+ # would then raise where the first call returned None.
2247
+ if self._future.cancelled():
2248
+ return None
2249
+ try:
2250
+ return self._future.result()
2251
+ except Exception:
2252
+ return None
2253
+ try:
2254
+ return await asyncio.wait_for(
2255
+ asyncio.wrap_future(self._future), timeout
2256
+ )
2257
+ except TimeoutError:
2258
+ # Stop listening. _avio_cmd runs on the keepalive path, so a
2259
+ # registration left behind on every unanswered command would grow
2260
+ # for as long as the session lives.
2261
+ self._router._forget(self)
2262
+ return None
2263
+ except asyncio.CancelledError:
2264
+ # Cancellation is the caller's task being stopped (entity teardown,
2265
+ # config-entry unload), NOT the camera declining to answer. Drop
2266
+ # the registration, then let it through: swallowing it would leave
2267
+ # the caller running inside a task the loop has already given up on.
2268
+ self._router._forget(self)
2269
+ raise
2270
+
2271
+
2272
+ class AvioResponseRouter:
2273
+ """Match inbound AVIO responses to the commands that asked for them.
2274
+
2275
+ The control channel also carries traffic nobody asked for (session-mode
2276
+ notifications, track switches), so "the next frame in" is not the answer to
2277
+ the last question out. Register interest with ``expect()`` *before* sending,
2278
+ then await the waiter; ``dispatch()`` is safe to call from the bridge thread
2279
+ and never raises or blocks it.
2280
+ """
2281
+
2282
+ def __init__(self) -> None:
2283
+ self._lock = threading.Lock()
2284
+ self._waiting: "dict[int, list[_AvioWaiter]]" = {}
2285
+
2286
+ def expect(self, command: int) -> _AvioWaiter:
2287
+ """Register interest in the next response carrying ``command``.
2288
+
2289
+ Call this before sending the request: the camera can answer before the
2290
+ sender returns, and a registration made afterwards would miss the reply.
2291
+ """
2292
+ waiter = _AvioWaiter(self, command)
2293
+ with self._lock:
2294
+ self._waiting.setdefault(command, []).append(waiter)
2295
+ return waiter
2296
+
2297
+ def dispatch(self, frame: bytes) -> bool:
2298
+ """Hand ``frame`` to whoever is waiting for it.
2299
+
2300
+ True if it answered an outstanding question. False - not an error - for
2301
+ junk, for a command nobody asked about, and for a retransmitted copy of
2302
+ a reply already delivered.
2303
+ """
2304
+ try:
2305
+ resp = parse_avio_response(frame)
2306
+ if resp is None:
2307
+ return False
2308
+ with self._lock:
2309
+ queue = self._waiting.get(resp.command)
2310
+ waiter = None
2311
+ while queue:
2312
+ candidate = queue.pop(0)
2313
+ if not candidate._future.done():
2314
+ waiter = candidate
2315
+ break
2316
+ if queue is not None and not queue:
2317
+ self._waiting.pop(resp.command, None)
2318
+ if waiter is None:
2319
+ return False
2320
+ waiter._future.set_result(resp)
2321
+ return True
2322
+ except Exception:
2323
+ # Runs inline on the bridge's receive path; a bad frame must never
2324
+ # take the media loop down with it.
2325
+ return False
2326
+
2327
+ def _forget(self, waiter: "_AvioWaiter") -> None:
2328
+ with self._lock:
2329
+ queue = self._waiting.get(waiter._command)
2330
+ if not queue:
2331
+ return
2332
+ try:
2333
+ queue.remove(waiter)
2334
+ except ValueError:
2335
+ return
2336
+ if not queue:
2337
+ self._waiting.pop(waiter._command, None)
2338
+
2339
+
2340
+ def _avio_cmd_id(frame: bytes) -> "Optional[int]":
2341
+ """The command id of an AVIO frame, or None if it does not look like one.
2342
+
2343
+ For the receive-path logs. Which ids this firmware actually sends is not
2344
+ documented anywhere we trust - the list we work from was assembled from
2345
+ frames seen on the wire - so naming them as they arrive is what turns the
2346
+ next question into a log grep instead of another live session.
2347
+ """
2348
+ resp = parse_avio_response(bytes(frame))
2349
+ return None if resp is None else resp.command
2350
+
2351
+
2352
+ #: How long a control call waits for the camera before reporting "no reply".
2353
+ #: Short on purpose: these run behind Home Assistant service calls, and a
2354
+ #: firmware that implements the request but not the response must not be able to
2355
+ #: hold up a view while we find that out.
2356
+ AVIO_RESPONSE_TIMEOUT_S = 2.0
2357
+
2358
+
2359
+ class AvioRequestMixin:
2360
+ """Ask an AVIO command and wait for the camera's reply.
2361
+
2362
+ Mixed into both session types. ``_avio_cmd`` itself is left exactly as it
2363
+ was - fire-and-forget, synchronous, returns a bool - because every camera on
2364
+ both transports goes through it, for the speaker, the keepalive heartbeat
2365
+ and resolution alike. Wanting an answer is opt-in and additive.
2366
+
2367
+ A session that mixes this in must create its router in ``__init__``:
2368
+ dispatch arrives from the bridge thread and creating one lazily could race
2369
+ with the event loop and lose the reply to a second router.
2370
+ """
2371
+
2372
+ _avio_responses: "AvioResponseRouter"
2373
+
2374
+ def dispatch_avio_frame(self, frame: bytes) -> bool:
2375
+ """Offer an inbound control frame to whoever is waiting for it.
2376
+
2377
+ Called from the transports' receive paths (the DTLS DataChannel message
2378
+ handler, the SDES bridge thread). False - not an error - when nothing
2379
+ was waiting for it, which is the common case: the camera sends session
2380
+ notifications and track switches nobody asked for.
2381
+ """
2382
+ return self._avio_responses.dispatch(frame)
2383
+
2384
+ async def async_avio_request(
2385
+ self,
2386
+ cmd: int,
2387
+ payload: bytes = b"",
2388
+ *,
2389
+ response_cmd: int,
2390
+ timeout: float = AVIO_RESPONSE_TIMEOUT_S,
2391
+ ) -> "Optional[AvioResponse]":
2392
+ """Send ``cmd`` and return the camera's ``response_cmd`` reply, or None.
2393
+
2394
+ None means the camera did not answer in time. That is a real result,
2395
+ not a failure: this firmware may implement a request without a response,
2396
+ and no reply at all points at the channel rather than the firmware.
2397
+ """
2398
+ waiter = self._avio_responses.expect(response_cmd)
2399
+ # Registered BEFORE the send: a camera on the LAN can answer before the
2400
+ # sending call returns, and a reply that arrives with nobody listening
2401
+ # would be reported as silence.
2402
+ try:
2403
+ sent = self._avio_cmd(cmd, payload)
2404
+ except Exception:
2405
+ sent = False
2406
+ if not sent:
2407
+ waiter.cancel()
2408
+ return None
2409
+ return await waiter.result(timeout)
@@ -10,11 +10,12 @@ import logging
10
10
  from typing import Callable, Optional
11
11
 
12
12
  from .constants import SDES_TALK_PUMP_IDLE_TICK
13
+ from .protocol import AvioRequestMixin, AvioResponseRouter
13
14
 
14
15
  _LOGGER = logging.getLogger(__name__)
15
16
 
16
17
 
17
- class SdesSession:
18
+ class SdesSession(AvioRequestMixin):
18
19
  """Active SDES-SRTP stream session managed by an ffmpeg subprocess.
19
20
 
20
21
  Obtain via ``await DeviceClient.async_open_webrtc_stream(...)`` when the
@@ -39,6 +40,7 @@ class SdesSession:
39
40
  first_video_pt=None,
40
41
  first_audio_pt=None,
41
42
  device_id=None,
43
+ responses=None,
42
44
  ) -> None:
43
45
  # Which camera this session belongs to, for logging only. Optional so
44
46
  # an existing caller that does not pass it still works; the logs then
@@ -80,6 +82,11 @@ class SdesSession:
80
82
  # Mutable one-element list shared with the bridge thread. Bridge sets
81
83
  # [0] to a callable(cmd, payload) once the SCTP channel is up.
82
84
  self._cmd_chan = cmd_chan if cmd_chan is not None else [None]
85
+ # Matches inbound AVIO replies to the commands that asked for them.
86
+ # Created here, not lazily: the bridge thread dispatches into it.
87
+ self._avio_responses = (
88
+ responses if responses is not None else AvioResponseRouter()
89
+ )
83
90
  # Shared talk state (dict) for outbound two-way audio, or None when the
84
91
  # session was not opened talk-capable (offer stayed recvonly). Populated
85
92
  # by the bridge (camera audio addr) and by async_start_talk (provider).
@@ -24,7 +24,10 @@ from .constants import (
24
24
  from .models import VideoFrame # noqa: F401 - forward-ref annotation
25
25
  from .sdes import SdesSession
26
26
  from .protocol import (
27
+ AVIO_HDR_LEN,
28
+ AvioResponseRouter,
27
29
  _build_sprop,
30
+ parse_avio_response,
28
31
  _build_stun_binding_success_response,
29
32
  _extract_param_sets_from_rtp,
30
33
  _grab_free_port,
@@ -45,6 +48,40 @@ _LOGGER = logging.getLogger(__name__)
45
48
  _MEDIA_RCVBUF_BYTES = 4 * 1024 * 1024
46
49
 
47
50
 
51
+ def _dispatch_sctp_avio(responses, payload) -> bool:
52
+ """Offer an inbound SCTP DATA payload to the AVIO response router.
53
+
54
+ This is where the camera's replies actually arrive on SDES: an encrypted
55
+ SCTP DATA chunk (PPID 53) on the same channel we send LIVING, the heartbeat
56
+ and SPEAKERSTART on - not the TUTK-audio framing the audio forward path
57
+ watches. Wiring only the latter is why a camera that answered SPEAKERSTART
58
+ in moments looked silent.
59
+
60
+ True if it answered something we were waiting for. Never raises and never
61
+ blocks: it runs inline on the bridge receive loop, where a bad frame must
62
+ not be able to take the media path down with it.
63
+ """
64
+ if responses is None or not payload:
65
+ return False
66
+ answered = False
67
+ try:
68
+ view = memoryview(payload)
69
+ while len(view) >= AVIO_HDR_LEN:
70
+ frame = parse_avio_response(bytes(view))
71
+ if frame is None:
72
+ break
73
+ if responses.dispatch(bytes(view)):
74
+ answered = True
75
+ # One chunk can carry more than one frame: 5377 declares a 12-byte
76
+ # payload and has been seen arriving in a 140-byte chunk. Reading
77
+ # only the first would lose a reply batched behind a notify, which
78
+ # presents as the camera intermittently not answering.
79
+ view = view[AVIO_HDR_LEN + len(frame.payload):]
80
+ except Exception:
81
+ return answered
82
+ return answered
83
+
84
+
48
85
  def _widen_media_rcvbuf(sock, kind: str, device_id: str = "?") -> int:
49
86
  """Ask for a large receive buffer on a camera media socket.
50
87
 
@@ -2756,6 +2793,12 @@ class _SdesOpenMixin:
2756
2793
  # once the SCTP DataChannel is established. SdesSession reads it via
2757
2794
  # _cmd_chan[0] to dispatch PTZ / IOCtrl commands from the main thread.
2758
2795
  _cmd_chan: list = [None]
2796
+ # The other direction: matches the camera's AVIO replies to the commands
2797
+ # that asked for them. Created here, not in SdesSession, because the
2798
+ # bridge thread starts dispatching into it before the session object
2799
+ # exists; the session is handed the same instance so both sides share
2800
+ # one registry.
2801
+ _avio_responses = AvioResponseRouter()
2759
2802
  # Proc holder: set to the ffmpeg Popen object after launch so the
2760
2803
  # bridge thread can poll for exit without a NameError race.
2761
2804
  _proc_holder: list = [None]
@@ -3807,9 +3850,17 @@ class _SdesOpenMixin:
3807
3850
  if len(_pd_plain) >= 28 else 0)
3808
3851
  _sc_cmd = (int.from_bytes(_sc_pay[4:8], 'little')
3809
3852
  if len(_sc_pay) >= 8 else 0)
3853
+ # This is where the camera's answers
3854
+ # come back on SDES. They were parsed
3855
+ # and logged here long before anything
3856
+ # could receive them; hand them to
3857
+ # whoever asked.
3858
+ _sc_answered = _dispatch_sctp_avio(
3859
+ _avio_responses, _sc_pay)
3810
3860
  _status(
3811
3861
  f"SDES DC: enc DATA ppid={_sc_ppid}"
3812
3862
  f" cmd={_sc_cmd} {len(_sc_pay)}B"
3863
+ f"{' (answered a request)' if _sc_answered else ''}"
3813
3864
  f" {_sc_pay[:32].hex()}"
3814
3865
  )
3815
3866
  else:
@@ -3878,6 +3929,14 @@ class _SdesOpenMixin:
3878
3929
  # f0.java:3224): the camera can send it unsolicited, and
3879
3930
  # without it here an 804 frame is misrouted as PCMA noise.
3880
3931
  _avio_cmds = {5376, 5377, 5156, 5157, 768, 769, 511, 804}
3932
+ # No response dispatch here. Measured
3933
+ # 2026-08-07: the camera's replies come back
3934
+ # as encrypted SCTP DATA (see
3935
+ # _dispatch_sctp_avio), never in this
3936
+ # framing. Offering every audio packet to
3937
+ # the router would be per-packet work on the
3938
+ # media path for something that has never
3939
+ # arrived on it.
3881
3940
  if _fwd_cmd not in _avio_cmds:
3882
3941
  try:
3883
3942
  _lo_a.sendto(
@@ -4964,4 +5023,5 @@ class _SdesOpenMixin:
4964
5023
  first_video_pt=_first_video_pt,
4965
5024
  first_audio_pt=_first_audio_pt,
4966
5025
  device_id=getattr(self, "device_id", None),
5026
+ responses=_avio_responses,
4967
5027
  )
@@ -12,10 +12,12 @@ import struct
12
12
  import time
13
13
  from typing import Any, Callable, Optional
14
14
 
15
+ from .protocol import AvioRequestMixin, AvioResponseRouter
16
+
15
17
  _LOGGER = logging.getLogger(__name__)
16
18
 
17
19
 
18
- class WebRTCSession:
20
+ class WebRTCSession(AvioRequestMixin):
19
21
  """Active WebRTC live-stream session for a liveType=2 AiDot camera.
20
22
 
21
23
  Obtain via ``await DeviceClient.async_open_webrtc_stream(...)``.
@@ -34,6 +36,7 @@ class WebRTCSession:
34
36
  audio_sender: Any = None,
35
37
  talk_track: Any = None,
36
38
  talk_holder: Any = None,
39
+ responses: Any = None,
37
40
  ) -> None:
38
41
  self._pc = pc
39
42
  self._outgoing_q = outgoing_q
@@ -41,6 +44,12 @@ class WebRTCSession:
41
44
  self._recorder = recorder
42
45
  self._track_tasks = track_tasks
43
46
  self._dc = dc # RTCDataChannel for AVIO IOCtrl commands
47
+ # Matches inbound AVIO replies to the commands that asked for them.
48
+ # Created here, not lazily: the DataChannel message handler can
49
+ # dispatch from outside this constructor's thread.
50
+ self._avio_responses = (
51
+ responses if responses is not None else AvioResponseRouter()
52
+ )
44
53
  # Two-way audio (talk): the sendrecv audio RTCRtpSender, an idle PCMA talk
45
54
  # track, and a mutable {"provider": callable|None} holder the track reads from.
46
55
  self._audio_sender = audio_sender
@@ -21,6 +21,8 @@ from .constants import _LIVE_PLAY_NOT_READY, fallback_turn_uris, stun_server_uri
21
21
  from .models import VideoFrame
22
22
  from .webrtc import WebRTCSession
23
23
  from .protocol import (
24
+ AvioResponseRouter,
25
+ _avio_cmd_id,
24
26
  _compress_sdp_for_camera,
25
27
  _dedup_bundle_candidates,
26
28
  _filter_sdp_candidates,
@@ -1729,6 +1731,12 @@ class _WebRTCOpenMixin:
1729
1731
  _status(f"talk: SPEAKER{'START' if start else 'STOP'} failed:"
1730
1732
  f" {_spk_exc}")
1731
1733
 
1734
+ # Matches the camera's AVIO replies to the commands that asked for
1735
+ # them. Created here rather than in the session because the message
1736
+ # handlers below are registered while the session does not exist yet;
1737
+ # it is handed to the session at the end so both sides share one.
1738
+ _avio_responses = AvioResponseRouter()
1739
+
1732
1740
  # Camera-initiated DCEP path: KVS firmware on A000088 doesn't include
1733
1741
  # m=application in its answer SDP, suggesting the data channel may
1734
1742
  # actually be opened by the camera (master), not the viewer. Hook
@@ -1748,8 +1756,10 @@ class _WebRTCOpenMixin:
1748
1756
  def _on_remote_dc_message(message) -> None:
1749
1757
  try:
1750
1758
  if isinstance(message, (bytes, bytearray)):
1759
+ _avio_responses.dispatch(bytes(message))
1751
1760
  _status(
1752
1761
  f"DC[remote:{channel.label}] RX {len(message)}B"
1762
+ f" cmd={_avio_cmd_id(message)}"
1753
1763
  f" hex={bytes(message)[:32].hex()}"
1754
1764
  )
1755
1765
  else:
@@ -1874,8 +1884,10 @@ class _WebRTCOpenMixin:
1874
1884
  def _on_kvs_dc_message(message) -> None:
1875
1885
  try:
1876
1886
  if isinstance(message, (bytes, bytearray)):
1887
+ _avio_responses.dispatch(bytes(message))
1877
1888
  _status(
1878
1889
  f"DC[{_dc_label}] RX {len(message)}B"
1890
+ f" cmd={_avio_cmd_id(message)}"
1879
1891
  f" hex={bytes(message)[:32].hex()}"
1880
1892
  )
1881
1893
  else:
@@ -3716,4 +3728,5 @@ class _WebRTCOpenMixin:
3716
3728
  audio_sender=_audio_sender,
3717
3729
  talk_track=_talk_track,
3718
3730
  talk_holder=_talk_holder,
3731
+ responses=_avio_responses,
3719
3732
  )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.15.7"
7
+ version = "0.16.0"
8
8
  description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.15.7
3
+ Version: 0.16.0
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
@@ -73,6 +73,9 @@ tests/test_aioice_compat.py
73
73
  tests/test_alarm_event.py
74
74
  tests/test_answer_sdp_video_pt.py
75
75
  tests/test_audio_mux_offline.py
76
+ tests/test_avio_request.py
77
+ tests/test_avio_response_parse.py
78
+ tests/test_avio_response_router.py
76
79
  tests/test_backoff.py
77
80
  tests/test_battery_detection.py
78
81
  tests/test_battery_evidence_corroboration.py
@@ -133,6 +136,7 @@ tests/test_restart_cycle.py
133
136
  tests/test_retry_policy.py
134
137
  tests/test_scripts_importable.py
135
138
  tests/test_sdes_adaptive.py
139
+ tests/test_sdes_avio_dispatch.py
136
140
  tests/test_sdes_bridge_break_gate.py
137
141
  tests/test_sdes_echo_wait_timeout.py
138
142
  tests/test_sdes_fast_liveplay.py
@@ -152,6 +156,7 @@ tests/test_sdp_narrowing_properties.py
152
156
  tests/test_sdp_payload_type_narrowing.py
153
157
  tests/test_security_hardening.py
154
158
  tests/test_serializable_login_info.py
159
+ tests/test_serve_loss_tolerance.py
155
160
  tests/test_serve_relay.py
156
161
  tests/test_session_stats.py
157
162
  tests/test_shared_house_seam.py