mirror of https://github.com/commaai/teleoprtc.git
CI: Test all supported Python versions (#7)
This commit is contained in:
parent
f2e5a6dd9e
commit
aa8a76a339
|
@ -9,10 +9,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/teleoprtc'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-tags: true
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Bump version and tag
|
||||
|
|
|
@ -5,11 +5,15 @@ on: [push, pull_request]
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install aiortc dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
|
@ -22,8 +26,8 @@ jobs:
|
|||
static_analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install pre-commit
|
||||
|
|
|
@ -9,7 +9,7 @@ authors = [{ name="Vehicle Researcher", email="user@comma.ai" }]
|
|||
description = "Comma webRTC abstractions"
|
||||
readme = "README.md"
|
||||
license = { file="LICENSE" }
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
|
@ -18,7 +18,7 @@ classifiers = [
|
|||
dependencies = [
|
||||
"aiortc>=1.6.0",
|
||||
"aiohttp>=3.7.0",
|
||||
"av>=9.0.0,<11.0.0",
|
||||
"av>=11.0.0,<13.0.0",
|
||||
"numpy>=1.19.0",
|
||||
]
|
||||
|
||||
|
@ -35,7 +35,7 @@ dev = [
|
|||
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
|
||||
[tool.ruff]
|
||||
line-length = 160
|
||||
target-version="py311"
|
||||
target-version="py38"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import abc
|
||||
from typing import Dict, List
|
||||
|
||||
import aiortc
|
||||
|
||||
|
@ -15,9 +16,9 @@ class WebRTCStreamBuilder(abc.ABC):
|
|||
class WebRTCOfferBuilder(WebRTCStreamBuilder):
|
||||
def __init__(self, connection_provider: ConnectionProvider):
|
||||
self.connection_provider = connection_provider
|
||||
self.requested_camera_types: list[str] = []
|
||||
self.requested_camera_types: List[str] = []
|
||||
self.requested_audio = False
|
||||
self.audio_tracks: list[aiortc.MediaStreamTrack] = []
|
||||
self.audio_tracks: List[aiortc.MediaStreamTrack] = []
|
||||
self.messaging_enabled = False
|
||||
|
||||
def offer_to_receive_video_stream(self, camera_type: str):
|
||||
|
@ -48,9 +49,9 @@ class WebRTCOfferBuilder(WebRTCStreamBuilder):
|
|||
class WebRTCAnswerBuilder(WebRTCStreamBuilder):
|
||||
def __init__(self, offer_sdp: str):
|
||||
self.offer_sdp = offer_sdp
|
||||
self.video_tracks: dict[str, aiortc.MediaStreamTrack] = dict()
|
||||
self.video_tracks: Dict[str, aiortc.MediaStreamTrack] = dict()
|
||||
self.requested_audio = False
|
||||
self.audio_tracks: list[aiortc.MediaStreamTrack] = []
|
||||
self.audio_tracks: List[aiortc.MediaStreamTrack] = []
|
||||
|
||||
def offer_to_receive_audio_stream(self):
|
||||
self.requested_audio = True
|
||||
|
|
|
@ -2,8 +2,7 @@ import abc
|
|||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Any
|
||||
from collections.abc import Callable, Awaitable
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
||||
|
||||
import aiortc
|
||||
from aiortc.contrib.media import MediaRelay
|
||||
|
@ -14,7 +13,7 @@ from teleoprtc.tracks import parse_video_track_id
|
|||
@dataclasses.dataclass
|
||||
class StreamingOffer:
|
||||
sdp: str
|
||||
video: list[str]
|
||||
video: List[str]
|
||||
|
||||
|
||||
ConnectionProvider = Callable[[StreamingOffer], Awaitable[aiortc.RTCSessionDescription]]
|
||||
|
@ -23,25 +22,25 @@ MessageHandler = Callable[[bytes], Awaitable[None]]
|
|||
|
||||
class WebRTCBaseStream(abc.ABC):
|
||||
def __init__(self,
|
||||
consumed_camera_types: list[str],
|
||||
consumed_camera_types: List[str],
|
||||
consume_audio: bool,
|
||||
video_producer_tracks: list[aiortc.MediaStreamTrack],
|
||||
audio_producer_tracks: list[aiortc.MediaStreamTrack],
|
||||
video_producer_tracks: List[aiortc.MediaStreamTrack],
|
||||
audio_producer_tracks: List[aiortc.MediaStreamTrack],
|
||||
should_add_data_channel: bool):
|
||||
self.peer_connection = aiortc.RTCPeerConnection()
|
||||
self.media_relay = MediaRelay()
|
||||
self.expected_incoming_camera_types = consumed_camera_types
|
||||
self.expected_incoming_audio = consume_audio
|
||||
self.expected_number_of_incoming_media: int | None = None
|
||||
self.expected_number_of_incoming_media: Optional[int] = None
|
||||
|
||||
self.incoming_camera_tracks: dict[str, aiortc.MediaStreamTrack] = dict()
|
||||
self.incoming_audio_tracks: list[aiortc.MediaStreamTrack] = []
|
||||
self.outgoing_video_tracks: list[aiortc.MediaStreamTrack] = video_producer_tracks
|
||||
self.outgoing_audio_tracks: list[aiortc.MediaStreamTrack] = audio_producer_tracks
|
||||
self.incoming_camera_tracks: Dict[str, aiortc.MediaStreamTrack] = dict()
|
||||
self.incoming_audio_tracks: List[aiortc.MediaStreamTrack] = []
|
||||
self.outgoing_video_tracks: List[aiortc.MediaStreamTrack] = video_producer_tracks
|
||||
self.outgoing_audio_tracks: List[aiortc.MediaStreamTrack] = audio_producer_tracks
|
||||
|
||||
self.should_add_data_channel = should_add_data_channel
|
||||
self.messaging_channel: aiortc.RTCDataChannel | None = None
|
||||
self.incoming_message_handlers: list[MessageHandler] = []
|
||||
self.messaging_channel: Optional[aiortc.RTCDataChannel] = None
|
||||
self.incoming_message_handlers: List[MessageHandler] = []
|
||||
|
||||
self.incoming_media_ready_event = asyncio.Event()
|
||||
self.messaging_channel_ready_event = asyncio.Event()
|
||||
|
@ -70,7 +69,7 @@ class WebRTCBaseStream(abc.ABC):
|
|||
if self.expected_incoming_audio:
|
||||
self.peer_connection.addTransceiver("audio", direction="recvonly")
|
||||
|
||||
def _find_trackless_transceiver(self, kind: str) -> aiortc.RTCRtpTransceiver | None:
|
||||
def _find_trackless_transceiver(self, kind: str) -> Optional[aiortc.RTCRtpTransceiver]:
|
||||
transceivers = self.peer_connection.getTransceivers()
|
||||
target_transceiver = None
|
||||
for t in transceivers:
|
||||
|
@ -97,7 +96,7 @@ class WebRTCBaseStream(abc.ABC):
|
|||
|
||||
self.peer_connection.addTrack(track)
|
||||
|
||||
def _add_messaging_channel(self, channel: aiortc.RTCDataChannel | None = None):
|
||||
def _add_messaging_channel(self, channel: Optional[aiortc.RTCDataChannel] = None):
|
||||
if not channel:
|
||||
channel = self.peer_connection.createDataChannel("data", ordered=True)
|
||||
|
||||
|
@ -256,7 +255,7 @@ class WebRTCAnswerStream(WebRTCBaseStream):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.session = session
|
||||
|
||||
def _probe_video_codecs(self) -> list[str]:
|
||||
def _probe_video_codecs(self) -> List[str]:
|
||||
codecs = []
|
||||
for track in self.outgoing_video_tracks:
|
||||
if hasattr(track, "codec_preference") and track.codec_preference() is not None:
|
||||
|
@ -264,14 +263,14 @@ class WebRTCAnswerStream(WebRTCBaseStream):
|
|||
|
||||
return codecs
|
||||
|
||||
def _override_incoming_video_codecs(self, remote_sdp: str, codecs: list[str]) -> str:
|
||||
def _override_incoming_video_codecs(self, remote_sdp: str, codecs: List[str]) -> str:
|
||||
desc = aiortc.sdp.SessionDescription.parse(remote_sdp)
|
||||
codec_mimes = [f"video/{c}" for c in codecs]
|
||||
for m in desc.media:
|
||||
if m.kind != "video":
|
||||
continue
|
||||
|
||||
preferred_codecs: list[aiortc.RTCRtpCodecParameters] = [c for c in m.rtp.codecs if c.mimeType in codec_mimes]
|
||||
preferred_codecs: List[aiortc.RTCRtpCodecParameters] = [c for c in m.rtp.codecs if c.mimeType in codec_mimes]
|
||||
if len(preferred_codecs) == 0:
|
||||
raise ValueError(f"None of {preferred_codecs} codecs is supported in remote SDP")
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import logging
|
||||
import time
|
||||
import fractions
|
||||
from typing import Any
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
import aiortc
|
||||
from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE
|
||||
|
@ -12,7 +12,7 @@ def video_track_id(camera_type: str, track_id: str) -> str:
|
|||
return f"{camera_type}:{track_id}"
|
||||
|
||||
|
||||
def parse_video_track_id(track_id: str) -> tuple[str, str]:
|
||||
def parse_video_track_id(track_id: str) -> Tuple[str, str]:
|
||||
parts = track_id.split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"Invalid video track id: {track_id}")
|
||||
|
@ -35,7 +35,7 @@ class TiciVideoStreamTrack(aiortc.MediaStreamTrack):
|
|||
self._dt: float = dt
|
||||
self._time_base: fractions.Fraction = time_base
|
||||
self._clock_rate: int = clock_rate
|
||||
self._start: float | None = None
|
||||
self._start: Optional[float] = None
|
||||
self._logger = logging.getLogger("WebRTCStream")
|
||||
|
||||
def log_debug(self, msg: Any, *args):
|
||||
|
@ -53,7 +53,7 @@ class TiciVideoStreamTrack(aiortc.MediaStreamTrack):
|
|||
|
||||
return pts
|
||||
|
||||
def codec_preference(self) -> str | None:
|
||||
def codec_preference(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from aiortc.mediastreams import AudioStreamTrack, VideoStreamTrack
|
||||
|
@ -11,6 +12,36 @@ from teleoprtc.stream import StreamingOffer
|
|||
from teleoprtc.info import parse_info_from_offer
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
timeout = asyncio.timeout
|
||||
else:
|
||||
class Timeout:
|
||||
def __init__(self, delay: float):
|
||||
self._delay = delay
|
||||
self._task = None
|
||||
self._timeout_handle = None
|
||||
|
||||
def _timeout(self):
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
|
||||
async def __aenter__(self):
|
||||
self._task = asyncio.current_task()
|
||||
loop = asyncio.events.get_running_loop()
|
||||
self._timeout_handle = loop.call_later(self._delay, self._timeout)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
if self._timeout_handle:
|
||||
self._timeout_handle.cancel()
|
||||
if exc_type is asyncio.CancelledError and self._task and self._task.cancelled():
|
||||
raise asyncio.TimeoutError from exc
|
||||
return False
|
||||
|
||||
def timeout(delay):
|
||||
return Timeout(delay)
|
||||
|
||||
|
||||
class SimpleAnswerProvider:
|
||||
def __init__(self):
|
||||
self.stream = None
|
||||
|
@ -57,7 +88,7 @@ class TestStreamIntegration(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertTrue(stream.is_started)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(2):
|
||||
async with timeout(2):
|
||||
await stream.wait_for_connection()
|
||||
except TimeoutError:
|
||||
self.fail("Timed out waiting for connection")
|
||||
|
@ -77,7 +108,7 @@ class TestStreamIntegration(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(track.kind, "audio")
|
||||
# test audio recv
|
||||
try:
|
||||
async with asyncio.timeout(1):
|
||||
async with timeout(1):
|
||||
await track.recv()
|
||||
except TimeoutError:
|
||||
self.fail("Timed out waiting for audio frame")
|
||||
|
@ -91,7 +122,7 @@ class TestStreamIntegration(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(track.kind, "video")
|
||||
# test video recv
|
||||
try:
|
||||
async with asyncio.timeout(1):
|
||||
async with timeout(1):
|
||||
await stream.get_incoming_video_track(cam, False).recv()
|
||||
except TimeoutError:
|
||||
self.fail("Timed out waiting for video frame")
|
||||
|
|
Loading…
Reference in New Issue