Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CI: Test all supported Python versions #7

Merged
merged 9 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fredyshox any reason for the less than?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, its probably gonna be ok

"numpy>=1.19.0",
]

Expand All @@ -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"]
Expand Down
9 changes: 5 additions & 4 deletions teleoprtc/builder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import abc
from typing import Dict, List

import aiortc

Expand All @@ -15,9 +16,9 @@ def stream(self) -> WebRTCBaseStream:
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):
Expand Down Expand Up @@ -48,9 +49,9 @@ def stream(self) -> WebRTCBaseStream:
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
Expand Down
35 changes: 17 additions & 18 deletions teleoprtc/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
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
Expand All @@ -14,7 +13,7 @@
@dataclasses.dataclass
class StreamingOffer:
sdp: str
video: list[str]
video: List[str]


ConnectionProvider = Callable[[StreamingOffer], Awaitable[aiortc.RTCSessionDescription]]
Expand All @@ -23,25 +22,25 @@ class StreamingOffer:

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()
Expand Down Expand Up @@ -70,7 +69,7 @@ def _add_consumer_transceivers(self):
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:
Expand All @@ -97,7 +96,7 @@ def _add_producer_tracks(self):

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)

Expand Down Expand Up @@ -256,22 +255,22 @@ def __init__(self, session: aiortc.RTCSessionDescription, *args, **kwargs):
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:
codecs.append(track.codec_preference())

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

Expand Down
8 changes: 4 additions & 4 deletions teleoprtc/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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
Expand All @@ -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}")
Expand All @@ -35,7 +35,7 @@ def __init__(self, camera_type: str, dt: float, time_base: fractions.Fraction =
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):
Expand All @@ -53,7 +53,7 @@ async def next_pts(self, current_pts) -> float:

return pts

def codec_preference(self) -> str | None:
def codec_preference(self) -> Optional[str]:
return None


Expand Down
37 changes: 34 additions & 3 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import asyncio
import sys
import unittest

from aiortc.mediastreams import AudioStreamTrack, VideoStreamTrack
Expand All @@ -11,6 +12,36 @@
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
Expand Down Expand Up @@ -57,7 +88,7 @@ async def test_multi_camera(self, name, cameras, recv_audio, add_messaging):
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")
Expand All @@ -77,7 +108,7 @@ async def test_multi_camera(self, name, cameras, recv_audio, add_messaging):
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")
Expand All @@ -91,7 +122,7 @@ async def test_multi_camera(self, name, cameras, recv_audio, add_messaging):
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")
Expand Down