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.
Files changed (123) hide show
  1. python_aidot_cameras-0.10.3/MANIFEST.in +1 -0
  2. {python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.10.3}/PKG-INFO +16 -14
  3. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/README.md +8 -8
  4. python_aidot_cameras-0.10.3/aidot/_vendor/__init__.py +2 -0
  5. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/__init__.py +100 -0
  6. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/clock.py +29 -0
  7. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/__init__.py +190 -0
  8. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/base.py +24 -0
  9. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/g711.py +89 -0
  10. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/g722.py +77 -0
  11. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/h264.py +319 -0
  12. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/opus.py +77 -0
  13. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/codecs/vpx.py +286 -0
  14. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/media.py +637 -0
  15. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/signaling.py +251 -0
  16. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/events.py +20 -0
  17. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/exceptions.py +14 -0
  18. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/jitterbuffer.py +124 -0
  19. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/mediastreams.py +148 -0
  20. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rate.py +579 -0
  21. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcconfiguration.py +66 -0
  22. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcdatachannel.py +221 -0
  23. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcdtlstransport.py +754 -0
  24. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcicetransport.py +377 -0
  25. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcpeerconnection.py +1397 -0
  26. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtpparameters.py +170 -0
  27. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtpreceiver.py +624 -0
  28. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtpsender.py +487 -0
  29. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcrtptransceiver.py +154 -0
  30. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcsctptransport.py +1832 -0
  31. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtcsessiondescription.py +19 -0
  32. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/rtp.py +790 -0
  33. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/sdp.py +588 -0
  34. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/stats.py +114 -0
  35. python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/utils.py +54 -0
  36. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/client.py +24 -24
  37. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/controls.py +5 -5
  38. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/lan_control.py +2 -2
  39. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/models.py +1 -1
  40. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/protocol.py +6 -6
  41. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/sdes_open.py +46 -46
  42. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/webrtc.py +1 -1
  43. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/webrtc_open.py +58 -58
  44. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/client.py +1 -1
  45. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/const.py +1 -1
  46. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/credentials.py +5 -5
  47. python_aidot_cameras-0.10.3/aidot/py.typed +0 -0
  48. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/pyproject.toml +30 -15
  49. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info}/PKG-INFO +16 -14
  50. python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info/SOURCES.txt +118 -0
  51. python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info/requires.txt +16 -0
  52. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_backoff.py +1 -1
  53. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_device_login_guard.py +1 -1
  54. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_device_user_info_cache.py +1 -1
  55. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_dtls_skip_signaling_wait.py +1 -1
  56. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_go2rtc.py +1 -1
  57. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_go2rtc_cli.py +1 -1
  58. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_ice_config_cache.py +1 -1
  59. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_keyframe_prompter.py +1 -1
  60. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_lan_control.py +1 -1
  61. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_live_stream_param.py +1 -1
  62. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_narrow_pc_ice.py +3 -3
  63. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_open_gate_delay.py +1 -1
  64. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_persistent_mqtt.py +1 -1
  65. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_persistent_mqtt_robustness.py +1 -1
  66. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_post_merge_hardening.py +1 -1
  67. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_retry_policy.py +1 -1
  68. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_adaptive.py +1 -1
  69. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_fast_liveplay.py +2 -2
  70. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_idle_release.py +4 -4
  71. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_serve_audio.py +1 -1
  72. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_serve_cmd.py +1 -1
  73. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_security_hardening.py +3 -3
  74. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_serve_relay.py +1 -1
  75. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_session_stats.py +1 -1
  76. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_stream_idle.py +1 -1
  77. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_stream_teardown.py +1 -1
  78. python_aidot_cameras-0.10.1/MANIFEST.in +0 -1
  79. python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -85
  80. python_aidot_cameras-0.10.1/src/python_aidot_cameras.egg-info/requires.txt +0 -14
  81. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/LICENSE +0 -0
  82. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/__init__.py +0 -0
  83. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/__main__.py +0 -0
  84. /python_aidot_cameras-0.10.1/src/aidot/py.typed → /python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
  85. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/aes_utils.py +0 -0
  86. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/__init__.py +0 -0
  87. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/constants.py +0 -0
  88. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/go2rtc.py +0 -0
  89. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/playback.py +0 -0
  90. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/sdes.py +0 -0
  91. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/camera/tutk.py +0 -0
  92. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/device_client.py +0 -0
  93. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/discover.py +0 -0
  94. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/exceptions.py +0 -0
  95. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/g711.py +0 -0
  96. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/login_const.py +0 -0
  97. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/__init__.py +0 -0
  98. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/device_client_model.py +0 -0
  99. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/device_model.py +0 -0
  100. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/aidot/models/discover_model.py +0 -0
  101. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  102. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  103. {python_aidot_cameras-0.10.1/src → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  104. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/setup.cfg +0 -0
  105. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_aioice_compat.py +0 -0
  106. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_alarm_event.py +0 -0
  107. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_dtls_not_ready_burst.py +0 -0
  108. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_dtls_pinning.py +0 -0
  109. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_egress_guard.py +0 -0
  110. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_highport_nomination.py +0 -0
  111. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_motion_poll.py +0 -0
  112. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_no_undefined_names.py +0 -0
  113. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_playback_tls.py +0 -0
  114. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_echo_wait_timeout.py +0 -0
  115. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_sprop.py +0 -0
  116. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_talk.py +0 -0
  117. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_sdes_watchdog.py +0 -0
  118. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_speak.py +0 -0
  119. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_stream_cap.py +0 -0
  120. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_talk.py +0 -0
  121. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_terminal_ack.py +0 -0
  122. {python_aidot_cameras-0.10.1 → python_aidot_cameras-0.10.3}/tests/test_token_refresh.py +0 -0
  123. {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.1
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: aiortc>=1.9.0; extra == "webrtc"
21
- Requires-Dist: aioice<0.12,>=0.9.0; extra == "webrtc"
22
- Requires-Dist: av; extra == "webrtc"
23
- Requires-Dist: pylibsrtp; extra == "webrtc"
24
- Requires-Dist: pyopenssl; extra == "webrtc"
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) 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).
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, ) needed for live
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 [`src/aidot/credentials.py`](src/aidot/credentials.py).
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 off-subnet/strict-NAT viewers must leave it off. | unset (off) |
156
- | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~23 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
+ | `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%→~19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
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) 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).
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, ) needed for live
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 [`src/aidot/credentials.py`](src/aidot/credentials.py).
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 off-subnet/strict-NAT viewers must leave it off. | unset (off) |
128
- | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~23 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) |
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%→~19%); 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%->~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,2 @@
1
+ """Vendored third-party packages. See aiortc/ (byte-identical to aiortc
2
+ 1.14.0; vendored so HA 2026.7 av==17 is satisfiable, stock aiortc caps av<17)."""
@@ -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