Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## Unreleased

* Add `webrtc.aiortc.install_alias()`, which makes `import aiortc` give the compat layer, for libraries that import aiortc themselves by [@MarshalX](https://github.com/MarshalX)
* Add aiortc's `rtp`, `jitterbuffer`, `utils` and `clock` modules and the ICE server helpers of `rtcicetransport` to the compat layer by [@MarshalX](https://github.com/MarshalX)
* Drop a compat layer track's buffered frames when its receiver's `_enabled` is set to `False`, and add the track's `_queue`, for libraries that skip stale frames by [@MarshalX](https://github.com/MarshalX)

## Version 0.1.0

**24.10.2026**
Expand Down
3 changes: 2 additions & 1 deletion THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# aiortc
The code of webrtc/aiortc/sdp.py, mediastreams.py, rtcconfiguration.py, rtcrtpparameters.py,
rtcsessiondescription.py, stats.py, contrib/media.py and contrib/signaling.py, copied from it:
rtcsessiondescription.py, stats.py, rtp.py, jitterbuffer.py, utils.py, clock.py, _ice_servers.py, contrib/media.py and
contrib/signaling.py, copied from it:
```
Copyright (c) Jeremy Lainé.
All rights reserved.
Expand Down
10 changes: 9 additions & 1 deletion docs/gen_api_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@
# the internals of the package, except the events every interface inherits
TOP_LEVEL = ['enums', 'exceptions', 'streams', 'utils.events', 'base', 'openh264', 'loopback']
# internal webrtc.utils modules, without a page
INTERNAL = ['utils.lifetime', 'utils.loops', 'utils.names', 'utils.operations', 'utils.strings', 'utils.transfer']
INTERNAL = [
'utils.lifetime',
'utils.loops',
'utils.module_alias',
'utils.names',
'utils.operations',
'utils.strings',
'utils.transfer',
]

# modules of several public classes, or whose class name isn't the best title
TITLES = {
Expand Down
23 changes: 22 additions & 1 deletion docs/source/guides/aiortc.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ startup, see [H.264](h264.md).

Python WebRTC isn't affiliated with or endorsed by aiortc.

## Libraries that import aiortc

A library that imports `aiortc` itself runs on the layer unchanged. Call `webrtc.aiortc.install_alias()` before
importing it:

```python
import webrtc.aiortc

webrtc.aiortc.install_alias()

import some_library # its aiortc imports now get webrtc.aiortc
```

`aiortc.x` then gives `webrtc.aiortc.x`, and a module the layer lacks raises `ModuleNotFoundError`. Call it first,
in each process: it raises `RuntimeError` if `aiortc` is imported already.

The library's aiortc dependency stays installed but unused. Keep it if the library imports aiortc's dependencies, such
as `aioice` or `cryptography`, directly.

## What differs

Not supported:
Expand All @@ -42,7 +61,9 @@ Not supported:
Transports come from `RTCPeerConnection`.
- **Certificate import.** `RTCCertificate(key, cert)` raises `NotImplementedError`. Generated certificates are ECDSA,
with one SHA-256 fingerprint.
- **Internal modules.** `rtp`, `jitterbuffer`, `clock`, `rate` and `utils` aren't there, and `codecs` has only the
- **Internal modules.** `rtp`, `jitterbuffer`, `clock` and `utils` are aiortc's code, but the engine's packets never
pass through them, so a jitter buffer given to a receiver does nothing. Setting `rtcsctptransport.USERDATA_MAX_LENGTH`
or `rtcdtlstransport.X509_DIGEST_ALGORITHMS` does nothing either. `rate` isn't there, and `codecs` has only the
bitrate constants: no encoder and decoder classes, no `CODECS` or `HEADER_EXTENSIONS` table. `MAX_FRAME_RATE` exists
but isn't applied, as in aiortc.
- **An explicit `id` on a non-negotiated data channel.** It's ignored, as the specification says.
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ extend-exclude = [
"python-webrtc/python/webrtc/aiortc/contrib/media.py", "python-webrtc/python/webrtc/aiortc/contrib/signaling.py",
"python-webrtc/python/webrtc/aiortc/rtcconfiguration.py", "python-webrtc/python/webrtc/aiortc/rtcrtpparameters.py",
"python-webrtc/python/webrtc/aiortc/rtcsessiondescription.py", "python-webrtc/python/webrtc/aiortc/stats.py",
"python-webrtc/python/webrtc/aiortc/rtp.py", "python-webrtc/python/webrtc/aiortc/jitterbuffer.py",
"python-webrtc/python/webrtc/aiortc/utils.py", "python-webrtc/python/webrtc/aiortc/clock.py",
"python-webrtc/python/webrtc/aiortc/_ice_servers.py",
]

[tool.ruff.format]
Expand Down Expand Up @@ -235,6 +238,9 @@ project-excludes = [
"python-webrtc/python/webrtc/aiortc/contrib/media.py", "python-webrtc/python/webrtc/aiortc/contrib/signaling.py",
"python-webrtc/python/webrtc/aiortc/rtcconfiguration.py", "python-webrtc/python/webrtc/aiortc/rtcrtpparameters.py",
"python-webrtc/python/webrtc/aiortc/rtcsessiondescription.py", "python-webrtc/python/webrtc/aiortc/stats.py",
"python-webrtc/python/webrtc/aiortc/rtp.py", "python-webrtc/python/webrtc/aiortc/jitterbuffer.py",
"python-webrtc/python/webrtc/aiortc/utils.py", "python-webrtc/python/webrtc/aiortc/clock.py",
"python-webrtc/python/webrtc/aiortc/_ice_servers.py",
]
# tests/fuzz runs with its directory on PYTHONPATH (.github/scripts/fuzz.sh)
search-path = ["python-webrtc/python", "stubs", ".", "tests/fuzz"]
Expand Down
1 change: 1 addition & 0 deletions python-webrtc/python/webrtc/aiortc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from __future__ import annotations

from webrtc.aiortc._alias import install_alias as install_alias
from webrtc.aiortc.events import RTCTrackEvent as RTCTrackEvent
from webrtc.aiortc.exceptions import InternalError, InvalidAccessError, InvalidStateError, OperationError
from webrtc.aiortc.mediastreams import AudioStreamTrack, MediaStreamError, MediaStreamTrack, VideoStreamTrack
Expand Down
27 changes: 27 additions & 0 deletions python-webrtc/python/webrtc/aiortc/_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# Copyright 2022-2026 Ilya (Marshal) <https://github.com/MarshalX>. All rights reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE.md file in the root of the project.
#

from __future__ import annotations

from webrtc.utils import module_alias


class _Finder(module_alias.ModuleAlias):
source = 'aiortc'
target = 'webrtc.aiortc'


def install_alias() -> None:
"""Makes ``import aiortc.x`` give ``webrtc.aiortc.x``, for libraries that import aiortc themselves.

A module this package lacks raises :obj:`ModuleNotFoundError`. Call it before importing such a library, in each
process. Calling it again does nothing.

Raises:
RuntimeError: If an ``aiortc`` module is imported already.
"""
module_alias.install(_Finder())
124 changes: 124 additions & 0 deletions python-webrtc/python/webrtc/aiortc/_ice_servers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copied from aiortc 1.15.0 (src/aiortc/rtcicetransport.py), its STUN and TURN URL helpers only.
#
# Copyright (c) Jeremy Lainé.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of aiortc nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import re
from typing import Any

from .rtcconfiguration import RTCIceServer

# See https://datatracker.ietf.org/doc/html/rfc7064
# transport is not defined by RFC 7064 and rejected by browsers.
STUN_REGEX = re.compile(
r"(?P<scheme>stun|stuns)\:(?P<host>[^?:]+)(\:(?P<port>[0-9]+?))?"
r"(\?transport=(?P<transport>.*))?"
)
# See https://datatracker.ietf.org/doc/html/rfc7065
TURN_REGEX = re.compile(
r"(?P<scheme>turn|turns)\:(?P<host>[^?:]+)(\:(?P<port>[0-9]+?))?"
r"(\?transport=(?P<transport>.*))?"
)


def connection_kwargs(servers: list[RTCIceServer]) -> dict[str, Any]:
kwargs: dict[str, Any] = {}

for server in servers:
if isinstance(server.urls, list):
uris = server.urls
else:
uris = [server.urls]

for uri in uris:
parsed = parse_stun_turn_uri(uri)

if parsed["scheme"] == "stun":
# only a single STUN server is supported
if "stun_server" in kwargs:
continue

kwargs["stun_server"] = (parsed["host"], parsed["port"])
elif parsed["scheme"] in ["turn", "turns"]:
# only a single TURN server is supported
if "turn_server" in kwargs:
continue

# only 'udp' and 'tcp' transports are supported
if parsed["scheme"] == "turn" and parsed["transport"] not in [
"udp",
"tcp",
]:
continue
elif parsed["scheme"] == "turns" and parsed["transport"] != "tcp":
continue

# only 'password' credentialType is supported
if server.credentialType != "password":
continue

kwargs["turn_server"] = (parsed["host"], parsed["port"])
kwargs["turn_ssl"] = parsed["scheme"] == "turns"
kwargs["turn_transport"] = parsed["transport"]
kwargs["turn_username"] = server.username
kwargs["turn_password"] = server.credential

return kwargs


def parse_stun_turn_uri(uri: str) -> dict[str, Any]:
if uri.startswith("stun"):
match = STUN_REGEX.fullmatch(uri)
elif uri.startswith("turn"):
match = TURN_REGEX.fullmatch(uri)
else:
raise ValueError("malformed uri: invalid scheme")

if not match:
raise ValueError("malformed uri")

# set port
parsed: dict[str, Any] = match.groupdict()
if parsed["port"]:
parsed["port"] = int(parsed["port"])
elif parsed["scheme"] in ["stuns", "turns"]:
parsed["port"] = 5349
else:
parsed["port"] = 3478

# set transport
if parsed["scheme"] == "turn" and not parsed["transport"]:
parsed["transport"] = "udp"
elif parsed["scheme"] == "turns" and not parsed["transport"]:
parsed["transport"] = "tcp"
elif parsed["scheme"] in ["stun", "stuns"]:
if parsed["transport"] is not None:
raise ValueError(
"malformed uri: " + parsed["scheme"] + " must not contain transport"
)
del parsed["transport"]

return parsed
57 changes: 57 additions & 0 deletions python-webrtc/python/webrtc/aiortc/clock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copied from aiortc 1.15.0 (src/aiortc/clock.py), unchanged.
#
# Copyright (c) Jeremy Lainé.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of aiortc nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import datetime

NTP_EPOCH = datetime.datetime(1900, 1, 1, tzinfo=datetime.timezone.utc)


def current_datetime() -> datetime.datetime:
return datetime.datetime.now(datetime.timezone.utc)


def current_ms() -> int:
delta = current_datetime() - NTP_EPOCH
return int(delta.total_seconds() * 1000)


def current_ntp_time() -> int:
return datetime_to_ntp(current_datetime())


def datetime_from_ntp(ntp: int) -> datetime.datetime:
seconds = ntp >> 32
microseconds = ((ntp & 0xFFFFFFFF) * 1000000) / (1 << 32)
return NTP_EPOCH + datetime.timedelta(seconds=seconds, microseconds=microseconds)


def datetime_to_ntp(dt: datetime.datetime) -> int:
delta = dt - NTP_EPOCH
high = int(delta.total_seconds())
low = round((delta.microseconds * (1 << 32)) // 1000000)
return (high << 32) | low
Loading
Loading