IQ.Pilot Prebuilt Release @ 27f668a

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-03 18:23:24 -05:00
commit b073c5182b
2554 changed files with 679696 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
from types import SimpleNamespace
import pytest
from iqpilot.system.webrtc import session as session_module
class FakeStream:
def get_receiver_report_stats(self):
return {}
class FakeBuilder:
instance = None
def __init__(self, sdp, bind_address=None, ice_servers=None):
self.sdp = sdp
self.bind_address = bind_address
self.ice_servers = ice_servers
self.video = []
self.audio = []
self.receive_audio = False
self.result = FakeStream()
FakeBuilder.instance = self
def add_video_stream(self, camera, track):
self.video.append((camera, track))
def add_audio_stream(self, track):
self.audio.append(track)
def offer_to_receive_audio_stream(self):
self.receive_audio = True
def stream(self):
return self.result
class FakeVideoTrack:
def __init__(self, camera):
self.camera = camera
self.timing_sei_enabled = False
self.switched = None
def switch_camera(self, camera):
self.switched = camera
class FakeAudioProducer:
def __init__(self):
self.enabled = True
def enable(self, enabled):
self.enabled = enabled
@pytest.fixture
def native_session(mocker):
config = SimpleNamespace(
n_expected_camera_tracks=1,
expected_audio_track=True,
incoming_audio_track=True,
incoming_datachannel=True,
)
mocker.patch("iqpilot.system.webrtc.rtc.info.parse_info_from_offer", return_value=config)
mocker.patch("iqpilot.system.webrtc.rtc.builder.WebRTCAnswerBuilder", FakeBuilder)
mocker.patch("iqpilot.system.webrtc.device.native_video.LiveStreamVideoStreamTrack", FakeVideoTrack)
mocker.patch("iqpilot.system.webrtc.device.native_audio.AudioInputOpusProducer", FakeAudioProducer)
mocker.patch.object(session_module, "_default_route_ip", return_value="192.0.2.1")
mocker.patch.object(session_module, "Params", return_value=mocker.Mock())
return session_module.StreamSession(
"offer", ["road"], [], [], [{"urls": "turn:example.com"}], ui_stream=False,
)
def test_native_session_builds_duplex_audio(native_session):
builder = FakeBuilder.instance
assert builder is not None
assert builder.bind_address == "192.0.2.1"
assert builder.ice_servers == [{"urls": "turn:example.com"}]
assert [camera for camera, _ in builder.video] == ["road"]
assert builder.audio == [native_session.audio_output]
assert builder.receive_audio
assert native_session.audio_recv_requested
def test_native_session_controls(native_session, mocker):
native_session.bitrate_controller = mocker.Mock()
native_session.message_handler('{"type":"timingSei","enabled":true}')
assert native_session.video_tracks[0].timing_sei_enabled
native_session.message_handler('{"type":"switchCamera","camera":"driver"}')
assert native_session.video_tracks[0].switched == "driver"
native_session.message_handler('{"type":"setAudioEnabled","enabled":false}')
assert not native_session.audio_output.enabled
native_session.message_handler('{"type":"setQuality","quality":"low"}')
native_session.bitrate_controller.set_quality.assert_called_once_with("low")

View File

