python-aidot-cameras 1.0.0b7__tar.gz → 1.0.0b9__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 (239) hide show
  1. python_aidot_cameras-1.0.0b9/MANIFEST.in +3 -0
  2. {python_aidot_cameras-1.0.0b7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b9}/PKG-INFO +148 -10
  3. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/README.md +137 -8
  4. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/__init__.py +10 -0
  5. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/__main__.py +48 -7
  6. python_aidot_cameras-1.0.0b9/aidot_cameras/_vendor/aiortc/LICENSE +25 -0
  7. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/client.py +80 -7
  8. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/controls.py +18 -0
  9. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/lan_control.py +4 -0
  10. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/models.py +19 -0
  11. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/protocol.py +118 -0
  12. python_aidot_cameras-1.0.0b9/aidot_cameras/camera/sd_playback.py +112 -0
  13. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/webrtc_open.py +121 -28
  14. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/client.py +4 -3
  15. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/pyproject.toml +17 -5
  16. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9/python_aidot_cameras.egg-info}/PKG-INFO +148 -10
  17. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/SOURCES.txt +7 -0
  18. python_aidot_cameras-1.0.0b9/tests/test_answer_section_selection.py +189 -0
  19. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_stdout_producer.py +48 -0
  20. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_go2rtc_cli.py +29 -1
  21. python_aidot_cameras-1.0.0b9/tests/test_local_only_controls.py +76 -0
  22. python_aidot_cameras-1.0.0b9/tests/test_on_frame_is_an_av_frame.py +64 -0
  23. python_aidot_cameras-1.0.0b9/tests/test_public_signature_matches_impl.py +70 -0
  24. python_aidot_cameras-1.0.0b9/tests/test_sd_playback.py +107 -0
  25. python_aidot_cameras-1.0.0b7/MANIFEST.in +0 -1
  26. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/LICENSE +0 -0
  27. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_upstream.py +0 -0
  28. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/__init__.py +0 -0
  29. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  30. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  31. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  32. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  33. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  34. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  35. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  36. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  37. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  38. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  39. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  40. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  41. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  42. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  43. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  44. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  45. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  46. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  47. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  48. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  49. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  50. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  51. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  52. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  53. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  54. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  55. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  56. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  57. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  58. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  59. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  60. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  61. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/ble_gateway.py +0 -0
  62. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/__init__.py +0 -0
  63. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/constants.py +0 -0
  64. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/go2rtc.py +0 -0
  65. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/hwaccel.py +0 -0
  66. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/playback.py +0 -0
  67. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sd_events.py +0 -0
  68. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sd_listing.py +0 -0
  69. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sdes.py +0 -0
  70. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/sdes_open.py +0 -0
  71. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/tutk.py +0 -0
  72. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/camera/webrtc.py +0 -0
  73. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/const.py +0 -0
  74. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/credentials.py +0 -0
  75. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/crypto.py +0 -0
  76. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/device_client.py +0 -0
  77. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/discover.py +0 -0
  78. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/exceptions.py +0 -0
  79. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/g711.py +0 -0
  80. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/lan_retry.py +0 -0
  81. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/aidot_cameras/py.typed +0 -0
  82. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  83. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  84. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/requires.txt +0 -0
  85. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  86. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/setup.cfg +0 -0
  87. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_aioice_compat.py +0 -0
  88. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_alarm_event.py +0 -0
  89. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_answer_sdp_video_pt.py +0 -0
  90. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_audio_mux_offline.py +0 -0
  91. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_avio_request.py +0 -0
  92. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_avio_response_parse.py +0 -0
  93. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_avio_response_router.py +0 -0
  94. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_backoff.py +0 -0
  95. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_battery_detection.py +0 -0
  96. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_battery_evidence_corroboration.py +0 -0
  97. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_battery_skip_turn.py +0 -0
  98. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_bitrate_campaign_arms.py +0 -0
  99. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ble_gateway.py +0 -0
  100. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_bridge_state_sentinels.py +0 -0
  101. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_busy_backoff.py +0 -0
  102. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_cloud_count.py +0 -0
  103. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_cloud_plan.py +0 -0
  104. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_constants_imports_resolve.py +0 -0
  105. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_control_verdicts.py +0 -0
  106. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_deferred_hardening.py +0 -0
  107. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_device_cmd_seq_match.py +0 -0
  108. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_device_login_guard.py +0 -0
  109. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_device_user_info_cache.py +0 -0
  110. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dormant_camera_cleanup.py +0 -0
  111. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_mux_terminal_write.py +0 -0
  112. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_not_ready_burst.py +0 -0
  113. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_pinning.py +0 -0
  114. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_resolution_ack.py +0 -0
  115. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_serve_decode_skip.py +0 -0
  116. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_serve_open_timeout.py +0 -0
  117. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_skip_signaling_wait.py +0 -0
  118. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_dtls_slow_probe.py +0 -0
  119. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_egress_guard.py +0 -0
  120. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_env_seams.py +0 -0
  121. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  122. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_futile_keepalive.py +0 -0
  123. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  124. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_go2rtc.py +0 -0
  125. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_h264_decode_rate_limit.py +0 -0
  126. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_highport_nomination.py +0 -0
  127. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_hwaccel_probe.py +0 -0
  128. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ice_config_cache.py +0 -0
  129. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ice_peer_reflexive.py +0 -0
  130. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_idle_release_both_loops.py +0 -0
  131. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_keepalive_renew.py +0 -0
  132. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_key_restart_sdp.py +0 -0
  133. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_keyframe_prompter.py +0 -0
  134. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_lan_control.py +0 -0
  135. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  136. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_light_active_color_mode.py +0 -0
  137. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_live_session_carries_talk.py +0 -0
  138. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_live_stream_param.py +0 -0
  139. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_live_validate_policy.py +0 -0
  140. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_logging_caps.py +0 -0
  141. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_login_connect_deadline.py +0 -0
  142. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_login_reconnect_backoff.py +0 -0
  143. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_media_counted_only_when_plaintext.py +0 -0
  144. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_media_socket_rcvbuf.py +0 -0
  145. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_media_wait_floor.py +0 -0
  146. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_motion_poll.py +0 -0
  147. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_credential_refetch.py +0 -0
  148. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_credential_self_heal.py +0 -0
  149. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_password_not_persisted.py +0 -0
  150. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_mqtt_session_reconnect.py +0 -0
  151. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_narrow_pc_ice.py +0 -0
  152. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_no_key_material_in_logs.py +0 -0
  153. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_no_self_referential_go2rtc.py +0 -0
  154. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_no_undefined_names.py +0 -0
  155. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  156. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_offline_keepalive_pause.py +0 -0
  157. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_open_fail_logger.py +0 -0
  158. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_open_gate_delay.py +0 -0
  159. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_peerid_reuse.py +0 -0
  160. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_persistent_mqtt.py +0 -0
  161. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_persistent_mqtt_robustness.py +0 -0
  162. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_playback_tls.py +0 -0
  163. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_post_merge_hardening.py +0 -0
  164. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  165. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_quality_campaign_in_session.py +0 -0
  166. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_recent_recordings.py +0 -0
  167. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_remb.py +0 -0
  168. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_resolution_persists.py +0 -0
  169. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_restart_cycle.py +0 -0
  170. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_retry_policy.py +0 -0
  171. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_scripts_importable.py +0 -0
  172. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sctp_init_ack_tsn.py +0 -0
  173. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_card_presence.py +0 -0
  174. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_event_decode.py +0 -0
  175. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_event_probe_payloads.py +0 -0
  176. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sd_listing.py +0 -0
  177. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_adaptive.py +0 -0
  178. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_avio_dispatch.py +0 -0
  179. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_bridge_break_gate.py +0 -0
  180. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_echo_wait_timeout.py +0 -0
  181. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_fast_liveplay.py +0 -0
  182. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  183. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_first_media_stall_report.py +0 -0
  184. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_idle_release.py +0 -0
  185. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_offer_video_order.py +0 -0
  186. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_offer_video_pin.py +0 -0
  187. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_open_cleanup.py +0 -0
  188. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_sctp_abort.py +0 -0
  189. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_serve_audio.py +0 -0
  190. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_serve_cmd.py +0 -0
  191. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_serve_stderr_drain.py +0 -0
  192. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_sprop.py +0 -0
  193. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_talk.py +0 -0
  194. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_teardown_exit_log.py +0 -0
  195. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_terminal_ack.py +0 -0
  196. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_terminal_ack_latency.py +0 -0
  197. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdes_watchdog.py +0 -0
  198. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdp_narrowing_properties.py +0 -0
  199. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sdp_payload_type_narrowing.py +0 -0
  200. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_security_hardening.py +0 -0
  201. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  202. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_serializable_login_info.py +0 -0
  203. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_serve_loss_tolerance.py +0 -0
  204. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_serve_relay.py +0 -0
  205. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_session_stats.py +0 -0
  206. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_shared_house_seam.py +0 -0
  207. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_snapshot_cancellation.py +0 -0
  208. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_speak.py +0 -0
  209. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sprop_capture_gate.py +0 -0
  210. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sprop_staleness.py +0 -0
  211. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  212. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_srtp_rx_rekey.py +0 -0
  213. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_srtp_tx_key_note.py +0 -0
  214. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  215. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  216. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  217. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  218. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  219. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_cap.py +0 -0
  220. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_drain_ownership.py +0 -0
  221. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_idle.py +0 -0
  222. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_slot_cap.py +0 -0
  223. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_stream_teardown.py +0 -0
  224. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_talk.py +0 -0
  225. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  226. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_terminal_ack.py +0 -0
  227. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_token_file_robustness.py +0 -0
  228. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_token_refresh.py +0 -0
  229. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_turn_create_permission.py +0 -0
  230. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  231. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_upstream_compat.py +0 -0
  232. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_video_profile_log.py +0 -0
  233. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_viewer_check_throttle.py +0 -0
  234. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_viewer_presence.py +0 -0
  235. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_viewer_present_push_mode.py +0 -0
  236. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_wait_or_event.py +0 -0
  237. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_wake_gate_camera_evidence.py +0 -0
  238. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_wake_readiness_retry.py +0 -0
  239. {python_aidot_cameras-1.0.0b7 → python_aidot_cameras-1.0.0b9}/tests/test_webrtc_answer_race.py +0 -0
