Skip to content

Commit 2a60662

Browse files
committed
try libdatachannel
1 parent cddb610 commit 2a60662

6 files changed

Lines changed: 45 additions & 209 deletions

File tree

openpilot/system/webrtc/device/video.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import time
44

55
import av
6-
from teleoprtc.tracks import TiciVideoStreamTrack
7-
from aiortc import MediaStreamError
6+
from teleoprtc.tracks import MediaStreamError, TiciVideoStreamTrack
87

98
from openpilot.cereal import messaging
109
from openpilot.common.realtime import DT_MDL
@@ -55,6 +54,9 @@ def enable(self, enabled: bool):
5554
if not enabled:
5655
self._seen_keyframe = False
5756

57+
def request_keyframe(self) -> None:
58+
self.params.put("LivestreamRequestKeyframe", True, block=False)
59+
5860
def _build_frame_data(self, msg) -> bytes:
5961
encode_data = getattr(msg, msg.which())
6062
if not self.timing_sei_enabled:

openpilot/system/webrtc/tests/test_stream_session.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import asyncio
22
import json
33
import time
4-
# for aiortc and its dependencies
5-
import warnings
6-
warnings.filterwarnings("ignore", category=DeprecationWarning)
7-
warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel
84

9-
from aiortc import RTCDataChannel
10-
from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE
115
import capnp
126
from openpilot.cereal import messaging, log
7+
from teleoprtc.tracks import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE
138

149
from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy
1510
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
@@ -31,7 +26,8 @@ def test_outgoing_proxy(self, mocker):
3126
expected_dict = {"type": "customReservedRawData0", "logMonoTime": 123, "valid": True, "data": "test"}
3227
expected_json = json.dumps(expected_dict).encode()
3328

34-
channel = mocker.Mock(spec=RTCDataChannel)
29+
channel = mocker.Mock()
30+
channel.is_open.return_value = True
3531
proxy = CerealOutgoingMessageProxy(["customReservedRawData0"])
3632
def mocked_update(t):
3733
proxy.sm.update_msgs(0, [test_msg])
@@ -83,4 +79,3 @@ def test_livestream_track(self, mocker):
8379
start_pts = packet.pts
8480
assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms
8581
assert packet.size == 0
86-

openpilot/system/webrtc/webrtcd.py

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,9 @@
1616
from urllib.parse import urlparse, parse_qs
1717
from typing import Any, TYPE_CHECKING
1818

19-
# aiortc and its dependencies have lots of internal warnings :(
20-
import warnings
21-
warnings.filterwarnings("ignore", category=DeprecationWarning)
22-
warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel
23-
2419
import capnp
2520
if TYPE_CHECKING:
26-
from aiortc.rtcdatachannel import RTCDataChannel
27-
import aioice.ice
21+
from teleoprtc.stream import RTCDataChannelAdapter
2822

2923
from openpilot.system.webrtc.helpers import StreamRequestBody
3024
from openpilot.system.webrtc.schema import generate_field
@@ -44,17 +38,6 @@ def _default_route_ip() -> str | None:
4438
finally:
4539
s.close()
4640

47-
# aioice patch: gather ICE candidates only on the default-route interface
48-
_get_host_addresses = aioice.ice.get_host_addresses
49-
def _primary_host_addresses(use_ipv4: bool, use_ipv6: bool) -> list[str]:
50-
addresses = _get_host_addresses(use_ipv4, use_ipv6)
51-
primary = _default_route_ip()
52-
if primary not in addresses:
53-
return addresses
54-
return [primary, ]
55-
aioice.ice.get_host_addresses = _primary_host_addresses
56-
57-
5841
class AsyncTaskRunner:
5942
def __init__(self):
6043
self.is_running = False
@@ -86,10 +69,10 @@ def __init__(self, services: list[str], enabled: bool = True):
8669
super().__init__()
8770
self.services = list(services)
8871
self.sm = messaging.SubMaster(self.services)
89-
self.channels: list[RTCDataChannel] = []
72+
self.channels: list[RTCDataChannelAdapter] = []
9073
self._enabled = enabled
9174

92-
def add_channel(self, channel: 'RTCDataChannel'):
75+
def add_channel(self, channel: 'RTCDataChannelAdapter'):
9376
self.channels.append(channel)
9477

9578
def enable(self, enable: bool):
@@ -118,20 +101,17 @@ def update(self):
118101
outgoing_msg = {"type": service, "logMonoTime": mono_time, "valid": valid, "data": msg_dict}
119102
encoded_msg = json.dumps(outgoing_msg).encode()
120103
for channel in self.channels:
104+
if hasattr(channel, "is_open") and not channel.is_open():
105+
continue
121106
channel.send(encoded_msg)
122107

