python-aidot-cameras 0.10.2__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.2/python_aidot_cameras.egg-info → python_aidot_cameras-0.10.3}/PKG-INFO +8 -6
- 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.2 → python_aidot_cameras-0.10.3}/aidot/camera/protocol.py +1 -1
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/webrtc_open.py +7 -7
- python_aidot_cameras-0.10.3/aidot/py.typed +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/pyproject.toml +26 -12
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info}/PKG-INFO +8 -6
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/SOURCES.txt +33 -0
- python_aidot_cameras-0.10.3/python_aidot_cameras.egg-info/requires.txt +16 -0
- python_aidot_cameras-0.10.2/python_aidot_cameras.egg-info/requires.txt +0 -14
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/LICENSE +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/README.md +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/__main__.py +0 -0
- /python_aidot_cameras-0.10.2/aidot/py.typed → /python_aidot_cameras-0.10.3/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/client.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/client.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/const.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/setup.cfg +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.10.2 → python_aidot_cameras-0.10.3}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.10.2/python_aidot_cameras.egg-info → python_aidot_cameras-0.10.3}/PKG-INFO
RENAMED
|
@@ -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
|
|
@@ -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
|