@@ -0,0 +1,116 @@
import pytest
from iqpilot.system.webrtc.rtc.info import parse_info_from_offer
from iqpilot.system.webrtc.rtc.stream import WebRTCBaseStream
from iqpilot.system.webrtc.rtc.tracks import TiciTrackWrapper, TiciVideoStreamTrack, parse_video_track_id, video_track_id
def sdp_with_media(media):
mids = " ".join(str(i) for i in range(len(media)))
sections = []
for index, (kind, direction) in enumerate(media):
if kind == "video":
payload, mapping = "96", "H264/90000"
protocol = "UDP/TLS/RTP/SAVPF"
elif kind == "audio":
payload, mapping = "111", "opus/48000/2"
protocol = "UDP/TLS/RTP/SAVPF"
else:
sections.extend([
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel",
"c=IN IP4 0.0.0.0",
f"a=mid:{index}",
"a=sctp-port:5000",
])
continue
sections.extend([
f"m={kind} 9 {protocol} {payload}",
"c=IN IP4 0.0.0.0",
f"a=mid:{index}",
f"a={direction}",
f"a=rtpmap:{payload} {mapping}",
"a=rtcp-mux",
])
lines = [
"v=0",
"o=- 1 1 IN IP4 0.0.0.0",
"s=-",
"t=0 0",
f"a=group:BUNDLE {mids}",
*sections,
]
return "\r\n".join(lines) + "\r\n"
@pytest.mark.parametrize("direction,expected_outgoing,expected_incoming", [
("recvonly", True, False),
("sendonly", False, True),
("sendrecv", True, True),
("inactive", False, False),
])
def test_audio_directions(direction, expected_outgoing, expected_incoming):
info = parse_info_from_offer(sdp_with_media([("audio", direction)]))
assert info.expected_audio_track == expected_outgoing
assert info.incoming_audio_track == expected_incoming
def test_video_and_data_channel_metadata():
info = parse_info_from_offer(sdp_with_media([
("video", "recvonly"),
("video", "sendrecv"),
("application", "sendrecv"),
]))
assert info.n_expected_camera_tracks == 2
assert info.incoming_datachannel
def test_explicit_empty_ice_servers_disable_defaults():
assert WebRTCBaseStream._make_ice_servers([]) == []
def test_default_ice_server():
servers = WebRTCBaseStream._make_ice_servers(None)
assert len(servers) == 1
assert servers[0].hostname == "stun.l.google.com"
assert servers[0].port == 19302
def test_authenticated_ice_servers():
servers = WebRTCBaseStream._make_ice_servers([{
"urls": ["turn:relay.example.com:3478", "stun:stun.example.com:3478"],
"username": "user",
"credential": "secret",
}])
assert [(server.hostname, server.port) for server in servers] == [
("relay.example.com", 3478),
("stun.example.com", 3478),
]
assert all(server.username == "user" and server.password == "secret" for server in servers)
def test_track_id_roundtrip():
assert parse_video_track_id(video_track_id("driver", "track")) == ("driver", "track")
def test_invalid_track_id():
with pytest.raises(ValueError):
parse_video_track_id("driver")
def test_track_wrapper_preserves_camera():
class Track:
kind = "video"
id = "source"
async def recv(self):
return b"frame"
wrapper = TiciTrackWrapper("road", Track())
assert parse_video_track_id(wrapper.id)[0] == "road"
wrapper.stop()
assert wrapper.readyState == "ended"
def test_track_stores_frame_period():
track = TiciVideoStreamTrack("wideRoad", 0.05)
assert track._dt == 0.05

View File

@@ -0,0 +1,95 @@
import asyncio
import os
import pytest
from iqpilot.system.webrtc.rtc import WebRTCOfferBuilder
from iqpilot.system.webrtc.rtc.stream import RTCSessionDescription
from iqpilot.system.webrtc.session import StreamSession
@pytest.mark.asyncio
async def test_native_video_audio_and_data_channel():
if not os.environ.get("CI"):
return
answer_session = None
video_received = asyncio.Event()
audio_received = asyncio.Event()
async def connect(offer):
nonlocal answer_session
browser_offer = offer.sdp.replace("profile-level-id=42e01f", "profile-level-id=640c1f", 1)
audio_offset = browser_offer.index("m=audio")
browser_offer = browser_offer[:audio_offset] + browser_offer[audio_offset:].replace(
"a=recvonly",
"a=sendrecv\r\na=msid:ios-microphone ios-audio-track\r\na=ssrc:123456 cname:ios-audio\r\na=ssrc:123456 msid:ios-microphone ios-audio-track",
1,
)
answer_session = StreamSession(browser_offer, offer.video, [], [], [], debug_mode=True)
answer = await answer_session.get_answer()
assert not any(line.startswith("m=video 0 ") for line in answer.sdp.splitlines())
assert answer_session.stream._track_state[0][0] is answer_session.stream._negotiated_tracks["road"]
answer_session.start()
return RTCSessionDescription(answer.sdp, answer.type)
builder = WebRTCOfferBuilder(connect, ice_servers=[])
builder.offer_to_receive_video_stream("road")
builder.offer_to_receive_audio_stream()
builder.add_messaging()
stream = builder.stream()
try:
await asyncio.wait_for(stream.start(), 10)
stream.get_incoming_video_track("road").on_frame(lambda *_: video_received.set())
stream.get_incoming_audio_track().on_frame(lambda *_: audio_received.set())
await asyncio.wait_for(stream.wait_for_connection(), 10)
await asyncio.wait_for(video_received.wait(), 10)
await asyncio.wait_for(audio_received.wait(), 10)
stream.get_messaging_channel().send('{"type":"timingSei","enabled":true}')
await asyncio.sleep(0.1)
assert answer_session is not None
assert answer_session.video_tracks[0].timing_sei_enabled
finally:
await stream.stop()
if answer_session is not None:
await answer_session.stop_async()
@pytest.mark.asyncio
async def test_native_duplex_audio_negotiation():
if not os.environ.get("CI"):
return
answer_session = None
async def connect(offer):
nonlocal answer_session
sendrecv_offer = offer.sdp.replace(
"a=recvonly",
"a=sendrecv\r\na=msid:ios-microphone ios-audio-track\r\na=ssrc:123456 cname:ios-audio\r\na=ssrc:123456 msid:ios-microphone ios-audio-track",
1,
)
answer_session = StreamSession(sendrecv_offer, offer.video, [], [], [], debug_mode=True)
answer = await answer_session.get_answer()
assert "ios-microphone" not in answer.sdp
assert "ios-audio-track" not in answer.sdp
assert answer.sdp.count("a=msid:audio audio") == 1
answer_session.start()
return RTCSessionDescription(answer.sdp, answer.type)
builder = WebRTCOfferBuilder(connect, ice_servers=[])
builder.offer_to_receive_audio_stream()
stream = builder.stream()
try:
await asyncio.wait_for(stream.start(), 10)
await asyncio.wait_for(stream.wait_for_connection(), 10)
assert stream.has_incoming_audio_track()
assert answer_session is not None
assert answer_session.audio_recv_requested
assert answer_session.audio_output is not None
finally:
await stream.stop()
if answer_session is not None:
await answer_session.stop_async()

