python-aidot-cameras 0.10.1__tar.gz → 0.10.3__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.3/MANIFEST.in +1 -0
- {python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.10.3}/PKG-INFO +16 -14
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/README.md +8 -8
- python_aidot_cameras-0.10.3/aidot/_vendor/__init__.py +2 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/__init__.py +100 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/clock.py +29 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/__init__.py +190 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/base.py +24 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/g711.py +89 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/g722.py +77 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/h264.py +319 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/opus.py +77 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/vpx.py +286 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/media.py +637 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/signaling.py +251 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/events.py +20 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/exceptions.py +14 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/jitterbuffer.py +124 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/mediastreams.py +148 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rate.py +579 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcconfiguration.py +66 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcdatachannel.py +221 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcdtlstransport.py +754 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcicetransport.py +377 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcpeerconnection.py +1397 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtpparameters.py +170 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtpreceiver.py +624 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtpsender.py +487 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtptransceiver.py +154 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcsctptransport.py +1832 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcsessiondescription.py +19 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtp.py +790 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/sdp.py +588 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/stats.py +114 -0
- python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/utils.py +54 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/client.py +24 -24
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/controls.py +5 -5
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/lan_control.py +2 -2
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/models.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/protocol.py +6 -6
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/sdes_open.py +46 -46
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/webrtc.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/webrtc_open.py +58 -58
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/client.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/const.py +1 -1
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/credentials.py +5 -5
- python_aidot_cameras-0.10.3/aidot/py.typed +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/pyproject.toml +30 -15
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info}/PKG-INFO +16 -14
- python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info/SOURCES.txt +118 -0
- python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info/requires.txt +16 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_backoff.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_device_login_guard.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_device_user_info_cache.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_dtls_skip_signaling_wait.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_go2rtc.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_go2rtc_cli.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_ice_config_cache.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_keyframe_prompter.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_lan_control.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_live_stream_param.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_narrow_pc_ice.py +3 -3
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_open_gate_delay.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_persistent_mqtt.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_persistent_mqtt_robustness.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_post_merge_hardening.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_retry_policy.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_adaptive.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_fast_liveplay.py +2 -2
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_idle_release.py +4 -4
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_serve_audio.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_serve_cmd.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_security_hardening.py +3 -3
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_serve_relay.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_session_stats.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_stream_idle.py +1 -1
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_stream_teardown.py +1 -1
- python_aidot_cameras-0.10.1/MANIFEST.in +0 -1
- python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -85
- python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info/requires.txt +0 -14
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/LICENSE +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/__main__.py +0 -0
- /python_aidot_cameras-0.10.1/src/aidot/py.typed → /python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/setup.cfg +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/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.3
|
|
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
|
|
@@ -17,11 +17,13 @@ Requires-Dist: cryptography>=42.0
|
|
|
17
17
|
Requires-Dist: pycryptodome>=3.20
|
|
18
18
|
Requires-Dist: dacite>=1.8
|
|
19
19
|
Provides-Extra: webrtc
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist: aioice<0.12,>=0.
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: av<18.0.0,>=14.0.0; extra == "webrtc"
|
|
21
|
+
Requires-Dist: aioice<0.12,>=0.10.1; extra == "webrtc"
|
|
22
|
+
Requires-Dist: pylibsrtp>=0.10.0; extra == "webrtc"
|
|
23
|
+
Requires-Dist: pyopenssl>=25.0.0; extra == "webrtc"
|
|
24
|
+
Requires-Dist: cryptography>=44.0.0; extra == "webrtc"
|
|
25
|
+
Requires-Dist: pyee>=13.0.0; extra == "webrtc"
|
|
26
|
+
Requires-Dist: google-crc32c>=1.1; extra == "webrtc"
|
|
25
27
|
Requires-Dist: numpy; extra == "webrtc"
|
|
26
28
|
Requires-Dist: Pillow; extra == "webrtc"
|
|
27
29
|
Dynamic: license-file
|
|
@@ -49,9 +51,9 @@ on this library.
|
|
|
49
51
|
|
|
50
52
|
The streaming transport is auto-selected per camera from its model id:
|
|
51
53
|
|
|
52
|
-
- **A000088** (M3 Pro)
|
|
53
|
-
- **A001513** ("L2")
|
|
54
|
-
- **A001064** (PTZ)
|
|
54
|
+
- **A000088** (M3 Pro) - DTLS-SRTP, wired/mains.
|
|
55
|
+
- **A001513** ("L2") - SDES-SRTP, **battery** (woken on demand; validated end-to-end).
|
|
56
|
+
- **A001064** (PTZ) - SDES-SRTP, wired/mains (role-reversal handshake).
|
|
55
57
|
|
|
56
58
|
Other battery models (A001108, A001360) are recognized in code with the same
|
|
57
59
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
@@ -68,7 +70,7 @@ pip install python-aidot-cameras
|
|
|
68
70
|
pip install python-aidot-cameras[webrtc]
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
`[webrtc]` pulls in the extra dependencies (aiortc, av,
|
|
73
|
+
`[webrtc]` pulls in the extra dependencies (aiortc, av, ...) needed for live
|
|
72
74
|
streaming, snapshots, and two-way audio. Without it you still get lights plus
|
|
73
75
|
the camera cloud/control APIs, but not live media.
|
|
74
76
|
|
|
@@ -132,7 +134,7 @@ The library reads the following environment variables.
|
|
|
132
134
|
### Credentials
|
|
133
135
|
|
|
134
136
|
Used by the credential helper (`aidot.credentials`); they take priority over any
|
|
135
|
-
stored credentials file. See [`
|
|
137
|
+
stored credentials file. See [`aidot/credentials.py`](aidot/credentials.py).
|
|
136
138
|
|
|
137
139
|
| Variable | Purpose | Default |
|
|
138
140
|
| --- | --- | --- |
|
|
@@ -152,12 +154,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
152
154
|
| --- | --- | --- |
|
|
153
155
|
| `AIDOT_MAX_CONCURRENT_OPENS` | Caps how many stream opens run concurrently across all cameras. | `2` |
|
|
154
156
|
| `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
|
|
157
|
+
| `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) |
|
|
158
|
+
| `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
159
|
| `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
160
|
| `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
161
|
| `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
|
|
162
|
+
| `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
163
|
| `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
164
|
| `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
165
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .exceptions import InvalidAccessError, InvalidStateError
|
|
5
|
+
from .mediastreams import (
|
|
6
|
+
AudioStreamTrack,
|
|
7
|
+
MediaStreamError,
|
|
8
|
+
MediaStreamTrack,
|
|
9
|
+
VideoStreamTrack,
|
|
10
|
+
)
|
|
11
|
+
from .rtcconfiguration import RTCBundlePolicy, RTCConfiguration, RTCIceServer
|
|
12
|
+
from .rtcdatachannel import RTCDataChannel, RTCDataChannelParameters
|
|
13
|
+
from .rtcdtlstransport import (
|
|
14
|
+
RTCCertificate,
|
|
15
|
+
RTCDtlsFingerprint,
|
|
16
|
+
RTCDtlsParameters,
|
|
17
|
+
RTCDtlsTransport,
|
|
18
|
+
)
|
|
19
|
+
from .rtcicetransport import (
|
|
20
|
+
RTCIceCandidate,
|
|
21
|
+
RTCIceGatherer,
|
|
22
|
+
RTCIceParameters,
|
|
23
|
+
RTCIceTransport,
|
|
24
|
+
)
|
|
25
|
+
from .rtcpeerconnection import RTCPeerConnection
|
|
26
|
+
from .rtcrtpparameters import (
|
|
27
|
+
RTCRtcpParameters,
|
|
28
|
+
RTCRtpCapabilities,
|
|
29
|
+
RTCRtpCodecCapability,
|
|
30
|
+
RTCRtpCodecParameters,
|
|
31
|
+
RTCRtpHeaderExtensionCapability,
|
|
32
|
+
RTCRtpHeaderExtensionParameters,
|
|
33
|
+
RTCRtpParameters,
|
|
34
|
+
)
|
|
35
|
+
from .rtcrtpreceiver import (
|
|
36
|
+
RTCRtpContributingSource,
|
|
37
|
+
RTCRtpReceiver,
|
|
38
|
+
RTCRtpSynchronizationSource,
|
|
39
|
+
)
|
|
40
|
+
from .rtcrtpsender import RTCRtpSender
|
|
41
|
+
from .rtcrtptransceiver import RTCRtpTransceiver
|
|
42
|
+
from .rtcsctptransport import RTCSctpCapabilities, RTCSctpTransport
|
|
43
|
+
from .rtcsessiondescription import RTCSessionDescription
|
|
44
|
+
from .stats import (
|
|
45
|
+
RTCInboundRtpStreamStats,
|
|
46
|
+
RTCOutboundRtpStreamStats,
|
|
47
|
+
RTCRemoteInboundRtpStreamStats,
|
|
48
|
+
RTCRemoteOutboundRtpStreamStats,
|
|
49
|
+
RTCStatsReport,
|
|
50
|
+
RTCTransportStats,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__version__ = "1.14.0"
|
|
54
|
+
|
|
55
|
+
# Set default logging handler to avoid "No handler found" warnings.
|
|
56
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"AudioStreamTrack",
|
|
60
|
+
"InvalidAccessError",
|
|
61
|
+
"InvalidStateError",
|
|
62
|
+
"MediaStreamError",
|
|
63
|
+
"MediaStreamTrack",
|
|
64
|
+
"RTCBundlePolicy",
|
|
65
|
+
"RTCCertificate",
|
|
66
|
+
"RTCConfiguration",
|
|
67
|
+
"RTCDataChannel",
|
|
68
|
+
"RTCDataChannelParameters",
|
|
69
|
+
"RTCDtlsFingerprint",
|
|
70
|
+
"RTCDtlsParameters",
|
|
71
|
+
"RTCDtlsTransport",
|
|
72
|
+
"RTCIceCandidate",
|
|
73
|
+
"RTCIceGatherer",
|
|
74
|
+
"RTCIceParameters",
|
|
75
|
+
"RTCIceServer",
|
|
76
|
+
"RTCIceTransport",
|
|
77
|
+
"RTCInboundRtpStreamStats",
|
|
78
|
+
"RTCOutboundRtpStreamStats",
|
|
79
|
+
"RTCPeerConnection",
|
|
80
|
+
"RTCRemoteInboundRtpStreamStats",
|
|
81
|
+
"RTCRemoteOutboundRtpStreamStats",
|
|
82
|
+
"RTCRtcpParameters",
|
|
83
|
+
"RTCRtpCapabilities",
|
|
84
|
+
"RTCRtpCodecCapability",
|
|
85
|
+
"RTCRtpCodecParameters",
|
|
86
|
+
"RTCRtpContributingSource",
|
|
87
|
+
"RTCRtpHeaderExtensionCapability",
|
|
88
|
+
"RTCRtpHeaderExtensionParameters",
|
|
89
|
+
"RTCRtpParameters",
|
|
90
|
+
"RTCRtpReceiver",
|
|
91
|
+
"RTCRtpSender",
|
|
92
|
+
"RTCRtpSynchronizationSource",
|
|
93
|
+
"RTCRtpTransceiver",
|
|
94
|
+
"RTCSctpCapabilities",
|
|
95
|
+
"RTCSctpTransport",
|
|
96
|
+
"RTCSessionDescription",
|
|
97
|
+
"RTCStatsReport",
|
|
98
|
+
"RTCTransportStats",
|
|
99
|
+
"VideoStreamTrack",
|
|
100
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
NTP_EPOCH = datetime.datetime(1900, 1, 1, tzinfo=datetime.timezone.utc)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def current_datetime() -> datetime.datetime:
|
|
7
|
+
return datetime.datetime.now(datetime.timezone.utc)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def current_ms() -> int:
|
|
11
|
+
delta = current_datetime() - NTP_EPOCH
|
|
12
|
+
return int(delta.total_seconds() * 1000)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def current_ntp_time() -> int:
|
|
16
|
+
return datetime_to_ntp(current_datetime())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def datetime_from_ntp(ntp: int) -> datetime.datetime:
|
|
20
|
+
seconds = ntp >> 32
|
|
21
|
+
microseconds = ((ntp & 0xFFFFFFFF) * 1000000) / (1 << 32)
|
|
22
|
+
return NTP_EPOCH + datetime.timedelta(seconds=seconds, microseconds=microseconds)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def datetime_to_ntp(dt: datetime.datetime) -> int:
|
|
26
|
+
delta = dt - NTP_EPOCH
|
|
27
|
+
high = int(delta.total_seconds())
|
|
28
|
+
low = round((delta.microseconds * (1 << 32)) // 1000000)
|
|
29
|
+
return (high << 32) | low
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from ..rtcrtpparameters import (
|
|
4
|
+
ParametersDict,
|
|
5
|
+
RTCRtcpFeedback,
|
|
6
|
+
RTCRtpCapabilities,
|
|
7
|
+
RTCRtpCodecCapability,
|
|
8
|
+
RTCRtpCodecParameters,
|
|
9
|
+
RTCRtpHeaderExtensionCapability,
|
|
10
|
+
RTCRtpHeaderExtensionParameters,
|
|
11
|
+
)
|
|
12
|
+
from .base import Decoder, Encoder
|
|
13
|
+
from .g711 import PcmaDecoder, PcmaEncoder, PcmuDecoder, PcmuEncoder
|
|
14
|
+
from .g722 import G722Decoder, G722Encoder
|
|
15
|
+
from .h264 import H264Decoder, H264Encoder, h264_depayload
|
|
16
|
+
from .opus import OpusDecoder, OpusEncoder
|
|
17
|
+
from .vpx import Vp8Decoder, Vp8Encoder, vp8_depayload
|
|
18
|
+
|
|
19
|
+
# The clockrate for G.722 is 8kHz even though the sampling rate is 16kHz.
|
|
20
|
+
# See https://datatracker.ietf.org/doc/html/rfc3551
|
|
21
|
+
G722_CODEC = RTCRtpCodecParameters(
|
|
22
|
+
mimeType="audio/G722", clockRate=8000, channels=1, payloadType=9
|
|
23
|
+
)
|
|
24
|
+
PCMU_CODEC = RTCRtpCodecParameters(
|
|
25
|
+
mimeType="audio/PCMU", clockRate=8000, channels=1, payloadType=0
|
|
26
|
+
)
|
|
27
|
+
PCMA_CODEC = RTCRtpCodecParameters(
|
|
28
|
+
mimeType="audio/PCMA", clockRate=8000, channels=1, payloadType=8
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
CODECS: dict[str, list[RTCRtpCodecParameters]] = {
|
|
32
|
+
"audio": [
|
|
33
|
+
RTCRtpCodecParameters(
|
|
34
|
+
mimeType="audio/opus", clockRate=48000, channels=2, payloadType=96
|
|
35
|
+
),
|
|
36
|
+
G722_CODEC,
|
|
37
|
+
PCMU_CODEC,
|
|
38
|
+
PCMA_CODEC,
|
|
39
|
+
],
|
|
40
|
+
"video": [],
|
|
41
|
+
}
|
|
42
|
+
# Note, the id space for these extensions is shared across media types when BUNDLE
|
|
43
|
+
# is negotiated. If you add a audio- or video-specific extension, make sure it has
|
|
44
|
+
# a unique id.
|
|
45
|
+
HEADER_EXTENSIONS: dict[str, list[RTCRtpHeaderExtensionParameters]] = {
|
|
46
|
+
"audio": [
|
|
47
|
+
RTCRtpHeaderExtensionParameters(
|
|
48
|
+
id=1, uri="urn:ietf:params:rtp-hdrext:sdes:mid"
|
|
49
|
+
),
|
|
50
|
+
RTCRtpHeaderExtensionParameters(
|
|
51
|
+
id=2, uri="urn:ietf:params:rtp-hdrext:ssrc-audio-level"
|
|
52
|
+
),
|
|
53
|
+
],
|
|
54
|
+
"video": [
|
|
55
|
+
RTCRtpHeaderExtensionParameters(
|
|
56
|
+
id=1, uri="urn:ietf:params:rtp-hdrext:sdes:mid"
|
|
57
|
+
),
|
|
58
|
+
RTCRtpHeaderExtensionParameters(
|
|
59
|
+
id=3, uri="http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"
|
|
60
|
+
),
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def init_codecs() -> None:
|
|
66
|
+
dynamic_pt = 97
|
|
67
|
+
|
|
68
|
+
def add_video_codec(
|
|
69
|
+
mimeType: str, parameters: Optional[ParametersDict] = None
|
|
70
|
+
) -> None:
|
|
71
|
+
nonlocal dynamic_pt
|
|
72
|
+
|
|
73
|
+
clockRate = 90000
|
|
74
|
+
CODECS["video"] += [
|
|
75
|
+
RTCRtpCodecParameters(
|
|
76
|
+
mimeType=mimeType,
|
|
77
|
+
clockRate=clockRate,
|
|
78
|
+
payloadType=dynamic_pt,
|
|
79
|
+
rtcpFeedback=[
|
|
80
|
+
RTCRtcpFeedback(type="nack"),
|
|
81
|
+
RTCRtcpFeedback(type="nack", parameter="pli"),
|
|
82
|
+
RTCRtcpFeedback(type="goog-remb"),
|
|
83
|
+
],
|
|
84
|
+
parameters=parameters or {},
|
|
85
|
+
),
|
|
86
|
+
RTCRtpCodecParameters(
|
|
87
|
+
mimeType="video/rtx",
|
|
88
|
+
clockRate=clockRate,
|
|
89
|
+
payloadType=dynamic_pt + 1,
|
|
90
|
+
parameters={"apt": dynamic_pt},
|
|
91
|
+
),
|
|
92
|
+
]
|
|
93
|
+
dynamic_pt += 2
|
|
94
|
+
|
|
95
|
+
add_video_codec("video/VP8")
|
|
96
|
+
for profile_level_id in ("42001f", "42e01f"):
|
|
97
|
+
add_video_codec(
|
|
98
|
+
"video/H264",
|
|
99
|
+
{
|
|
100
|
+
"level-asymmetry-allowed": "1",
|
|
101
|
+
"packetization-mode": "1",
|
|
102
|
+
"profile-level-id": profile_level_id,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def depayload(codec: RTCRtpCodecParameters, payload: bytes) -> bytes:
|
|
108
|
+
if codec.name == "VP8":
|
|
109
|
+
return vp8_depayload(payload)
|
|
110
|
+
elif codec.name == "H264":
|
|
111
|
+
return h264_depayload(payload)
|
|
112
|
+
else:
|
|
113
|
+
return payload
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_capabilities(kind: str) -> RTCRtpCapabilities:
|
|
117
|
+
if kind not in CODECS:
|
|
118
|
+
raise ValueError(f"cannot get capabilities for unknown media {kind}")
|
|
119
|
+
|
|
120
|
+
codecs = []
|
|
121
|
+
rtx_added = False
|
|
122
|
+
for params in CODECS[kind]:
|
|
123
|
+
if not is_rtx(params):
|
|
124
|
+
codecs.append(
|
|
125
|
+
RTCRtpCodecCapability(
|
|
126
|
+
mimeType=params.mimeType,
|
|
127
|
+
clockRate=params.clockRate,
|
|
128
|
+
channels=params.channels,
|
|
129
|
+
parameters=params.parameters,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
elif not rtx_added:
|
|
133
|
+
# There will only be a single entry in codecs[] for retransmission
|
|
134
|
+
# via RTX, with sdpFmtpLine not present.
|
|
135
|
+
codecs.append(
|
|
136
|
+
RTCRtpCodecCapability(
|
|
137
|
+
mimeType=params.mimeType, clockRate=params.clockRate
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
rtx_added = True
|
|
141
|
+
|
|
142
|
+
headerExtensions = []
|
|
143
|
+
for extension in HEADER_EXTENSIONS[kind]:
|
|
144
|
+
headerExtensions.append(RTCRtpHeaderExtensionCapability(uri=extension.uri))
|
|
145
|
+
return RTCRtpCapabilities(codecs=codecs, headerExtensions=headerExtensions)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_decoder(codec: RTCRtpCodecParameters) -> Decoder:
|
|
149
|
+
mimeType = codec.mimeType.lower()
|
|
150
|
+
|
|
151
|
+
if mimeType == "audio/g722":
|
|
152
|
+
return G722Decoder()
|
|
153
|
+
elif mimeType == "audio/opus":
|
|
154
|
+
return OpusDecoder()
|
|
155
|
+
elif mimeType == "audio/pcma":
|
|
156
|
+
return PcmaDecoder()
|
|
157
|
+
elif mimeType == "audio/pcmu":
|
|
158
|
+
return PcmuDecoder()
|
|
159
|
+
elif mimeType == "video/h264":
|
|
160
|
+
return H264Decoder()
|
|
161
|
+
elif mimeType == "video/vp8":
|
|
162
|
+
return Vp8Decoder()
|
|
163
|
+
else:
|
|
164
|
+
raise ValueError(f"No decoder found for MIME type `{mimeType}`")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_encoder(codec: RTCRtpCodecParameters) -> Encoder:
|
|
168
|
+
mimeType = codec.mimeType.lower()
|
|
169
|
+
|
|
170
|
+
if mimeType == "audio/g722":
|
|
171
|
+
return G722Encoder()
|
|
172
|
+
elif mimeType == "audio/opus":
|
|
173
|
+
return OpusEncoder()
|
|
174
|
+
elif mimeType == "audio/pcma":
|
|
175
|
+
return PcmaEncoder()
|
|
176
|
+
elif mimeType == "audio/pcmu":
|
|
177
|
+
return PcmuEncoder()
|
|
178
|
+
elif mimeType == "video/h264":
|
|
179
|
+
return H264Encoder()
|
|
180
|
+
elif mimeType == "video/vp8":
|
|
181
|
+
return Vp8Encoder()
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError(f"No encoder found for MIME type `{mimeType}`")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def is_rtx(codec: Union[RTCRtpCodecCapability, RTCRtpCodecParameters]) -> bool:
|
|
187
|
+
return codec.name.lower() == "rtx"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
init_codecs()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
|
|
3
|
+
from av.frame import Frame
|
|
4
|
+
from av.packet import Packet
|
|
5
|
+
|
|
6
|
+
from ..jitterbuffer import JitterFrame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Decoder(metaclass=ABCMeta):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def decode(self, encoded_frame: JitterFrame) -> list[Frame]:
|
|
12
|
+
pass # pragma: no cover
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Encoder(metaclass=ABCMeta):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def encode(
|
|
18
|
+
self, frame: Frame, force_keyframe: bool = False
|
|
19
|
+
) -> tuple[list[bytes], int]:
|
|
20
|
+
pass # pragma: no cover
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def pack(self, packet: Packet) -> tuple[list[bytes], int]:
|
|
24
|
+
pass # pragma: no cover
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fractions
|
|
2
|
+
from typing import Literal, cast
|
|
3
|
+
|
|
4
|
+
from av import AudioFrame, AudioResampler, CodecContext
|
|
5
|
+
from av.frame import Frame
|
|
6
|
+
from av.packet import Packet
|
|
7
|
+
|
|
8
|
+
from ..jitterbuffer import JitterFrame
|
|
9
|
+
from ..mediastreams import convert_timebase
|
|
10
|
+
from .base import Decoder, Encoder
|
|
11
|
+
|
|
12
|
+
SAMPLE_RATE = 8000
|
|
13
|
+
SAMPLE_WIDTH = 2
|
|
14
|
+
SAMPLES_PER_FRAME = 160
|
|
15
|
+
TIME_BASE = fractions.Fraction(1, 8000)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PcmDecoder(Decoder):
|
|
19
|
+
def __init__(self, codec_name: Literal["pcm_alaw", "pcm_mulaw"]) -> None:
|
|
20
|
+
self.codec = CodecContext.create(codec_name, "r")
|
|
21
|
+
self.codec.format = "s16"
|
|
22
|
+
self.codec.layout = "mono"
|
|
23
|
+
self.codec.sample_rate = SAMPLE_RATE
|
|
24
|
+
|
|
25
|
+
def decode(self, encoded_frame: JitterFrame) -> list[Frame]:
|
|
26
|
+
packet = Packet(encoded_frame.data)
|
|
27
|
+
packet.pts = encoded_frame.timestamp
|
|
28
|
+
packet.time_base = TIME_BASE
|
|
29
|
+
return cast(list[Frame], self.codec.decode(packet))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PcmEncoder(Encoder):
|
|
33
|
+
def __init__(self, codec_name: Literal["pcm_alaw", "pcm_mulaw"]) -> None:
|
|
34
|
+
self.codec = CodecContext.create(codec_name, "w")
|
|
35
|
+
self.codec.format = "s16"
|
|
36
|
+
self.codec.layout = "mono"
|
|
37
|
+
self.codec.sample_rate = SAMPLE_RATE
|
|
38
|
+
self.codec.time_base = TIME_BASE
|
|
39
|
+
|
|
40
|
+
# Create our own resampler to control the frame size.
|
|
41
|
+
self.resampler = AudioResampler(
|
|
42
|
+
format="s16",
|
|
43
|
+
layout="mono",
|
|
44
|
+
rate=SAMPLE_RATE,
|
|
45
|
+
frame_size=SAMPLES_PER_FRAME,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def encode(
|
|
49
|
+
self, frame: Frame, force_keyframe: bool = False
|
|
50
|
+
) -> tuple[list[bytes], int]:
|
|
51
|
+
assert isinstance(frame, AudioFrame)
|
|
52
|
+
assert frame.format.name == "s16"
|
|
53
|
+
assert frame.layout.name in ["mono", "stereo"]
|
|
54
|
+
|
|
55
|
+
# Send frame through resampler and encoder.
|
|
56
|
+
packets = []
|
|
57
|
+
for frame in self.resampler.resample(frame):
|
|
58
|
+
packets += self.codec.encode(frame)
|
|
59
|
+
|
|
60
|
+
if packets:
|
|
61
|
+
# Packets were returned.
|
|
62
|
+
return [bytes(p) for p in packets], packets[0].pts
|
|
63
|
+
else:
|
|
64
|
+
# No packets were returned due to buffering.
|
|
65
|
+
return [], None
|
|
66
|
+
|
|
67
|
+
def pack(self, packet: Packet) -> tuple[list[bytes], int]:
|
|
68
|
+
timestamp = convert_timebase(packet.pts, packet.time_base, TIME_BASE)
|
|
69
|
+
return [bytes(packet)], timestamp
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PcmaDecoder(PcmDecoder):
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
super().__init__("pcm_alaw")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PcmaEncoder(PcmEncoder):
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
super().__init__("pcm_alaw")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PcmuDecoder(PcmDecoder):
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
super().__init__("pcm_mulaw")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PcmuEncoder(PcmEncoder):
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
super().__init__("pcm_mulaw")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fractions
|
|
2
|
+
from typing import Optional, cast
|
|
3
|
+
|
|
4
|
+
from av import AudioCodecContext, AudioFrame, AudioResampler, CodecContext
|
|
5
|
+
from av.frame import Frame
|
|
6
|
+
from av.packet import Packet
|
|
7
|
+
|
|
8
|
+
from ..jitterbuffer import JitterFrame
|
|
9
|
+
from ..mediastreams import convert_timebase
|
|
10
|
+
from .base import Decoder, Encoder
|
|
11
|
+
|
|
12
|
+
SAMPLE_RATE = 16000
|
|
13
|
+
SAMPLE_WIDTH = 2
|
|
14
|
+
SAMPLES_PER_FRAME = 320
|
|
15
|
+
TIME_BASE = fractions.Fraction(1, 16000)
|
|
16
|
+
|
|
17
|
+
# Even though the sample rate is 16kHz, the clockrate is defined as 8kHz.
|
|
18
|
+
# This is why we have multiplications and divisions by 2 in the code.
|
|
19
|
+
CLOCK_BASE = fractions.Fraction(1, 8000)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class G722Decoder(Decoder):
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.codec = cast(AudioCodecContext, CodecContext.create("g722", "r"))
|
|
25
|
+
self.codec.format = "s16"
|
|
26
|
+
self.codec.layout = "mono"
|
|
27
|
+
self.codec.sample_rate = SAMPLE_RATE
|
|
28
|
+
|
|
29
|
+
def decode(self, encoded_frame: JitterFrame) -> list[Frame]:
|
|
30
|
+
packet = Packet(encoded_frame.data)
|
|
31
|
+
packet.pts = encoded_frame.timestamp * 2
|
|
32
|
+
packet.time_base = TIME_BASE
|
|
33
|
+
return cast(list[Frame], self.codec.decode(packet))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class G722Encoder(Encoder):
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.codec = cast(AudioCodecContext, CodecContext.create("g722", "w"))
|
|
39
|
+
self.codec.format = "s16"
|
|
40
|
+
self.codec.layout = "mono"
|
|
41
|
+
self.codec.sample_rate = SAMPLE_RATE
|
|
42
|
+
self.codec.time_base = TIME_BASE
|
|
43
|
+
self.first_pts: Optional[int] = None
|
|
44
|
+
|
|
45
|
+
# Create our own resampler to control the frame size.
|
|
46
|
+
self.resampler = AudioResampler(
|
|
47
|
+
format="s16",
|
|
48
|
+
layout="mono",
|
|
49
|
+
rate=SAMPLE_RATE,
|
|
50
|
+
frame_size=SAMPLES_PER_FRAME,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def encode(
|
|
54
|
+
self, frame: Frame, force_keyframe: bool = False
|
|
55
|
+
) -> tuple[list[bytes], int]:
|
|
56
|
+
assert isinstance(frame, AudioFrame)
|
|
57
|
+
assert frame.format.name == "s16"
|
|
58
|
+
assert frame.layout.name in ["mono", "stereo"]
|
|
59
|
+
|
|
60
|
+
# Send frame through resampler and encoder.
|
|
61
|
+
packets = []
|
|
62
|
+
for frame in self.resampler.resample(frame):
|
|
63
|
+
packets += self.codec.encode(frame)
|
|
64
|
+
|
|
65
|
+
if packets:
|
|
66
|
+
# Packets were returned.
|
|
67
|
+
if self.first_pts is None:
|
|
68
|
+
self.first_pts = packets[0].pts
|
|
69
|
+
timestamp = (packets[0].pts - self.first_pts) // 2
|
|
70
|
+
return [bytes(p) for p in packets], timestamp
|
|
71
|
+
else:
|
|
72
|
+
# No packets were returned due to buffering.
|
|
73
|
+
return [], None
|
|
74
|
+
|
|
75
|
+
def pack(self, packet: Packet) -> tuple[list[bytes], int]:
|
|
76
|
+
timestamp = convert_timebase(packet.pts, packet.time_base, CLOCK_BASE)
|
|
77
|
+
return [bytes(packet)], timestamp
|