@@ -0,0 +1,3 @@
1
+ # py.typed ships via pyproject.toml's package-data; nothing extra is needed
2
+ # here. The previous line pointed at an `aidot/` directory that does not
3
+ # exist in this distribution.
@@ -1,16 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b7
3
+ Version: 1.0.0b9
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
7
7
  Project-URL: Homepage, https://github.com/cbrightly/python-aidot-cameras
8
8
  Project-URL: Issue Tracker, https://github.com/cbrightly/python-aidot-cameras/issues
9
- Classifier: Programming Language :: Python :: 3
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
10
12
  Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Home Automation
18
+ Classifier: Typing :: Typed
11
19
  Requires-Python: >=3.11
12
20
  Description-Content-Type: text/markdown
13
21
  License-File: LICENSE
22
+ License-File: aidot_cameras/_vendor/aiortc/LICENSE
14
23
  Requires-Dist: python-aidot<0.4,>=0.3.55
15
24
  Requires-Dist: aiohttp>=3.9
16
25
  Requires-Dist: paho-mqtt>=1.6.1
@@ -33,7 +42,7 @@ Dynamic: license-file
33
42
 
34
43
  [![PyPI version](https://img.shields.io/pypi/v/python-aidot-cameras)](https://pypi.org/project/python-aidot-cameras/)
35
44
  [![Python versions](https://img.shields.io/pypi/pyversions/python-aidot-cameras)](https://pypi.org/project/python-aidot-cameras/)
36
- [![License: MIT](https://img.shields.io/pypi/l/python-aidot-cameras)](LICENSE)
45
+ [![License: MIT](https://img.shields.io/pypi/l/python-aidot-cameras)](https://github.com/cbrightly/python-aidot-cameras/blob/main/LICENSE)
37
46
 
38
47
  Control AIDOT WiFi lights **and cameras** from Python.
39
48
 
@@ -46,7 +55,7 @@ recordings/thumbnails, and two-way (push-to-talk) audio.
46
55
  Upstream owns the `aidot` import name and handles non-camera devices (lights,
47
56
  plugs, switches) with none of this package's code in the path; everything here
48
57
  lives under `aidot_cameras`. Taking a new upstream release is a dependency bump
49
- plus a test run - see [`docs/UPSTREAM.md`](docs/UPSTREAM.md).
58
+ plus a test run - see [`docs/UPSTREAM.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/UPSTREAM.md).
50
59
 
51
60
  This repository is the **library** (distribution name `python-aidot-cameras`).
52
61
  The Home Assistant custom component (`custom_components/aidot/`) lives in the
@@ -63,7 +72,7 @@ The streaming transport is auto-selected per camera from its model id:
63
72
  - **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
64
73
 
65
74
  Other battery models (A001108, A001360) are recognized in code with the same
66
- battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
75
+ battery handling. See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#supported-cameras) for
67
76
  the authoritative table and per-model notes.
68
77
 
69
78
  ## Known characteristics
@@ -164,7 +173,25 @@ uv tool install "python-aidot-cameras[webrtc]"
164
173
 
165
174
  export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
166
175
  aidot-go2rtc --list # discover cameras + their transport
167
- aidot-go2rtc <device_id> '{output}' # stream one camera (as a go2rtc exec: source)
176
+
177
+ # Stream one camera. Pick the form that matches how you are running it:
178
+ aidot-go2rtc <device_id> - # MPEG-TS to stdout
179
+ aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull it
180
+ aidot-go2rtc <device_id> '{output}' # go2rtc exec: source only
181
+ ```
182
+
183
+ `'{output}'` is a placeholder go2rtc substitutes; run by hand it is passed
184
+ through as an RTSP push URL, which DTLS cameras (A000088) have no path for and
185
+ the tool refuses. Use `-` or an `http://host:port/name.ts` serve for a
186
+ standalone run.
187
+
188
+ **ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
189
+ stream entirely through an ffmpeg subprocess, and pip cannot install a system
190
+ binary:
191
+
192
+ ```bash
193
+ sudo apt install ffmpeg # Debian/Ubuntu
194
+ brew install ffmpeg # macOS
168
195
  ```
169
196
 
170
197
  Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
@@ -176,13 +203,49 @@ the full list of authentication and stream env vars.
176
203
 
177
204
  ## Usage
178
205
 
179
- Open a live WebRTC stream from a camera device client:
206
+ Import from `aidot_cameras`, never from `aidot`. The upstream `aidot` package is
207
+ a dependency of this one and its `AidotClient` is lights-only: importing from it
208
+ gives you a client with no camera surface and **no ImportError** to tell you
209
+ why.
210
+
211
+ Log in, find a camera, and open a stream:
212
+
213
+ ```python
214
+ import aiohttp
215
+ from aidot_cameras.client import AidotClient
216
+ from aidot_cameras.const import CONF_DEVICE_LIST, CONF_NAME
217
+
218
+ async with aiohttp.ClientSession() as http:
219
+ client = AidotClient(http, country_code="US", username=..., password=...)
220
+ await client.async_post_login()
221
+ devices = (await client.async_get_all_device())[CONF_DEVICE_LIST]
222
+ cam = next(d for d in devices if d.get(CONF_NAME) == "Front Door")
223
+ device_client = client.get_device_client(cam)
224
+ ```
225
+
226
+ `get_device_client` returns a `CameraDeviceClient` for a camera. Plain
227
+ `DeviceClient` and `LightDeviceClient` are the lights clients and carry none of
228
+ the camera surface, so a `hasattr` probe against the wrong one fails silently.
229
+
230
+ Open a live WebRTC stream:
180
231
 
181
232
  ```python
182
233
  session = await device_client.async_open_webrtc_stream(on_frame=cb, timeout=30.0)
183
234
  # ... session.stop() when done
184
235
  ```
185
236
 
237
+ `on_frame` receives a PyAV `av.VideoFrame` - use `pts` / `time_base` /
238
+ `to_ndarray()`. It is **DTLS-only** (A000088): SDES cameras (A001513, A001064)
239
+ decode out of process, so `on_frame` never fires for them and no error is
240
+ raised. For those, take the media as a file or a stream instead:
241
+
242
+ ```python
243
+ session = await device_client.async_open_webrtc_stream(
244
+ output_path="/tmp/live.ts", # record, or
245
+ rtsp_push_url="rtsp://127.0.0.1:8554/cam", # push to go2rtc
246
+ )
247
+ ```
248
+
186
249
  Two-way (push-to-talk) audio:
187
250
 
188
251
  ```python
@@ -192,9 +255,84 @@ await session.async_start_talk(pcm_provider) # provider() -> 320B s16le PCM (2
192
255
  await session.async_stop_talk()
193
256
  ```
194
257
 
195
- See [`docs/CAMERAS.md`](docs/CAMERAS.md) for the full camera API (streaming,
258
+ See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
196
259
  snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
197
260
 
261
+ ## Getting an RTSP URL
262
+
263
+ Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
264
+ not from this library alone: **it does not run an RTSP server**, and neither do
265
+ the cameras - they have no RTSP and no ONVIF endpoint. The only way video
266
+ leaves them is the cloud-signalled WebRTC handshake and SRTP decrypt this
267
+ library does. Pair it with [go2rtc](https://github.com/AlexxIT/go2rtc) and
268
+ go2rtc publishes the RTSP URL for you.
269
+
270
+ Three steps, about five minutes.
271
+
272
+ **1. List your cameras.** The transport column decides one word in step 3, so
273
+ keep the output around:
274
+
275
+ ```console
276
+ $ pip install "python-aidot-cameras[webrtc]"
277
+ $ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
278
+ $ aidot-go2rtc --list
279
+ 1a2b3c4d... LK.IPC.A000088 DTLS (http-pull)
280
+ 5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
281
+ ```
282
+
283
+ **2. Add one line per camera to `go2rtc.yaml`.** Both forms make go2rtc the
284
+ parent process, so it starts the stream on the first viewer and kills it when
285
+ the last one leaves - nothing talks to your cameras while nobody is watching:
286
+
287
+ ```yaml
288
+ streams:
289
+ driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
290
+ frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
291
+ ```
292
+
293
+ Use `-` for a DTLS camera and `{output}` for an SDES one - that is the whole
294
+ difference, and `--list` told you which is which. (`{output}` is a placeholder
295
+ go2rtc fills in with its own publish URL. It is not something you type at a
296
+ shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
297
+ variables the CLI does, so set them wherever go2rtc starts.
298
+
299
+ **3. Use the URL.** go2rtc serves every stream you named at:
300
+
301
+ ```
302
+ rtsp://<go2rtc-host>:8554/driveway
303
+ ```
304
+
305
+ Check it before wiring anything up to it:
306
+
307
+ ```console
308
+ $ ffprobe rtsp://127.0.0.1:8554/driveway
309
+ Stream #0:0: Video: h264 (High), yuv420p, 1280x720, 15 fps
310
+ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
311
+ ```
312
+
313
+ ### Details worth knowing
314
+
315
+ - **Which camera is which.** `--list` reports the transport per device. DTLS is
316
+ the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
317
+ form is not silent - the CLI tells you which one to use and exits.
318
+ - **Audio.** The SDES `{output}` push carries G.711 audio alongside video; the
319
+ DTLS `-` producer carries AAC (48 kHz mono, resampled from the camera's 8 kHz
320
+ A-law, because 8 kHz AAC plays silent in a lot of browsers).
321
+ - **A stream that starts with no video** and picks it up a few seconds later is
322
+ normal: the mux waits for a keyframe so the first GOP is decodable. A stream
323
+ that stays audio-only is a camera that never sent one - retry the view.
324
+ - **Skipping go2rtc.** `aidot-go2rtc <id> http://127.0.0.1:8555/cam.ts` serves
325
+ MPEG-TS on a listening socket instead, for a consumer that pulls rather than
326
+ spawns. That process has to stay running to keep the port bound, so it holds
327
+ a camera session whether or not anyone is watching. Prefer the `exec:` forms.
328
+ - **From Python**, `rtsp_push_url=` publishes to your own RTSP server directly
329
+ (SDES cameras only - there is no RTSP-push path for DTLS), and
330
+ `output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
331
+ address above, and `ensure_stream(name, source)` registers a stream with a
332
+ running go2rtc if you would rather not edit YAML.
333
+ - **Home Assistant users need none of this.** The integration manages go2rtc
334
+ itself; this section is for standalone use.
335
+
198
336
  ## Home Assistant component and CLI
199
337
 
200
338
  The Home Assistant custom component (`custom_components/aidot/`) is **not** part
@@ -210,7 +348,7 @@ The library reads the following environment variables.
210
348
  ### Credentials
211
349
 
212
350
  Used by the credential helper (`aidot.credentials`); they take priority over any
213
- stored credentials file. See [`aidot_cameras/credentials.py`](aidot_cameras/credentials.py).
351
+ stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
214
352
 
215
353
  | Variable | Purpose | Default |
216
354
  | --- | --- | --- |
@@ -224,7 +362,7 @@ The most useful knobs read by the camera client (`aidot_cameras.camera.client`).
224
362
  are chosen to work out of the box; override only when tuning. Finer-grained
225
363
  internal knobs (audio normalization, keyframe/PLI cadence, retry timing, SDES
226
364
  audio, idle release, the sprop cache path) are documented in
227
- [`docs/CAMERAS.md`](docs/CAMERAS.md#advanced-tuning-environment-variables).
365
+ [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#advanced-tuning-environment-variables).
228
366
 
229
367
  | Variable | Purpose | Default |
230
368
  | --- | --- | --- |
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![PyPI version](https://img.shields.io/pypi/v/python-aidot-cameras)](https://pypi.org/project/python-aidot-cameras/)
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/python-aidot-cameras)](https://pypi.org/project/python-aidot-cameras/)
5
- [![License: MIT](https://img.shields.io/pypi/l/python-aidot-cameras)](LICENSE)
5
+ [![License: MIT](https://img.shields.io/pypi/l/python-aidot-cameras)](https://github.com/cbrightly/python-aidot-cameras/blob/main/LICENSE)
6
6
 
7
7
  Control AIDOT WiFi lights **and cameras** from Python.
8
8
 
@@ -15,7 +15,7 @@ recordings/thumbnails, and two-way (push-to-talk) audio.
15
15
  Upstream owns the `aidot` import name and handles non-camera devices (lights,
16
16
  plugs, switches) with none of this package's code in the path; everything here
17
17
  lives under `aidot_cameras`. Taking a new upstream release is a dependency bump
18
- plus a test run - see [`docs/UPSTREAM.md`](docs/UPSTREAM.md).
18
+ plus a test run - see [`docs/UPSTREAM.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/UPSTREAM.md).
19
19
 
20
20
  This repository is the **library** (distribution name `python-aidot-cameras`).
21
21
  The Home Assistant custom component (`custom_components/aidot/`) lives in the
@@ -32,7 +32,7 @@ The streaming transport is auto-selected per camera from its model id:
32
32
  - **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
33
33
 
34
34
  Other battery models (A001108, A001360) are recognized in code with the same
35
- battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
35
+ battery handling. See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#supported-cameras) for
36
36
  the authoritative table and per-model notes.
37
37
 
38
38
  ## Known characteristics
@@ -133,7 +133,25 @@ uv tool install "python-aidot-cameras[webrtc]"
133
133
 
134
134
  export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
135
135
  aidot-go2rtc --list # discover cameras + their transport
136
- aidot-go2rtc <device_id> '{output}' # stream one camera (as a go2rtc exec: source)
136
+
137
+ # Stream one camera. Pick the form that matches how you are running it:
138
+ aidot-go2rtc <device_id> - # MPEG-TS to stdout
139
+ aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull it
140
+ aidot-go2rtc <device_id> '{output}' # go2rtc exec: source only
141
+ ```
142
+
143
+ `'{output}'` is a placeholder go2rtc substitutes; run by hand it is passed
144
+ through as an RTSP push URL, which DTLS cameras (A000088) have no path for and
145
+ the tool refuses. Use `-` or an `http://host:port/name.ts` serve for a
146
+ standalone run.
147
+
148
+ **ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
149
+ stream entirely through an ffmpeg subprocess, and pip cannot install a system
150
+ binary:
151
+
152
+ ```bash
153
+ sudo apt install ffmpeg # Debian/Ubuntu
154
+ brew install ffmpeg # macOS
137
155
  ```
138
156
 
139
157
  Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
@@ -145,13 +163,49 @@ the full list of authentication and stream env vars.
145
163
 
146
164
  ## Usage
147
165
 
148
- Open a live WebRTC stream from a camera device client:
166
+ Import from `aidot_cameras`, never from `aidot`. The upstream `aidot` package is
167
+ a dependency of this one and its `AidotClient` is lights-only: importing from it
168
+ gives you a client with no camera surface and **no ImportError** to tell you
169
+ why.
170
+
171
+ Log in, find a camera, and open a stream:
172
+
173
+ ```python
174
+ import aiohttp
175
+ from aidot_cameras.client import AidotClient
176
+ from aidot_cameras.const import CONF_DEVICE_LIST, CONF_NAME
177
+
178
+ async with aiohttp.ClientSession() as http:
179
+ client = AidotClient(http, country_code="US", username=..., password=...)
180
+ await client.async_post_login()
181
+ devices = (await client.async_get_all_device())[CONF_DEVICE_LIST]
182
+ cam = next(d for d in devices if d.get(CONF_NAME) == "Front Door")
183
+ device_client = client.get_device_client(cam)
184
+ ```
185
+
186
+ `get_device_client` returns a `CameraDeviceClient` for a camera. Plain
187
+ `DeviceClient` and `LightDeviceClient` are the lights clients and carry none of
188
+ the camera surface, so a `hasattr` probe against the wrong one fails silently.
189
+
190
+ Open a live WebRTC stream:
149
191
 
150
192
  ```python
151
193
  session = await device_client.async_open_webrtc_stream(on_frame=cb, timeout=30.0)
152
194
  # ... session.stop() when done
153
195
  ```
154
196
 
197
+ `on_frame` receives a PyAV `av.VideoFrame` - use `pts` / `time_base` /
198
+ `to_ndarray()`. It is **DTLS-only** (A000088): SDES cameras (A001513, A001064)
199
+ decode out of process, so `on_frame` never fires for them and no error is
200
+ raised. For those, take the media as a file or a stream instead:
201
+
202
+ ```python
203
+ session = await device_client.async_open_webrtc_stream(
204
+ output_path="/tmp/live.ts", # record, or
205
+ rtsp_push_url="rtsp://127.0.0.1:8554/cam", # push to go2rtc
206
+ )
207
+ ```
208
+
155
209
  Two-way (push-to-talk) audio:
156
210
 
157
211
  ```python
@@ -161,9 +215,84 @@ await session.async_start_talk(pcm_provider) # provider() -> 320B s16le PCM (2
161
215
  await session.async_stop_talk()
162
216
  ```
163
217
 
164
- See [`docs/CAMERAS.md`](docs/CAMERAS.md) for the full camera API (streaming,
218
+ See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
165
219
  snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
166
220
 
221
+ ## Getting an RTSP URL
222
+
223
+ Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
224
+ not from this library alone: **it does not run an RTSP server**, and neither do
225
+ the cameras - they have no RTSP and no ONVIF endpoint. The only way video
226
+ leaves them is the cloud-signalled WebRTC handshake and SRTP decrypt this
227
+ library does. Pair it with [go2rtc](https://github.com/AlexxIT/go2rtc) and
228
+ go2rtc publishes the RTSP URL for you.
229
+
230
+ Three steps, about five minutes.
231
+
232
+ **1. List your cameras.** The transport column decides one word in step 3, so
233
+ keep the output around:
234
+
235
+ ```console
236
+ $ pip install "python-aidot-cameras[webrtc]"
237
+ $ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
238
+ $ aidot-go2rtc --list
239
+ 1a2b3c4d... LK.IPC.A000088 DTLS (http-pull)
240
+ 5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
241
+ ```
242
+
243
+ **2. Add one line per camera to `go2rtc.yaml`.** Both forms make go2rtc the
244
+ parent process, so it starts the stream on the first viewer and kills it when
245
+ the last one leaves - nothing talks to your cameras while nobody is watching:
246
+
247
+ ```yaml
248
+ streams:
249
+ driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
250
+ frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
251
+ ```
252
+
253
+ Use `-` for a DTLS camera and `{output}` for an SDES one - that is the whole
254
+ difference, and `--list` told you which is which. (`{output}` is a placeholder
255
+ go2rtc fills in with its own publish URL. It is not something you type at a
256
+ shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
257
+ variables the CLI does, so set them wherever go2rtc starts.
258
+
259
+ **3. Use the URL.** go2rtc serves every stream you named at:
260
+
261
+ ```
262
+ rtsp://<go2rtc-host>:8554/driveway
263
+ ```
264
+
265
+ Check it before wiring anything up to it:
266
+
267
+ ```console
268
+ $ ffprobe rtsp://127.0.0.1:8554/driveway
269
+ Stream #0:0: Video: h264 (High), yuv420p, 1280x720, 15 fps
270
+ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
271
+ ```
272
+
273
+ ### Details worth knowing
274
+
275
+ - **Which camera is which.** `--list` reports the transport per device. DTLS is
276
+ the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
277
+ form is not silent - the CLI tells you which one to use and exits.
278
+ - **Audio.** The SDES `{output}` push carries G.711 audio alongside video; the
279
+ DTLS `-` producer carries AAC (48 kHz mono, resampled from the camera's 8 kHz
280
+ A-law, because 8 kHz AAC plays silent in a lot of browsers).
281
+ - **A stream that starts with no video** and picks it up a few seconds later is
282
+ normal: the mux waits for a keyframe so the first GOP is decodable. A stream
283
+ that stays audio-only is a camera that never sent one - retry the view.
284
+ - **Skipping go2rtc.** `aidot-go2rtc <id> http://127.0.0.1:8555/cam.ts` serves
285
+ MPEG-TS on a listening socket instead, for a consumer that pulls rather than
286
+ spawns. That process has to stay running to keep the port bound, so it holds
287
+ a camera session whether or not anyone is watching. Prefer the `exec:` forms.
288
+ - **From Python**, `rtsp_push_url=` publishes to your own RTSP server directly
289
+ (SDES cameras only - there is no RTSP-push path for DTLS), and
290
+ `output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
291
+ address above, and `ensure_stream(name, source)` registers a stream with a
292
+ running go2rtc if you would rather not edit YAML.
293
+ - **Home Assistant users need none of this.** The integration manages go2rtc
294
+ itself; this section is for standalone use.
295
+
167
296
  ## Home Assistant component and CLI
168
297
 
169
298
  The Home Assistant custom component (`custom_components/aidot/`) is **not** part
@@ -179,7 +308,7 @@ The library reads the following environment variables.
179
308
  ### Credentials
180
309
 
181
310
  Used by the credential helper (`aidot.credentials`); they take priority over any
182
- stored credentials file. See [`aidot_cameras/credentials.py`](aidot_cameras/credentials.py).
311
+ stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
183
312
 
184
313
  | Variable | Purpose | Default |
185
314
  | --- | --- | --- |
@@ -193,7 +322,7 @@ The most useful knobs read by the camera client (`aidot_cameras.camera.client`).
193
322
  are chosen to work out of the box; override only when tuning. Finer-grained
194
323
  internal knobs (audio normalization, keyframe/PLI cadence, retry timing, SDES
195
324
  audio, idle release, the sprop cache path) are documented in
196
- [`docs/CAMERAS.md`](docs/CAMERAS.md#advanced-tuning-environment-variables).
325
+ [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#advanced-tuning-environment-variables).
197
326
 
198
327
  | Variable | Purpose | Default |
199
328
  | --- | --- | --- |
@@ -13,6 +13,12 @@ import time as _time
13
13
  from .client import AidotClient, CameraClient
14
14
  from .device_client import CameraDeviceClient, DeviceInformation, DeviceStatusData
15
15
  from .camera.client import configure_stream_limits
16
+ # Promised by docs/API-STABILITY.md and previously not reachable from the
17
+ # package root. Verified to pull no optional dependency: importing these does
18
+ # not drag in av or aiortc, so a plain (non-[webrtc]) install still imports.
19
+ from .camera.models import CameraDeviceInformation, CameraStatusData
20
+ from .camera.sdes import SdesSession
21
+ from .camera.webrtc import WebRTCSession
16
22
  from .discover import CameraDiscover, Discover
17
23
  from .exceptions import (
18
24
  AidotAuthFailed,
@@ -141,12 +147,16 @@ __all__ = [
141
147
  "AidotUserOrPassIncorrect",
142
148
  "CameraClient",
143
149
  "CameraDeviceClient",
150
+ "CameraDeviceInformation",
144
151
  "CameraDiscover",
152
+ "CameraStatusData",
145
153
  "DeviceInformation",
146
154
  "DeviceStatusData",
147
155
  "Discover",
148
156
  "HTTPError",
149
157
  "InvalidHost",
150
158
  "InvalidURL",
159
+ "SdesSession",
160
+ "WebRTCSession",
151
161
  "configure_stream_limits",
152
162
  ]
@@ -7,13 +7,16 @@ use them: it authenticates to the AiDot cloud, opens the keepalive session for
7
7
  one camera, and pushes/serves the decrypted H.264 (+ G711 audio on the RTSP-push
8
8
  path) to a URL the consumer reads.
9
9
 
10
- Run it as ``aidot-go2rtc`` (console script) or ``python -m aidot``.
10
+ Run it as ``aidot-go2rtc`` (console script) or ``python -m aidot_cameras``.
11
11
 
12
12
  Two modes:
13
13
 
14
14
  * --list Print every camera on the account with its device id
15
- and transport (SDES = RTSP-push capable, DTLS =
16
- HTTP-pull only). Use this to fill in go2rtc.yaml.
15
+ and transport, followed by a paste-ready go2rtc
16
+ ``streams:`` block - each camera already paired with
17
+ the right output argument, which is the one thing
18
+ that differs between the transports and the easiest
19
+ thing to get wrong.
17
20
 
18
21
  * <dev_id> <output_url> Stream one camera. Meant to be invoked by go2rtc as
19
22
  an ``exec:`` source - go2rtc substitutes ``{output}``
@@ -66,14 +69,16 @@ import asyncio
66
69
  import json
67
70
  import logging
68
71
  import os
72
+ import re
69
73
  import signal
70
74
  import sys
71
75
  import tempfile
76
+ from typing import Optional
72
77
 
73
78
  import aiohttp
74
79
 
75
80
  from .client import AidotClient
76
- from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID
81
+ from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID, CONF_NAME
77
82
 
78
83
  _LOGGER = logging.getLogger("aidot.go2rtc")
79
84
 
@@ -206,8 +211,30 @@ async def _cameras(client: AidotClient) -> list[dict]:
206
211
  ]
207
212
 
208
213
 
214
+ def _stream_slug(name: Optional[str], dev_id: str) -> str:
215
+ """A go2rtc stream key from the camera's name (``Front Door`` -> ``front_door``).
216
+
217
+ go2rtc stream names end up in URLs, so keep it to lowercase word characters
218
+ and fall back to the device id when a name is missing or is punctuation
219
+ only. Pure (unit-testable)."""
220
+ slug = re.sub(r"[^a-z0-9]+", "_", (name or "").lower()).strip("_")
221
+ return slug or f"camera_{dev_id[:8]}"
222
+
223
+
224
+ def _go2rtc_source(dev_id: str, is_sdes: bool) -> str:
225
+ """The ``exec:`` source line for one camera.
226
+
227
+ The only difference between the two transports is the output argument:
228
+ SDES cameras RTSP-push into go2rtc's own ``{output}`` URL, DTLS cameras
229
+ write MPEG-TS to stdout (``-``). Getting that backwards is the single most
230
+ common way to end up with a stream that never starts, which is why --list
231
+ writes the line out rather than describing it."""
232
+ out = "{output}" if is_sdes else "-"
233
+ return f"exec:aidot-go2rtc {dev_id} {out}"
234
+
235
+
209
236
  async def cmd_list() -> int:
210
- """Print device id + transport for every camera, for go2rtc.yaml."""
237
+ """Print device id + transport for every camera, plus paste-ready go2rtc YAML."""
211
238
  async with aiohttp.ClientSession() as session:
212
239
  client = await _make_client(session)
213
240
  cams = await _cameras(client)
@@ -216,10 +243,20 @@ async def cmd_list() -> int:
216
243
  return 1
217
244
  print(f"{'device_id':<40} {'model':<14} transport")
218
245
  print("-" * 72)
246
+ rows = []
219
247
  for d in cams:
220
248
  dc = client.get_device_client(d)
221
- transport = "SDES (rtsp-push)" if dc.is_sdes_camera else "DTLS (http-pull)"
249
+ transport = "SDES (rtsp-push)" if dc.is_sdes_camera else "DTLS (stdout)"
222
250
  print(f"{d[CONF_ID]:<40} {(d.get(CONF_MODEL_ID) or ''):<14} {transport}")
251
+ rows.append((_stream_slug(d.get(CONF_NAME), d[CONF_ID]),
252
+ _go2rtc_source(d[CONF_ID], dc.is_sdes_camera)))
253
+ width = max(len(slug) for slug, _ in rows) + 1
254
+ print("\n# Paste into go2rtc.yaml (rename the streams to taste). go2rtc needs")
255
+ print("# the same AIDOT_* environment variables this command just used.")
256
+ print("streams:")
257
+ for slug, source in rows:
258
+ print(f" {(slug + ':'):<{width}} {source}")
259
+ print("\n# Then each camera is at rtsp://<go2rtc-host>:8554/<stream-name>")
223
260
  return 0
224
261
 
225
262
 
@@ -293,9 +330,13 @@ async def cmd_stream(dev_id: str, output_url: str) -> int:
293
330
 
294
331
 
295
332
  def main(argv: list[str] | None = None) -> int:
333
+ # The module docstring above IS the reference for every env var this tool
334
+ # reads, and the README tells users --help will show it. Print it rather
335
+ # than a one-line summary that leaves them hunting through source.
296
336
  parser = argparse.ArgumentParser(
297
337
  prog="aidot-go2rtc",
298
- description="Bridge an AiDot camera into go2rtc (HA-independent).",
338
+ description=__doc__,
339
+ formatter_class=argparse.RawDescriptionHelpFormatter,
299
340
  epilog="Run with --list first to discover device ids and transports.",
300
341
  )
301
342
  parser.add_argument("dev_id", nargs="?", help="camera device id (see --list)")
@@ -0,0 +1,25 @@
1
+ Copyright (c) Jeremy Lainé.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name of aiortc nor the names of its contributors may
13
+ be used to endorse or promote products derived from this software without
14
+ specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.