View File

@@ -0,0 +1,110 @@
import asyncio
import gc
import json
import capnp
from iqpilot.cereal import messaging, log
from iqpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy
from iqpilot.system.webrtc.device.native_audio import AudioInputOpusProducer, DebugAudioOpusProducer
from iqpilot.system.webrtc.device.native_video import DebugVideoStreamTrack, LiveStreamVideoStreamTrack
from iqpilot.system.webrtc.rtc.tracks import VIDEO_TIME_BASE
class TestStreamSession:
def setup_method(self):
self.loop = asyncio.new_event_loop()
def teardown_method(self):
self.loop.stop()
self.loop.close()
gc.collect()
def test_outgoing_proxy(self, mocker):
test_msg = log.Event.new_message()
test_msg.logMonoTime = 123
test_msg.valid = True
test_msg.customReservedRawData0 = b"test"
expected_dict = {"type": "customReservedRawData0", "logMonoTime": 123, "valid": True, "data": "test"}
expected_json = json.dumps(expected_dict).encode()
channel = mocker.Mock()
mocked_submaster = mocker.MagicMock()
mocked_submaster.updated = {"customReservedRawData0": True}
mocked_submaster.logMonoTime = {"customReservedRawData0": 123}
mocked_submaster.valid = {"customReservedRawData0": True}
mocked_submaster.__getitem__.return_value = test_msg.customReservedRawData0
proxy = CerealOutgoingMessageProxy(mocked_submaster)
proxy.add_channel(channel)
proxy.update()
channel.send.assert_called_once_with(expected_json)
def test_incoming_proxy(self, mocker):
tested_msgs = [
{"type": "customReservedRawData0", "data": "test"},
{"type": "can", "data": [{"address": 0, "dat": "", "src": 0}]},
{"type": "testJoystick", "data": {"axes": [0, 0], "buttons": [False]}},
]
mocked_pubmaster = mocker.MagicMock(spec=messaging.PubMaster)
proxy = CerealIncomingMessageProxy(mocked_pubmaster)
for msg in tested_msgs:
proxy.send(json.dumps(msg).encode())
mocked_pubmaster.send.assert_called_once()
mt, md = mocked_pubmaster.send.call_args.args
assert mt == msg["type"]
assert isinstance(md, capnp._DynamicStructBuilder)
assert hasattr(md, msg["type"])
mocked_pubmaster.reset_mock()
def test_livestream_track(self, mocker):
fake_msg = messaging.new_message("livestreamDriverEncodeData")
fake_msg.livestreamDriverEncodeData.header = b"header"
fake_msg.livestreamDriverEncodeData.data = b"\x00\x00\x00\x01\x65"
mocker.patch("iqpilot.system.webrtc.device.native_video.messaging.sub_sock", return_value=mocker.Mock())
mocker.patch("iqpilot.system.webrtc.device.native_video.messaging.recv_one_or_none", return_value=fake_msg)
track = LiveStreamVideoStreamTrack("driver")
assert track.id.startswith("driver")
packet = self.loop.run_until_complete(track.recv())
assert packet.time_base == VIDEO_TIME_BASE
assert packet.pts is not None
assert packet.size == len(b"header\x00\x00\x00\x01\x65")
def test_input_audio_track(self, mocker):
packet_time, rate = 0.02, 16000
sample_count = int(packet_time * rate)
fake_msg = messaging.new_message("rawAudioData")
fake_msg.rawAudioData.data = b"\x00" * 2 * sample_count
fake_msg.rawAudioData.sampleRate = rate
mocker.patch("iqpilot.system.webrtc.device.native_audio.messaging.sub_sock", return_value=mocker.Mock())
track = AudioInputOpusProducer()
track._source_rate = rate
mocker.patch("iqpilot.system.webrtc.device.native_audio.messaging.recv_one_or_none", return_value=fake_msg)
packet = self.loop.run_until_complete(track.recv())
assert packet is not None
payload, pts = packet
assert payload
assert pts >= 0
def test_debug_video_track(self):
track = DebugVideoStreamTrack("road")
packet = self.loop.run_until_complete(track.recv())
assert packet.size > 0
assert packet.pts == 0
def test_debug_audio_track(self):
track = DebugAudioOpusProducer()
packet = self.loop.run_until_complete(track.recv())
assert packet is not None
payload, pts = packet
assert payload
assert pts == 0

