CI: Test all supported Python versions (#7)

This commit is contained in:
Cameron Clough 2024-06-04 22:11:38 +01:00 committed by GitHub
parent f2e5a6dd9e
commit aa8a76a339
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 74 additions and 39 deletions

View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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")