diff --git a/.devin/wiki.json b/.devin/wiki.json new file mode 100644 index 0000000..af73f2a --- /dev/null +++ b/.devin/wiki.json @@ -0,0 +1,109 @@ +{ + "repo_notes": [ + { + "content": "Python WebRTC binds libwebrtc M152, the WebRTC engine of Chrome, and exposes it through the W3C browser APIs in asyncio. It is installed with `pip install wrtc` and imported as `webrtc`. Descriptions of it as bindings to WebRTC M92 that aren't ready for use are about its versions from 2023 and are out of date." + }, + { + "content": "The public API follows the specifications: WebRTC, Media Capture and Streams, WebCodecs (VideoFrame, AudioData), WebRTC Encoded Transform with SFrame, WHATWG Streams and WebRTC Statistics. Describe it in the terms of those specifications and of MDN, with snake_case names (`create_offer`). The camelCase originals (`createOffer`) are aliases. Dictionaries are typed dataclasses, enums compare equal to the strings of the specifications, and handlers are registered with `on`." + }, + { + "content": "Layout: `python-webrtc/python/webrtc` is the Python package (`interfaces`, `models`, `enums.py`, `exceptions.py`, `streams.py`, `utils`, and the `aiortc` compatibility layer). `python-webrtc/cpp/src` is the native extension module `wrtc`, written with pybind11. `docs/source` is the Sphinx documentation published at https://wrtc.marshal.dev. `tests` holds the pytest suite, web-platform-tests, the WebIDL comparison and fuzzing. `benchmarks` and `examples` are at the root." + }, + { + "content": "`python-webrtc/cpp/NATIVE.md` is the authoritative description of the native threads, the Dispatcher and mailboxes, ownership and lifetime: native threads never run Python and only post records to the mailbox of an event loop. Benchmark numbers come only from `benchmarks/RESULTS.md`. H.264 isn't compiled into the package: macOS uses VideoToolbox, and elsewhere Cisco's OpenH264 binary is downloaded at runtime on request." + } + ], + "pages": [ + { + "title": "Overview", + "purpose": "What Python WebRTC is, what it covers, the supported Python versions and platforms, and how the repository is organised." + }, + { + "title": "Getting started", + "parent": "Overview", + "purpose": "Installing `wrtc`, connecting two peers with a data channel as in docs/source/quickstart.md, and how the API maps from the browser's JavaScript to Python." + }, + { + "title": "Python API", + "purpose": "The layout of the `webrtc` package: interfaces, models (dictionaries), enums, exceptions and streams, and the conventions they share." + }, + { + "title": "Peer connections and signaling", + "parent": "Python API", + "purpose": "RTCPeerConnection, RTCConfiguration and ICE servers, offers and answers, session descriptions, ICE candidates and the ICE and DTLS transports." + }, + { + "title": "Data channels", + "parent": "Python API", + "purpose": "RTCDataChannel and RTCSctpTransport: text and binary messages, reliability options, negotiated channels and back pressure." + }, + { + "title": "Events", + "parent": "Python API", + "purpose": "The event targets in `webrtc/utils/events.py`: registering handlers with `on` and `once`, coroutine handlers, decorators, and the event objects." + }, + { + "title": "Media tracks and transceivers", + "parent": "Python API", + "purpose": "MediaStream, MediaStreamTrack, the synthetic camera and microphone of MediaDevices, track constraints, transceivers, senders, receivers and RTP parameters." + }, + { + "title": "Media processing", + "parent": "Python API", + "purpose": "Reading and writing frames: MediaStreamTrackProcessor, VideoTrackGenerator and MediaStreamTrackGenerator, VideoFrame and AudioData, and the streams in `webrtc/streams.py`." + }, + { + "title": "Encoded transforms and SFrame", + "parent": "Python API", + "purpose": "RTCRtpScriptTransform and encoded frames, and end-to-end encryption with the SFrame transforms." + }, + { + "title": "Stats", + "parent": "Python API", + "purpose": "`get_stats` and the stats dictionaries of WebRTC Statistics in `webrtc/models/rtc_stats.py`." + }, + { + "title": "Field trials and WARP", + "parent": "Python API", + "purpose": "FieldTrials, which turns on experimental features of the engine, and WARP (DTLS 1.3, SNAP and SPED) for faster connection setup." + }, + { + "title": "Codecs and H.264", + "purpose": "The supported codecs, the video codec factories in `cpp/src/codecs`, VideoToolbox on macOS, and the OpenH264 binary downloaded at runtime by `webrtc/openh264.py`." + }, + { + "title": "aiortc compatibility layer", + "purpose": "`webrtc.aiortc`, which runs code written for aiortc on this engine, and the differences documented in docs/source/guides/aiortc.md." + }, + { + "title": "Native extension", + "purpose": "The pybind11 module in `python-webrtc/cpp/src`: its modules (interfaces, media, codecs, models, utils) and how Python wrappers map to native objects." + }, + { + "title": "Threads, the Dispatcher and mailboxes", + "parent": "Native extension", + "purpose": "The threading model of `python-webrtc/cpp/NATIVE.md`: libwebrtc's threads, mailboxes of event loops, the Dispatcher, locking rules and free-threaded Python." + }, + { + "title": "Ownership and lifetime", + "parent": "Native extension", + "purpose": "Who owns native objects and when they are released, as described in the Ownership and Lifetime sections of `python-webrtc/cpp/NATIVE.md` and in `webrtc/utils/lifetime.py`." + }, + { + "title": "Build and release", + "purpose": "Building from sources with scikit-build-core and CMake, the prebuilt libwebrtc downloaded by `cmake/libwebrtc.cmake`, wheels built with cibuildwheel, and the steps in CONTRIBUTING.md." + }, + { + "title": "Testing", + "purpose": "The pytest suite in `tests`, the web-platform-tests in `tests/wpt`, the WebIDL comparison in `tests/idl`, fuzzing in `tests/fuzz`, and the sanitizer and clang-tidy scripts in `.github/scripts`." + }, + { + "title": "Benchmarks", + "purpose": "The benchmark suites and drivers in `benchmarks`, their method and caveats from benchmarks/README.md, and the results in benchmarks/RESULTS.md." + }, + { + "title": "Examples", + "purpose": "The runnable programs in `examples`: the echo peer, the recorder, the Janus live stream in the terminal and the voice chat with OpenAI." + } + ] +} diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..44c091c --- /dev/null +++ b/context7.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "Python WebRTC", + "description": "Chrome's WebRTC engine (libwebrtc M152) with the W3C browser API, in Python and asyncio: peer connections, data channels, tracks, WebCodecs frames, encoded transforms and stats.", + "folders": ["docs/source", "examples"], + "excludeFolders": ["docs/source/_static"], + "excludeFiles": ["LICENSE.md", "THIRD_PARTY_LICENSES.md", "llms-txt.md"], + "rules": [ + "Install with `pip install wrtc` and import as `webrtc`.", + "The API is the browser's WebRTC API with snake_case names: `pc.create_offer()`, `pc.set_local_description(offer)`, `pc.create_data_channel('chat')`.", + "Methods that return a promise in the browser are coroutines: await them inside a running asyncio event loop.", + "Register handlers with `target.on('event', handler)` or the `@target.on('event')` decorator; handlers can be coroutines.", + "Dictionaries are dataclasses: `webrtc.RTCConfiguration(ice_servers=[webrtc.RTCIceServer(urls='stun:stun.l.google.com:19302')])`.", + "Close connections with `pc.close()` when done.", + "To run code written for aiortc, install `wrtc[aiortc]` and change `from aiortc` to `from webrtc.aiortc`." + ] +} diff --git a/docs/source/conf.py b/docs/source/conf.py index c1c5a50..0ef27a8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,6 +17,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from docutils import nodes from pygments.formatters import HtmlFormatter from sphinx.ext.autodoc.mock import mock from sphinxawesome_theme.postprocess import Icons @@ -54,6 +55,7 @@ 'sphinx_sitemap', 'sphinx_favicon', 'myst_parser', + 'sphinx_llm.txt', ] DOCSEARCH_APP_ID = os.environ.get('DOCSEARCH_APP_ID') @@ -198,6 +200,14 @@ # -- Sitemap --------------------------------------------------- sitemap_locales = [None] sitemap_url_scheme = '{link}' +sitemap_excludes = ['llms-txt/'] + +# -- llms.txt --------------------------------------------------- +# a Markdown copy of every page, e.g. /quickstart.md, for agents +llms_txt_override_source = 'llms-txt' +llms_txt_nested_enabled = False +llms_txt_full_build = True +LLMS_MARKDOWN_BUILDER = 'llms-markdown' # -- Favicons --------------------------------------------------- favicons = [ @@ -374,8 +384,48 @@ def scope_pygments_to_theme(app: Sphinx, exception: Exception | None) -> None: (Path(app.outdir) / '_static' / 'pygments.css').write_text('\n'.join(blocks), encoding='UTF-8') +def _design_components(root: nodes.Element, component: str) -> list[nodes.container]: + return [node for node in root.findall(nodes.container) if node.get('design_component') == component] + + +def _flat_card(card: nodes.container) -> list[nodes.Node]: + """A card as its title, linked to the card's target, and its text.""" + title = next(iter(_design_components(card, 'card-title')), None) + links = [node for node in card.findall(nodes.reference) if 'sd-stretched-link' in node['classes']] + + label: nodes.Element = nodes.strong(text=title.astext().strip() if title else '') + if links: + target = {key: links[0][key] for key in ('refuri', 'refid', 'internal') if key in links[0]} + label = nodes.reference('', '', label, **target) + + body = next(iter(_design_components(card, 'card-body')), None) + text = [child.deepcopy() for child in body.children if child is not title] if body else [] + return [nodes.paragraph('', '', label), *text] + + +def simplify_for_markdown(app: Sphinx, doctree: nodes.document, _docname: str) -> None: + """Rewrite the nodes the Markdown builder of llms.txt drops or can't render. + + Cards become a linked title and their text, captions of code blocks a line above them, and the raw HTML of icons, + the abbreviations of signatures and the meta tags are dropped. + """ + if app.builder.name != LLMS_MARKDOWN_BUILDER: + return + + for node in [*doctree.findall(nodes.raw), *doctree.findall(nodes.meta)]: + if node.parent is not None: + node.parent.remove(node) + for abbreviation in list(doctree.findall(nodes.abbreviation)): + abbreviation.replace_self(nodes.Text(abbreviation.astext())) + for caption in list(doctree.findall(nodes.caption)): + caption.replace_self(nodes.paragraph('', '', nodes.literal(text=caption.astext()))) + for card in _design_components(doctree, 'card'): + card.replace_self(_flat_card(card)) + + def setup(app: Sphinx) -> None: """Connects the handlers of the build events.""" + app.connect('doctree-resolved', simplify_for_markdown) app.connect('builder-inited', disable_search_index) app.connect('autodoc-skip-member', skip_member) # after Napoleon, which connects at the default priority diff --git a/docs/source/llms-txt.md b/docs/source/llms-txt.md new file mode 100644 index 0000000..ae0991b --- /dev/null +++ b/docs/source/llms-txt.md @@ -0,0 +1,66 @@ +--- +orphan: true +nosearch: true +--- + +# Python WebRTC + +> Chrome's WebRTC engine (libwebrtc M152) with the W3C browser API, in Python and asyncio. Install it with +> `pip install wrtc` and import it as `webrtc`. + +Pages describing it as bindings to WebRTC M92 that aren't ready for use are about its versions from 2023 and are out +of date. + +- The API is the one of the browsers: `RTCPeerConnection`, `RTCDataChannel`, tracks, `MediaStreamTrackProcessor`, + `VideoTrackGenerator`, encoded transforms and stats. Names are snake_case (`create_offer`), and the camelCase + originals (`createOffer`) work too. +- Handlers are registered with `on`, as a call or a decorator, and can be coroutines. +- Dictionaries are typed dataclasses, and enums compare equal to the strings of the specifications. +- Code written for aiortc runs after installing `wrtc[aiortc]` and changing `from aiortc` to `from webrtc.aiortc`. +- Prebuilt wheels for CPython 3.9 to 3.15 and free-threaded 3.14t and 3.15t, on Linux, macOS and Windows. + +## Start + +- {doc}`Quickstart `: installation, and two peers connected with a data channel. +- {doc}`Migrating from aiortc `: run aiortc code by changing its imports, and what changes on the way. + +## Guides + +- {doc}`Connections `: configuration, STUN and TURN servers, signalling, states and stats. +- {doc}`Data channels `: text and binary messages, reliability, negotiated channels, back + pressure. +- {doc}`Events `: handlers as functions or coroutines, decorators, one-shot handlers, removing them. +- {doc}`Media `: reading, writing and transforming the frames of audio and video tracks. +- {doc}`H.264 `: VideoToolbox on macOS, Cisco's OpenH264 binary elsewhere, and its licensing. +- {doc}`Field trials `: experimental features of the engine, and WARP for faster connections. + +## API reference + +- {doc}`API reference `: every interface, dictionary, enum and exception. +- {doc}`RTCPeerConnection `: the connection to a remote peer. +- {doc}`RTCDataChannel `: text and binary messages over SCTP. +- {doc}`RTCConfiguration `: the configuration of a connection and its ICE servers. +- {doc}`RTCSessionDescriptionInit `: offers and answers as plain data, to + exchange over signalling. +- {doc}`MediaStreamTrack `: tracks and the synthetic camera and microphone. +- {doc}`MediaStreamTrackProcessor `: reads the frames of a track. +- {doc}`Track generators `: tracks of frames the application writes. +- {doc}`VideoFrame ` and {doc}`AudioData `: the frames of WebCodecs. +- {doc}`Event objects `: the events handlers are called with. +- {doc}`Stats `: the dictionaries of WebRTC Statistics. +- {doc}`Enums ` and {doc}`Exceptions `. + +## Examples + +- {doc}`Echo peer `: sends back the video it receives, in grayscale. +- {doc}`Recorder `: writes the video and audio a peer receives to files. +- {doc}`Live stream in the terminal `: plays a stream of the public Janus demo server. +- {doc}`Voice chat with OpenAI GPT-Live `: talks to the model through the microphone and + speakers. + +## Optional + +- [llms-full.txt](https://wrtc.marshal.dev/llms-full.txt): the whole documentation in one file. +- {doc}`Benchmarks `: connection setup, data channels, memory and media against other libraries. +- {doc}`Change Log `: what changed in each release. +- {doc}`Security Policy `: supported versions and reporting vulnerabilities. diff --git a/pyproject.toml b/pyproject.toml index b2a3166..80a9cac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ docs = [ "sphinx-docsearch==0.3.1; python_version >= '3.12'", "sphinx-favicon==1.1.0; python_version >= '3.12'", "sphinxext-opengraph==0.13.0; python_version >= '3.12'", + "sphinx-llm==1.1.0; python_version >= '3.12'", ] [tool.scikit-build] @@ -94,7 +95,7 @@ minimum-version = "build-system.requires" cmake.version = ">=3.26" build-dir = "build/{wheel_tag}" wheel.packages = ["python-webrtc/python/webrtc"] -sdist.exclude = [".github", ".idea", "benchmarks", "docs", "examples", "wpt"] +sdist.exclude = [".github", ".idea", "benchmarks", "docs", "examples", "wpt", ".devin", "context7.json"] [tool.cibuildwheel] # free-threaded from 3.14, where it's no longer experimental diff --git a/uv.lock b/uv.lock index d39b0c4..821527d 100644 --- a/uv.lock +++ b/uv.lock @@ -4315,6 +4315,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/fb/e496f16fa11fbe2dbdd0b5e306ede153dfed050aae4766fc89d500720dc7/sphinx_last_updated_by_git-0.3.8-py3-none-any.whl", hash = "sha256:6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499", size = 8580, upload-time = "2024-08-11T07:15:53.244Z" }, ] +[[package]] +name = "sphinx-llm" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "sphinx" }, + { name = "sphinx-markdown-builder" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/2b/2529cb0b1acb85489167904649387ea7b17e3172ac3286fc0c8af2887bc5/sphinx_llm-1.1.0.tar.gz", hash = "sha256:5f3b41c0d7888c75a6fe9f92b1a965c50ee233eaf38673c0ef3501072599c617", size = 408770, upload-time = "2026-09-21T10:15:26.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/06/8db70029d13ae3b5ef4dabd7496e15941f0f700b9e2f1fb77859bef97263/sphinx_llm-1.1.0-py3-none-any.whl", hash = "sha256:40cfadbfeb4483f4fd5b6c1c0b1a48152847f30febfa94fc413bbbe09c137953", size = 85878, upload-time = "2026-09-21T10:15:25.627Z" }, +] + +[[package]] +name = "sphinx-markdown-builder" +version = "0.6.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/13/102d7134743120db25e636ffb57a30818e95a416583dca83aadac9c5dd87/sphinx_markdown_builder-0.6.11.tar.gz", hash = "sha256:ba3bf33e0e42ff30ae686ac73957a95e6d045790dcfbaca012dd8ae01794e577", size = 24020, upload-time = "2026-08-16T06:51:38.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/71/06b415631c2e494c80546073b46013d1715dceb3abe30bd63b34b4e7d9ca/sphinx_markdown_builder-0.6.11-py3-none-any.whl", hash = "sha256:acfb94d6ee66c7a47da0ca0420578fa16f115812af25b8fd21755d5909aacf51", size = 18088, upload-time = "2026-08-16T06:51:36.81Z" }, +] + [[package]] name = "sphinx-sitemap" version = "2.9.0" @@ -4406,6 +4434,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/a4/66c1fd4f8fab88faf71cee04a945f9806ba0fef753f2cfc8be6353f64508/sphinxext_opengraph-0.13.0-py3-none-any.whl", hash = "sha256:936c07828edc9ad9a7b07908b29596dc84ed0b3ceaa77acdf51282d232d4d80e", size = 1004152, upload-time = "2025-08-29T12:20:29.072Z" }, ] +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -4574,6 +4611,7 @@ docs = [ { name = "sphinx-design", marker = "python_full_version >= '3.12'" }, { name = "sphinx-docsearch", marker = "python_full_version >= '3.12'" }, { name = "sphinx-favicon", marker = "python_full_version >= '3.12'" }, + { name = "sphinx-llm", marker = "python_full_version >= '3.12'" }, { name = "sphinx-sitemap", marker = "python_full_version >= '3.12'" }, { name = "sphinxawesome-theme", marker = "python_full_version >= '3.12'" }, { name = "sphinxext-opengraph", marker = "python_full_version >= '3.12'" }, @@ -4629,6 +4667,7 @@ docs = [ { name = "sphinx-design", marker = "python_full_version >= '3.12'", specifier = "==0.7.0" }, { name = "sphinx-docsearch", marker = "python_full_version >= '3.12'", specifier = "==0.3.1" }, { name = "sphinx-favicon", marker = "python_full_version >= '3.12'", specifier = "==1.1.0" }, + { name = "sphinx-llm", marker = "python_full_version >= '3.12'", specifier = "==1.1.0" }, { name = "sphinx-sitemap", marker = "python_full_version >= '3.12'", specifier = "==2.9.0" }, { name = "sphinxawesome-theme", marker = "python_full_version >= '3.12'", specifier = "==6.0.5" }, { name = "sphinxext-opengraph", marker = "python_full_version >= '3.12'", specifier = "==0.13.0" },