View File

@@ -0,0 +1,303 @@
import asyncio
import json
import math
import pytest
from iqpilot.cereal import log, messaging
from iqpilot.system.webrtc.ui_stream import (
UI_STREAM_SERVICES,
UIStreamMessageProxy,
compute_ui_status,
frame_to_str,
MAX_BUFFERED_BYTES,
)
OpenpilotState = log.SelfdriveState.OpenpilotState
def make_readers(**overrides):
readers = {}
for service in UI_STREAM_SERVICES:
if service == "onroadEvents":
msg = messaging.new_message(service, 0)
else:
msg = messaging.new_message(service)
readers[service] = msg
readers.update(overrides)
return {s: getattr(m, s) for s, m in readers.items()}
class FakeSubMaster:
def __init__(self, readers, updated=None, valid=None):
self.readers = readers
self.updated = updated or dict.fromkeys(UI_STREAM_SERVICES, True)
self.valid = valid or dict.fromkeys(UI_STREAM_SERVICES, True)
self.logMonoTime = dict.fromkeys(UI_STREAM_SERVICES, 42)
self.update_calls = 0
def __getitem__(self, service):
return self.readers[service]
def update(self, timeout):
self.update_calls += 1
class FakeChannel:
def __init__(self, buffered_amount=0):
self.bufferedAmount = buffered_amount
self.sent = []
def send(self, data):
self.sent.append(data)
def make_proxy(sm, **kwargs):
return UIStreamMessageProxy(sm=sm, **kwargs)
class TestComputeUiStatus:
def _msgs(self):
ss = messaging.new_message("selfdriveState")
iq = messaging.new_message("iqState")
ev = messaging.new_message("onroadEvents", 0)
return ss.selfdriveState, iq.iqState, ev.onroadEvents
def test_disengaged(self):
ss, iq, ev = self._msgs()
assert compute_ui_status(ss, iq, ev) == "disengaged"
def test_engaged_no_guidance(self):
ss, iq, ev = self._msgs()
ss.enabled = True
assert compute_ui_status(ss, iq, ev) == "engaged"
def test_pre_enabled_is_override(self):
ss, iq, ev = self._msgs()
ss.state = OpenpilotState.preEnabled
assert compute_ui_status(ss, iq, ev) == "override"
def test_lat_only(self):
ss, iq, ev = self._msgs()
iq.aol.available = True
iq.aol.enabled = True
assert compute_ui_status(ss, iq, ev) == "lat_only"
def test_long_only(self):
ss, iq, ev = self._msgs()
ss.enabled = True
iq.aol.available = True
assert compute_ui_status(ss, iq, ev) == "long_only"
def test_both_engaged(self):
ss, iq, ev = self._msgs()
ss.enabled = True
iq.aol.available = True
iq.aol.enabled = True
assert compute_ui_status(ss, iq, ev) == "engaged"
class TestUIStreamFrame:
def test_frame_shape_and_json(self):
model_msg = messaging.new_message("modelV2")
model = model_msg.modelV2
model.position.x = [float(i) for i in range(33)]
model.position.y = [0.123456] * 33
model.position.z = [0.0] * 33
model.init("laneLines", 4)
for lane in model.laneLines:
lane.x = [1.0, 2.0]
lane.y = [0.1, 0.2]
lane.z = [0.0, 0.0]
model.laneLineProbs = [0.9, 0.8, 0.7, 0.6]
model.init("roadEdges", 2)
for edge in model.roadEdges:
edge.x = [1.0]
edge.y = [2.0]
edge.z = [0.0]
model.roadEdgeStds = [0.1, 0.2]
model.acceleration.x = [0.5] * 33
cs_msg = messaging.new_message("carState")
cs_msg.carState.vEgo = 12.345
cs_msg.carState.leftBlinker = True
readers = make_readers(modelV2=model_msg, carState=cs_msg)
sm = FakeSubMaster(readers)
proxy = make_proxy(sm)
channel = FakeChannel()
proxy.add_channel(channel)
proxy.update()
assert len(channel.sent) == 1
frame = json.loads(channel.sent[0])
assert frame["type"] == "uiStream"
data = frame["data"]
assert len(data["modelV2"]["position"]["x"]) == 33
assert data["modelV2"]["position"]["y"][0] == 0.12
assert len(data["modelV2"]["laneLines"]) == 4
assert data["carState"]["vEgo"] == 12.35
assert data["carState"]["leftBlinker"] is True
assert data["uiStatus"] == "disengaged"
assert data["selfdriveState"]["alertSize"] == "none"
assert "hasLongitudinalControl" in data["init"]
assert "cameraOffset" in data["init"]
assert "isMetric" in data["init"]
def test_nan_scrubbed(self):
model_msg = messaging.new_message("modelV2")
model_msg.modelV2.position.x = [math.nan, math.inf, 1.0]
readers = make_readers(modelV2=model_msg)
sm = FakeSubMaster(readers)
proxy = make_proxy(sm)
channel = FakeChannel()
proxy.add_channel(channel)
proxy.update()
raw = channel.sent[0]
assert "NaN" not in raw and "Infinity" not in raw
frame = json.loads(raw)
assert frame["data"]["modelV2"]["position"]["x"] == [0.0, 0.0, 1.0]
def test_backpressure_drops_frames(self):
readers = make_readers()
sm = FakeSubMaster(readers)
proxy = make_proxy(sm)
channel = FakeChannel(buffered_amount=MAX_BUFFERED_BYTES + 1)
proxy.add_channel(channel)
proxy.update()
assert channel.sent == []
assert proxy.dropped_frames == 1
def test_no_send_without_model_update(self):
readers = make_readers()
updated = dict.fromkeys(UI_STREAM_SERVICES, False)
sm = FakeSubMaster(readers, updated=updated)
proxy = make_proxy(sm)
proxy._last_emit_time = float("inf")
channel = FakeChannel()
proxy.add_channel(channel)
proxy.update()
assert channel.sent == []
assert sm.update_calls == 1
def test_heartbeat_without_model_update(self):
readers = make_readers()
updated = dict.fromkeys(UI_STREAM_SERVICES, False)
sm = FakeSubMaster(readers, updated=updated)
proxy = make_proxy(sm)
channel = FakeChannel()
proxy.add_channel(channel)
proxy.update()
assert len(channel.sent) == 1
frame = json.loads(channel.sent[0])
assert frame["data"]["modelV2"] is None
def test_low_bandwidth_decimation(self):
readers = make_readers()
sm = FakeSubMaster(readers)
proxy = make_proxy(sm, bitrate_getter=lambda: 500_000)
channel = FakeChannel()
proxy.add_channel(channel)
for _ in range(4):
proxy.update()
assert len(channel.sent) == 2
def test_full_rate_at_high_bitrate(self):
readers = make_readers()
sm = FakeSubMaster(readers)
proxy = make_proxy(sm, bitrate_getter=lambda: 5_000_000)
channel = FakeChannel()
proxy.add_channel(channel)
for _ in range(4):
proxy.update()
assert len(channel.sent) == 4
def test_sticky_status_when_engaged_like(self):
ss_msg = messaging.new_message("selfdriveState")
ss_msg.selfdriveState.enabled = True
iq_msg = messaging.new_message("iqState")
iq_msg.iqState.aol.available = True
iq_msg.iqState.aol.enabled = True
readers = make_readers(selfdriveState=ss_msg, iqState=iq_msg)
sm = FakeSubMaster(readers)
proxy = make_proxy(sm)
channel = FakeChannel()
proxy.add_channel(channel)
proxy.update()
assert json.loads(channel.sent[-1])["data"]["uiStatus"] == "engaged"
iq_msg.iqState.aol.available = False
proxy.update()
assert json.loads(channel.sent[-1])["data"]["uiStatus"] == "engaged"
def test_frame_size_budget(self):
model_msg = messaging.new_message("modelV2")
model = model_msg.modelV2
model.position.x = [float(i) * 3.03 for i in range(33)]
model.position.y = [1.234567] * 33
model.position.z = [0.456789] * 33
model.init("laneLines", 4)
for lane in model.laneLines:
lane.x = [float(i) * 3.03 for i in range(33)]
lane.y = [1.234567] * 33
lane.z = [0.456789] * 33
model.laneLineProbs = [0.9] * 4
model.init("roadEdges", 2)
for edge in model.roadEdges:
edge.x = [float(i) * 3.03 for i in range(33)]
edge.y = [1.234567] * 33
edge.z = [0.456789] * 33
model.acceleration.x = [1.23] * 33
readers = make_readers(modelV2=model_msg)
sm = FakeSubMaster(readers)
proxy = make_proxy(sm)
frame = proxy._build_frame()
encoded = frame_to_str(frame)
assert len(encoded) < 8 * 1024
class TestSessionWiring:
@pytest.mark.asyncio
async def test_set_ui_stream_control_message(self, mocker):
import logging
from types import SimpleNamespace
from iqpilot.system.webrtc.session import StreamSession
mocker.patch("iqpilot.system.webrtc.ui_stream.messaging.SubMaster", return_value=FakeSubMaster(make_readers()))
session = StreamSession.__new__(StreamSession)
session.logger = logging.getLogger("webrtcd")
session.ui_stream_runner = None
session.bitrate_controller = None
session.incoming_bridge = None
channel = FakeChannel()
session.stream = SimpleNamespace(
has_messaging_channel=lambda: True,
get_messaging_channel=lambda: channel,
)
session.message_handler(b'{"type":"setUiStream","enabled":true}')
assert session.ui_stream_runner is not None
await asyncio.sleep(0.05)
session.message_handler(b'{"type":"setUiStream","enabled":false}')
assert session.ui_stream_runner is None
assert len(channel.sent) >= 1
frame = json.loads(channel.sent[0])
assert frame["type"] == "uiStream"

