python-aidot-cameras 0.10.1__tar.gz → 0.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_aidot_cameras-0.10.2/MANIFEST.in +1 -0
- {python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.10.2}/PKG-INFO +9 -9
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/README.md +8 -8
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/client.py +24 -24
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/controls.py +5 -5
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/lan_control.py +2 -2
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/models.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/protocol.py +5 -5
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/sdes_open.py +46 -46
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/webrtc.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/webrtc_open.py +51 -51
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/client.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/const.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/credentials.py +5 -5
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/pyproject.toml +5 -4
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2/python_aidot_cameras.egg-info}/PKG-INFO +9 -9
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/python_aidot_cameras.egg-info/SOURCES.txt +36 -36
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_backoff.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_device_login_guard.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_device_user_info_cache.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_dtls_skip_signaling_wait.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_go2rtc.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_go2rtc_cli.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_ice_config_cache.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_keyframe_prompter.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_lan_control.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_live_stream_param.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_narrow_pc_ice.py +3 -3
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_open_gate_delay.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_persistent_mqtt.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_persistent_mqtt_robustness.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_post_merge_hardening.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_retry_policy.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_adaptive.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_fast_liveplay.py +2 -2
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_idle_release.py +4 -4
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_serve_audio.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_serve_cmd.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_security_hardening.py +3 -3
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_serve_relay.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_session_stats.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_stream_idle.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_stream_teardown.py +1 -1
- python_aidot_cameras-0.10.1/MANIFEST.in +0 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/LICENSE +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/__main__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/aidot/py.typed +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.2}/tests/test_wait_or_event.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include aidot/py.typed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
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
|
|
@@ -49,9 +49,9 @@ on this library.
|
|
|
49
49
|
|
|
50
50
|
The streaming transport is auto-selected per camera from its model id:
|
|
51
51
|
|
|
52
|
-
- **A000088** (M3 Pro)
|
|
53
|
-
- **A001513** ("L2")
|
|
54
|
-
- **A001064** (PTZ)
|
|
52
|
+
- **A000088** (M3 Pro) - DTLS-SRTP, wired/mains.
|
|
53
|
+
- **A001513** ("L2") - SDES-SRTP, **battery** (woken on demand; validated end-to-end).
|
|
54
|
+
- **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
|
|
55
55
|
|
|
56
56
|
Other battery models (A001108, A001360) are recognized in code with the same
|
|
57
57
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
@@ -68,7 +68,7 @@ pip install python-aidot-cameras
|
|
|
68
68
|
pip install python-aidot-cameras[webrtc]
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
`[webrtc]` pulls in the extra dependencies (aiortc, av,
|
|
71
|
+
`[webrtc]` pulls in the extra dependencies (aiortc, av, ...) needed for live
|
|
72
72
|
streaming, snapshots, and two-way audio. Without it you still get lights plus
|
|
73
73
|
the camera cloud/control APIs, but not live media.
|
|
74
74
|
|
|
@@ -132,7 +132,7 @@ The library reads the following environment variables.
|
|
|
132
132
|
### Credentials
|
|
133
133
|
|
|
134
134
|
Used by the credential helper (`aidot.credentials`); they take priority over any
|
|
135
|
-
stored credentials file. See [`
|
|
135
|
+
stored credentials file. See [`aidot/credentials.py`](aidot/credentials.py).
|
|
136
136
|
|
|
137
137
|
| Variable | Purpose | Default |
|
|
138
138
|
| --- | --- | --- |
|
|
@@ -152,12 +152,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
152
152
|
| --- | --- | --- |
|
|
153
153
|
| `AIDOT_MAX_CONCURRENT_OPENS` | Caps how many stream opens run concurrently across all cameras. | `2` |
|
|
154
154
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
155
|
-
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only
|
|
156
|
-
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2
|
|
155
|
+
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
156
|
+
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
157
157
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
|
|
158
158
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
159
159
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
160
|
-
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soak cut SDES NO_MEDIA ~57
|
|
160
|
+
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soak cut SDES NO_MEDIA ~57%->~19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
161
161
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
162
162
|
| `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
|
|
163
163
|
|
|
@@ -21,9 +21,9 @@ on this library.
|
|
|
21
21
|
|
|
22
22
|
The streaming transport is auto-selected per camera from its model id:
|
|
23
23
|
|
|
24
|
-
- **A000088** (M3 Pro)
|
|
25
|
-
- **A001513** ("L2")
|
|
26
|
-
- **A001064** (PTZ)
|
|
24
|
+
- **A000088** (M3 Pro) - DTLS-SRTP, wired/mains.
|
|
25
|
+
- **A001513** ("L2") - SDES-SRTP, **battery** (woken on demand; validated end-to-end).
|
|
26
|
+
- **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
|
|
27
27
|
|
|
28
28
|
Other battery models (A001108, A001360) are recognized in code with the same
|
|
29
29
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
@@ -40,7 +40,7 @@ pip install python-aidot-cameras
|
|
|
40
40
|
pip install python-aidot-cameras[webrtc]
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`[webrtc]` pulls in the extra dependencies (aiortc, av,
|
|
43
|
+
`[webrtc]` pulls in the extra dependencies (aiortc, av, ...) needed for live
|
|
44
44
|
streaming, snapshots, and two-way audio. Without it you still get lights plus
|
|
45
45
|
the camera cloud/control APIs, but not live media.
|
|
46
46
|
|
|
@@ -104,7 +104,7 @@ The library reads the following environment variables.
|
|
|
104
104
|
### Credentials
|
|
105
105
|
|
|
106
106
|
Used by the credential helper (`aidot.credentials`); they take priority over any
|
|
107
|
-
stored credentials file. See [`
|
|
107
|
+
stored credentials file. See [`aidot/credentials.py`](aidot/credentials.py).
|
|
108
108
|
|
|
109
109
|
| Variable | Purpose | Default |
|
|
110
110
|
| --- | --- | --- |
|
|
@@ -124,12 +124,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
124
124
|
| --- | --- | --- |
|
|
125
125
|
| `AIDOT_MAX_CONCURRENT_OPENS` | Caps how many stream opens run concurrently across all cameras. | `2` |
|
|
126
126
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
127
|
-
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only
|
|
128
|
-
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2
|
|
127
|
+
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
128
|
+
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
129
129
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
|
|
130
130
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
131
131
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
132
|
-
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soak cut SDES NO_MEDIA ~57
|
|
132
|
+
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soak cut SDES NO_MEDIA ~57%->~19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
133
133
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
134
134
|
| `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
|
|
135
135
|
|
|
@@ -363,10 +363,10 @@ def _build_sdes_serve_cmd(
|
|
|
363
363
|
# Use DeviceClient.async_open_live_stream() to obtain an instance.
|
|
364
364
|
#
|
|
365
365
|
# Protocol source: classes.jar.decompiled.zip / TutkManager.java
|
|
366
|
-
# IOTC_Connect_ByUID_Parallel(uid, sid)
|
|
367
|
-
# avClientStart2(nSID, "admin", "admin123", ...)
|
|
368
|
-
# avSendIOCtrl(avIndex, 511, ...)
|
|
369
|
-
# avRecvFrameData2(avIndex, ...)
|
|
366
|
+
# IOTC_Connect_ByUID_Parallel(uid, sid) -> nSID
|
|
367
|
+
# avClientStart2(nSID, "admin", "admin123", ...) -> avIndex
|
|
368
|
+
# avSendIOCtrl(avIndex, 511, ...) -> start video (IOTYPE_USER_IPCAM_START)
|
|
369
|
+
# avRecvFrameData2(avIndex, ...) -> frame data loop
|
|
370
370
|
#
|
|
371
371
|
# Requires: libIOTCAPIs.so + libAVAPIs.so from the TUTK SDK.
|
|
372
372
|
# Obtain them from the TUTK SDK distribution or an extracted AiDot APK.
|
|
@@ -380,7 +380,7 @@ def _build_sdes_serve_cmd(
|
|
|
380
380
|
# Manages a single live-stream TCP session for a Leedarson/AiDot camera.
|
|
381
381
|
# Use DeviceClient.async_open_live_stream() to obtain an instance.
|
|
382
382
|
#
|
|
383
|
-
# Protocol source: iOS LDSXplayer startRealPlay
|
|
383
|
+
# Protocol source: iOS LDSXplayer startRealPlay -> LDSTCPManager
|
|
384
384
|
# connectHost:port:sessionId:aesKey:heartbeat:msg:cmd:subCmd:cmdParam:tls:
|
|
385
385
|
#
|
|
386
386
|
# Wire format: same 37-byte header + payload as CloudPlaybackSession, but:
|
|
@@ -596,7 +596,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
596
596
|
self._raw_device: dict = device
|
|
597
597
|
|
|
598
598
|
# Seed camera/diagnostic status from the cloud device "properties" payload
|
|
599
|
-
# (Battery_remaining, Occupancy, SDcardStatus, MotionDetection_*,
|
|
599
|
+
# (Battery_remaining, Occupancy, SDcardStatus, MotionDetection_*, ...). This
|
|
600
600
|
# is the authoritative, always-current source the official app itself reads
|
|
601
601
|
# - cameras do not push these reliably over MQTT - so sensors populate
|
|
602
602
|
# immediately on load, before any poll. No-op for devices without it.
|
|
@@ -723,7 +723,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
723
723
|
|
|
724
724
|
The device-list payload carries every camera attribute under
|
|
725
725
|
``properties`` (battery, SD-card, occupancy, motion/night-vision
|
|
726
|
-
settings,
|
|
726
|
+
settings, ...) plus a top-level ``online`` flag. This is the reliable,
|
|
727
727
|
always-current source the official app reads, so HA populates camera
|
|
728
728
|
sensors and control-entity states from it instead of an MQTT push the
|
|
729
729
|
camera never sends. Returns the updated status.
|
|
@@ -1249,7 +1249,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1249
1249
|
|
|
1250
1250
|
Tries several sources in order:
|
|
1251
1251
|
1. POST /v21/devices/batchGetDeviceUserInfo (AiDot platform API)
|
|
1252
|
-
2. POST /v5/deviceController/getP2pId?deviceId
|
|
1252
|
+
2. POST /v5/deviceController/getP2pId?deviceId=... (Leedarson smarthome)
|
|
1253
1253
|
3. AiDot v32 IPC device detail (fallback)
|
|
1254
1254
|
"""
|
|
1255
1255
|
import aiohttp
|
|
@@ -1517,7 +1517,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1517
1517
|
"payload": inner,
|
|
1518
1518
|
})
|
|
1519
1519
|
pub_topic = f"iot/v1/c/{device_id}/device/setDevAttrReq"
|
|
1520
|
-
_LOGGER.info("setDevAttrReq: %s=%s
|
|
1520
|
+
_LOGGER.info("setDevAttrReq: %s=%s -> %s seq=%s", attr, value, device_id, seq)
|
|
1521
1521
|
ok = await self._mqtt_device_cmd(
|
|
1522
1522
|
pub_topic, payload, timeout=timeout, ack_keyword="setDevAttr")
|
|
1523
1523
|
if ok:
|
|
@@ -1562,7 +1562,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1562
1562
|
},
|
|
1563
1563
|
})
|
|
1564
1564
|
pub_topic = f"iot/v1/s/{user_id}/device/devActionReq"
|
|
1565
|
-
_LOGGER.info("devActionReq: %s %s
|
|
1565
|
+
_LOGGER.info("devActionReq: %s %s -> %s", action, params, device_id)
|
|
1566
1566
|
return await self._mqtt_device_cmd(
|
|
1567
1567
|
pub_topic, payload, timeout=timeout, ack_keyword="devAction")
|
|
1568
1568
|
|
|
@@ -1927,7 +1927,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1927
1927
|
"""
|
|
1928
1928
|
import asyncio as _asyncio
|
|
1929
1929
|
|
|
1930
|
-
#
|
|
1930
|
+
# -- SDES path: stream briefly to a temp TS file, extract one JPEG ---- #
|
|
1931
1931
|
if self.is_sdes_camera:
|
|
1932
1932
|
import os as _os
|
|
1933
1933
|
import tempfile as _tf
|
|
@@ -2009,7 +2009,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2009
2009
|
except Exception:
|
|
2010
2010
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_snapshot', exc_info=True)
|
|
2011
2011
|
|
|
2012
|
-
#
|
|
2012
|
+
# -- DTLS path: on_frame callback delivers frames from aiortc ------- #
|
|
2013
2013
|
frame_event = _asyncio.Event()
|
|
2014
2014
|
captured: list = [None] # stores PIL Image or ndarray once decoded
|
|
2015
2015
|
|
|
@@ -2382,7 +2382,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2382
2382
|
open_timeout: float = 30.0,
|
|
2383
2383
|
retries: int = 3,
|
|
2384
2384
|
) -> bool:
|
|
2385
|
-
"""Play viewer
|
|
2385
|
+
"""Play viewer->camera (push-to-talk / announce) audio through the speaker.
|
|
2386
2386
|
|
|
2387
2387
|
``pcm_provider()`` is polled every 20 ms and must return 320 bytes of s16le
|
|
2388
2388
|
PCM @ 8 kHz mono (a short/empty frame is padded to silence) and ``None``
|
|
@@ -2578,13 +2578,13 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2578
2578
|
break
|
|
2579
2579
|
if _idle_on and _serve_port is not None:
|
|
2580
2580
|
_present = self._sdes_serve_consumer_present(_serve_port)
|
|
2581
|
-
if _present: # True
|
|
2581
|
+
if _present: # True -> a viewer is pulling; stay alive
|
|
2582
2582
|
_last_consumer = time.monotonic()
|
|
2583
2583
|
elif _idle_release_due(_present, _last_consumer,
|
|
2584
2584
|
time.monotonic(), _idle_secs):
|
|
2585
2585
|
_idle_release = True
|
|
2586
2586
|
break
|
|
2587
|
-
# _present is None (unreadable table)
|
|
2587
|
+
# _present is None (unreadable table) -> don't release
|
|
2588
2588
|
except asyncio.CancelledError:
|
|
2589
2589
|
_done.cancel()
|
|
2590
2590
|
self._stream_session = None
|
|
@@ -2836,7 +2836,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2836
2836
|
"""Serve idle-release window (seconds). A per-camera ``stream_idle_s``
|
|
2837
2837
|
(set via start_keepalive) overrides the ``AIDOT_STREAM_IDLE_S`` env
|
|
2838
2838
|
default of 120 s. <= 0 means *never* idle-release (keep the warm session
|
|
2839
|
-
for instant re-views
|
|
2839
|
+
for instant re-views - sensible for mains cameras, which have no battery
|
|
2840
2840
|
cost; note it holds a concurrent-stream slot for the camera's lifetime)."""
|
|
2841
2841
|
opt = getattr(self, "_stream_idle_opt", None)
|
|
2842
2842
|
if opt is not None:
|
|
@@ -3124,7 +3124,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3124
3124
|
import queue as _queue
|
|
3125
3125
|
serve_url = self._keepalive_rtsp_url
|
|
3126
3126
|
# APK parity: the official app gates re-connects to ~15 s (f0.java I1=15000)
|
|
3127
|
-
# and never hammers. We previously floored at 5 s and a partial-success
|
|
3127
|
+
# and never hammers. We previously floored at 5 s and a partial-success ->
|
|
3128
3128
|
# reset path could drop the effective spacing even lower, pounding a flaky
|
|
3129
3129
|
# A000088 camera fast enough to wedge its DTLS stack (ICE completes but DTLS
|
|
3130
3130
|
# never fires until a power-cycle). Floor the backoff at the 15 s gate and,
|
|
@@ -3233,7 +3233,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3233
3233
|
elif _t - _disc_since[0] >= _DISC_DEBOUNCE:
|
|
3234
3234
|
return True
|
|
3235
3235
|
else:
|
|
3236
|
-
_disc_since[0] = None # connected/connecting
|
|
3236
|
+
_disc_since[0] = None # connected/connecting -> reset debounce
|
|
3237
3237
|
return False
|
|
3238
3238
|
|
|
3239
3239
|
for _ in range(40): # ~12s for the receiver/track(s) to exist
|
|
@@ -3436,7 +3436,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3436
3436
|
_LOGGER.warning("DTLS serve: ffmpeg launch failed: %s", exc)
|
|
3437
3437
|
return None
|
|
3438
3438
|
|
|
3439
|
-
#
|
|
3439
|
+
# -- PTZ physical pan/tilt (A001064) ----------------------------------- #
|
|
3440
3440
|
# Uses IOCtrl cmd=4097 (IOTYPE_USER_IPCAM_PTZ_COMMAND) - NOT MQTT.
|
|
3441
3441
|
# DTLS path: sent via WebRTC DataChannel. SDES path: sent via encrypted
|
|
3442
3442
|
# SCTP cmd_chan. Requires an active stream session (_stream_session).
|
|
@@ -3573,11 +3573,11 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3573
3573
|
# Returns a running TutkStreamSession, or None on failure.
|
|
3574
3574
|
#
|
|
3575
3575
|
# Protocol: TUTK IOTC P2P (confirmed from classes.jar.decompiled.zip).
|
|
3576
|
-
# p2pId (TUTK UID)
|
|
3577
|
-
# IOTC_Connect_ByUID_Parallel(uid)
|
|
3578
|
-
# avClientStart2(nSID, "admin", "admin123")
|
|
3579
|
-
# avSendIOCtrl(avIndex, 511, ...)
|
|
3580
|
-
# avRecvFrameData2(avIndex, ...)
|
|
3576
|
+
# p2pId (TUTK UID) <- POST /v21/devices/batchGetDeviceUserInfo
|
|
3577
|
+
# IOTC_Connect_ByUID_Parallel(uid) -> nSID
|
|
3578
|
+
# avClientStart2(nSID, "admin", "admin123") -> avIndex
|
|
3579
|
+
# avSendIOCtrl(avIndex, 511, ...) -> start video stream
|
|
3580
|
+
# avRecvFrameData2(avIndex, ...) -> frame loop
|
|
3581
3581
|
#
|
|
3582
3582
|
# Requires libIOTCAPIs.so + libAVAPIs.so from the TUTK SDK.
|
|
3583
3583
|
|
|
@@ -19,7 +19,7 @@ class _CameraControlsMixin:
|
|
|
19
19
|
|
|
20
20
|
async def async_set_motion_detection(self, enabled: bool) -> bool:
|
|
21
21
|
"""Enable or disable the camera's motion detection."""
|
|
22
|
-
# MotionDetection_Enable is read as getString() in IpcServiceImpl
|
|
22
|
+
# MotionDetection_Enable is read as getString() in IpcServiceImpl -> use str
|
|
23
23
|
return await self.async_set_device_attribute(
|
|
24
24
|
"MotionDetection_Enable", "1" if enabled else "0")
|
|
25
25
|
|
|
@@ -108,7 +108,7 @@ class _CameraControlsMixin:
|
|
|
108
108
|
speed: 0-255, default 4 (matches app default)
|
|
109
109
|
preset: preset slot for "goto" command
|
|
110
110
|
|
|
111
|
-
Source: a.java d1() / f0.java A2()
|
|
111
|
+
Source: a.java d1() / f0.java A2() -> avSendIOCtrl(4097, 8B payload)
|
|
112
112
|
Payload: [direction_code, speed, preset, 0, 0, 0, 0, 0]
|
|
113
113
|
|
|
114
114
|
Direction codes (AVIOCTRLDEFs.java):
|
|
@@ -128,7 +128,7 @@ class _CameraControlsMixin:
|
|
|
128
128
|
return False
|
|
129
129
|
ok = session._avio_cmd(4097, payload)
|
|
130
130
|
_LOGGER.debug(
|
|
131
|
-
"PTZ %s (code=%d speed=%d preset=%d)
|
|
131
|
+
"PTZ %s (code=%d speed=%d preset=%d) -> %s",
|
|
132
132
|
direction, code, speed, preset, "sent" if ok else "no channel yet",
|
|
133
133
|
)
|
|
134
134
|
return ok
|
|
@@ -144,7 +144,7 @@ class _CameraControlsMixin:
|
|
|
144
144
|
mirroring the official app's HD/SD toggle. Rides the active stream
|
|
145
145
|
session (SETSTREAMCTRL=800), so the camera must be streaming.
|
|
146
146
|
|
|
147
|
-
Source: f0.java g3()
|
|
147
|
+
Source: f0.java g3() -> X2(800, SetStreamCtrlReq.parseContent(0, quality)).
|
|
148
148
|
Payload <IB3x> = channel(0) + quality byte + 3 reserved.
|
|
149
149
|
"""
|
|
150
150
|
q = _STREAM_QUALITY.get(quality.lower())
|
|
@@ -158,7 +158,7 @@ class _CameraControlsMixin:
|
|
|
158
158
|
return False
|
|
159
159
|
ok = session._avio_cmd(SETSTREAMCTRL_CMD, payload)
|
|
160
160
|
_LOGGER.debug(
|
|
161
|
-
"set resolution %s (quality=%d)
|
|
161
|
+
"set resolution %s (quality=%d) -> %s",
|
|
162
162
|
quality, q, "sent" if ok else "no channel yet",
|
|
163
163
|
)
|
|
164
164
|
return ok
|
|
@@ -11,7 +11,7 @@ Design constraints (validated against LK.IPC.A000088 firmware):
|
|
|
11
11
|
AES-ECB-encrypted JSON body, keyed by the device's 16-char ``aesKey``.
|
|
12
12
|
* It is **single-session**: a second ``loginReq`` evicts the first. So this client
|
|
13
13
|
never holds a socket open - every operation is a short-lived
|
|
14
|
-
*connect
|
|
14
|
+
*connect -> login -> command(s) -> close*, serialized by a per-camera lock.
|
|
15
15
|
* The camera acks changes with ``setDevAttrResp`` but emits no ``setDevAttrNotif``,
|
|
16
16
|
so there is no push; status is obtained by polling :meth:`async_get_attributes`.
|
|
17
17
|
* Only cameras that advertise ``localCtrFlag == 1`` on unicast discovery AND are
|
|
@@ -89,7 +89,7 @@ async def discover_unicast(ip: str, timeout: float = 2.0) -> Optional[dict]:
|
|
|
89
89
|
|
|
90
90
|
Cameras ignore the broadcast sweep but answer a unicast probe. Returns the
|
|
91
91
|
``payload`` dict (``devId``, ``mac``, ``productModel``, ``lanMode``,
|
|
92
|
-
``localCtrFlag``
|
|
92
|
+
``localCtrFlag`` ...) or ``None`` if nothing answered.
|
|
93
93
|
"""
|
|
94
94
|
msg = {
|
|
95
95
|
"protocolVer": "2.0.0",
|
|
@@ -138,7 +138,7 @@ class CameraStatusData(DeviceStatusData):
|
|
|
138
138
|
|
|
139
139
|
Accepts either a setDevAttrNotif ``attr`` dict or a cloud device
|
|
140
140
|
``properties`` dict - both share the same camera attribute keys
|
|
141
|
-
(Battery_remaining, Occupancy, SDcardStatus, MotionDetection_*,
|
|
141
|
+
(Battery_remaining, Occupancy, SDcardStatus, MotionDetection_*, ...).
|
|
142
142
|
"""
|
|
143
143
|
self.update({
|
|
144
144
|
k: v for k, v in attrs.items()
|
|
@@ -328,7 +328,7 @@ def _compress_sdp_for_camera(sdp: str) -> str:
|
|
|
328
328
|
|
|
329
329
|
|
|
330
330
|
def _make_talk_audio_track(pcm_provider: Callable[[], "Optional[bytes]"]):
|
|
331
|
-
"""Build an aiortc MediaStreamTrack that emits viewer
|
|
331
|
+
"""Build an aiortc MediaStreamTrack that emits viewer->camera talk audio.
|
|
332
332
|
|
|
333
333
|
Mirrors the official app (f0.java:695): a real audio track is present on the
|
|
334
334
|
PeerConnection from the start, so the camera sees a genuine sender. The
|
|
@@ -1185,7 +1185,7 @@ def _mqtt_session_sync(
|
|
|
1185
1185
|
conn_ev = threading.Event()
|
|
1186
1186
|
status = {"connected": False, "rc": None, "rc_str": "", "error": None, "log": []}
|
|
1187
1187
|
|
|
1188
|
-
# Build client - handle paho
|
|
1188
|
+
# Build client - handle paho >=2.0 (VERSION2) and <2.0
|
|
1189
1189
|
try:
|
|
1190
1190
|
client = _paho.Client(
|
|
1191
1191
|
callback_api_version=_paho.CallbackAPIVersion.VERSION2,
|
|
@@ -1203,7 +1203,7 @@ def _mqtt_session_sync(
|
|
|
1203
1203
|
client.tls_set_context(ctx)
|
|
1204
1204
|
|
|
1205
1205
|
def _on_connect(c, ud, flags, reason_code, props=None):
|
|
1206
|
-
# paho
|
|
1206
|
+
# paho >=2 passes ReasonCode; paho <2 passes int
|
|
1207
1207
|
try:
|
|
1208
1208
|
rc = int(reason_code)
|
|
1209
1209
|
except (TypeError, ValueError):
|
|
@@ -1742,7 +1742,7 @@ def _upgrade_sctp(sdp: str) -> str:
|
|
|
1742
1742
|
RFC 8841 (and cameras / modern browsers) expect:
|
|
1743
1743
|
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
|
|
1744
1744
|
a=sctp-port:5000
|
|
1745
|
-
a=max-message-size:65536
|
|
1745
|
+
a=max-message-size:65536 <- required by RFC 8841 section 4.3.1
|
|
1746
1746
|
"""
|
|
1747
1747
|
import re as _re
|
|
1748
1748
|
out = []
|
|
@@ -1759,7 +1759,7 @@ def _upgrade_sctp(sdp: str) -> str:
|
|
|
1759
1759
|
def _normalize_bundle_ice_credentials(sdp: str) -> str:
|
|
1760
1760
|
"""Unify all m-section ICE credentials to match the BUNDLE master (mid:0).
|
|
1761
1761
|
|
|
1762
|
-
RFC 8843
|
|
1762
|
+
RFC 8843 section 7.1.3 requires all bundled m-sections to carry the same
|
|
1763
1763
|
ice-ufrag and ice-pwd. aiortc generates a separate ICETransport per
|
|
1764
1764
|
transceiver, giving each a unique credential pair. Cameras that
|
|
1765
1765
|
validate this requirement silently reject offers with mismatched
|