123108
async def run(self):
124-
from aiortc.exceptions import InvalidStateError
125-
126109
while True:
127110
if not self._enabled:
128111
await asyncio.sleep(0.01)
129112
continue
130113
try:
131114
self.update()
132-
except InvalidStateError:
133-
self.logger.warning("Cereal outgoing proxy invalid state (connection closed)")
134-
break
135115
except Exception:
136116
self.logger.exception("Cereal outgoing proxy failure")
137117
await asyncio.sleep(0.01)
@@ -217,6 +197,8 @@ async def run(self):
217197
self._publish(self.bitrates[self.level])
218198

219199
async def _sample(self) -> float | None:
200+
if not hasattr(self.pc, "getStats"):
201+
return None
220202
report = await self.pc.getStats()
221203
packets_lost = packets_sent = 0
222204
for s in report.values():
@@ -248,17 +230,15 @@ class StreamSession:
248230
shared_pub_master = DynamicPubMaster([])
249231

250232
def __init__(self, body: StreamRequestBody, debug_mode: bool = False):
251-
if debug_mode:
252-
from aiortc.mediastreams import VideoStreamTrack
253233
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
254234
from teleoprtc.builder import WebRTCAnswerBuilder
255235

256236
self.identifier = str(uuid.uuid4())
257237
self.params = Params()
258-
builder = WebRTCAnswerBuilder(body.sdp)
238+
builder = WebRTCAnswerBuilder(body.sdp, bind_address=_default_route_ip())
259239

260240
self.enabled = body.enabled
261-
self.video_track = LiveStreamVideoStreamTrack(body.init_camera, self.enabled) if not debug_mode else VideoStreamTrack()
241+
self.video_track = LiveStreamVideoStreamTrack(body.init_camera, self.enabled)
262242
builder.add_video_stream(body.init_camera, self.video_track)
263243
self.stream = builder.stream()
264244

@@ -305,13 +285,16 @@ def message_handler(self, message: bytes):
305285
case "livestreamCameraSwitch":
306286
self.video_track.switch_camera(payload["data"]["camera"])
307287
case "livestreamSettings":
308-
self.bitrate_controller.set_quality(payload["data"]["quality"])
288+
if self.bitrate_controller is not None:
289+
self.bitrate_controller.set_quality(payload["data"]["quality"])
309290
case "livestreamVideoEnable":
310291
enabled = payload["data"]["enabled"]
311292
self.enabled = enabled
312293
self.video_track.enable(enabled)
313-
self.outgoing_bridge.enable(enabled)
314-
self.bitrate_controller.enable(enabled)
294+
if self.outgoing_bridge is not None:
295+
self.outgoing_bridge.enable(enabled)
296+
if self.bitrate_controller is not None:
297+
self.bitrate_controller.enable(enabled)
315298
if not enabled:
316299
self.params.put("LivestreamRequestKeyframe", True)
317300
case "clockSync":
@@ -325,7 +308,8 @@ def message_handler(self, message: bytes):
325308
case _:
326309
if payload.get("type") not in self.incoming_bridge_services:
327310
return
328-
self.incoming_bridge.send(message)
311+
if self.incoming_bridge is not None:
312+
self.incoming_bridge.send(message)
329313
except Exception:
330314
self.logger.exception("Cereal incoming proxy failure")
331315

@@ -341,7 +325,8 @@ async def run(self):
341325
channel = self.stream.get_messaging_channel()
342326
self.outgoing_bridge.add_channel(channel)
343327
self.outgoing_bridge.start()
344-
self.bitrate_controller.start()
328+
if self.bitrate_controller is not None:
329+
self.bitrate_controller.start()
345330

346331
self.logger.info("Stream session (%s) connected", self.identifier)
347332
await self.stream.wait_for_disconnection()
@@ -357,7 +342,8 @@ async def post_run_cleanup(self):
357342
return
358343
self._cleanup_done = True
359344
self.params.put("LivestreamRequestKeyframe", False)
360-
await self.bitrate_controller.stop()
345+
if self.bitrate_controller is not None:
346+
await self.bitrate_controller.stop()
361347
if self.outgoing_bridge is not None:
362348
await self.outgoing_bridge.stop()
363349
if self.video_track is not None:
@@ -558,9 +544,6 @@ async def _shutdown(server: WebrtcdHTTPServer, state: ServerState, loop: asyncio
558544

559545

560546
def prewarm_stream_session_imports(debug_mode: bool = False) -> None:
561-
if debug_mode:
562-
from aiortc.mediastreams import VideoStreamTrack
563-
assert VideoStreamTrack
564547
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
565548
from teleoprtc.builder import WebRTCAnswerBuilder
566549
assert LiveStreamVideoStreamTrack

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies = [
4646

4747
# body / webrtcd
4848
"av",
49-
"aiortc",
49+
"libdatachannel-py>=2026.1.0.dev2",
5050

5151
# panda
5252
"libusb1",

0 commit comments

Comments
 (0)