View File

@@ -0,0 +1,75 @@
import json
from types import SimpleNamespace
from parameterized import parameterized_class
import pytest
from iqpilot.system.webrtc.webrtcd import add_ice, get_stream
class FakeSession:
instances = []
def __init__(self, identifier="session"):
self.identifier = identifier
self.started = False
self.stopped = False
self.candidates = []
self.instances.append(self)
async def get_answer(self):
return SimpleNamespace(sdp="answer", type="answer")
def start(self):
self.started = True
async def stop_async(self):
self.stopped = True
async def add_ice_candidate(self, candidate):
self.candidates.append(candidate)
@parameterized_class(("in_services", "out_services"), [
(["testJoystick"], ["carState"]),
([], ["carState"]),
(["testJoystick"], []),
([], []),
])
@pytest.mark.asyncio
class TestWebrtcdProc:
async def test_webrtcd(self, mocker):
session = FakeSession()
mocker.patch("iqpilot.system.webrtc.webrtcd._new_stream_session", return_value=session)
request = mocker.MagicMock()
request.app = {"streams": {}, "debug": False}
request.json = mocker.AsyncMock(return_value={
"sdp": "offer",
"cameras": ["road"],
"bridge_services_in": self.in_services,
"bridge_services_out": self.out_services,
})
response = await get_stream(request)
assert response.status == 200
assert json.loads(response.text) == {"sdp": "answer", "type": "answer"}
assert request.app["streams"] == {session.identifier: session}
assert session.started
async def test_replaces_session_and_routes_ice(self, mocker):
previous = FakeSession("previous")
session = FakeSession("current")
mocker.patch("iqpilot.system.webrtc.webrtcd._new_stream_session", return_value=session)
request = mocker.MagicMock()
request.app = {"streams": {previous.identifier: previous}, "debug": False}
request.json = mocker.AsyncMock(return_value={"sdp": "offer", "cameras": ["road"]})
response = await get_stream(request)
assert response.status == 200
assert previous.stopped
request.json = mocker.AsyncMock(return_value={"candidate": {"candidate": "candidate:1"}})
ice_response = await add_ice(request)
assert ice_response.status == 200
assert session.candidates == [{"candidate": "candidate:1"}]