Files
StarPilot/openpilot/starpilot/system/android_auto/encoder.py
T
firestar5683 99a8b1c07e Bluetooth and Android Auto
Manage named devices, car-first projection reconnect, and saved head-unit layouts. Keep runtime-safe Bluetooth operations available without changing controller ownership or restarting the radio unnecessarily.
2026-10-03 22:34:37 +00:00

129 lines
4.8 KiB
Python

"""One-frame-at-a-time software H.264 for Android Auto (baseline, zero latency).
Each call encodes exactly one RGBA frame into one AUD-delimited Annex B access
unit, so there is never an encoder-side queue. Keyframes carry SPS/PPS
(``repeat-headers``) and are forced on request, which the session requires at
the start of every media epoch (new connection or regained display focus).
PyAV is taken from the system Python when its bundled FFmpeg has libx264;
otherwise from an isolated ``/data/android_auto/deps`` target (see
docs/how-to/wireless-android-auto.md). Adapted from yummydirtx/openpilot
``tools/android_auto/live_encode.py`` (MIT), pinned at 672a16f6183567c0ada53654f8527d97e1a483fa.
"""
from __future__ import annotations
import os
import re
import sys
import time
from fractions import Fraction
ISOLATED_DEPS = os.environ.get("ANDROID_AUTO_DEPS", "/data/android_auto/deps")
MAX_ACCESS_UNIT = 2 * 1024 * 1024 - 16
START_CODE = re.compile(b"\x00\x00(?:\x00)?\x01")
def nal_types(data: bytes) -> list[int]:
types = []
for match in START_CODE.finditer(data):
if match.end() < len(data):
types.append(data[match.end()] & 31)
return types
def import_av():
"""Import a PyAV whose FFmpeg includes libx264, preferring the system install."""
errors = []
try:
import av
if "libx264" in av.codecs_available:
return av
errors.append(f"system PyAV {av.__version__} lacks libx264")
except ImportError as error:
errors.append(f"system PyAV missing ({error})")
if os.path.isdir(ISOLATED_DEPS):
for name in [key for key in sys.modules if key == "av" or key.startswith("av.")]:
del sys.modules[name]
sys.path.insert(0, ISOLATED_DEPS)
try:
import av
if "libx264" in av.codecs_available:
return av
errors.append(f"isolated PyAV {av.__version__} lacks libx264")
except ImportError as error:
errors.append(f"isolated PyAV missing ({error})")
finally:
if sys.path and sys.path[0] == ISOLATED_DEPS:
sys.path.pop(0)
raise RuntimeError("No H.264 encoder available: " + "; ".join(errors))
class H264Encoder:
backend = "libx264"
def __init__(self, width: int, height: int, fps: int = 30, bitrate_kbps: int = 4000):
if not (0 < width <= 1920 and 0 < height <= 1080) or width % 2 or height % 2:
raise ValueError("H.264 requires even dimensions no larger than 1920x1080")
av = import_av()
self.av = av
self.width, self.height, self.fps = width, height, fps
self.frame_index = 0
self.last_encode_ms = 0.0
level = "4.0" if height > 720 else "3.1"
codec = av.CodecContext.create("libx264", "w")
codec.width = width
codec.height = height
codec.pix_fmt = "yuv420p"
codec.time_base = Fraction(1, fps)
codec.framerate = Fraction(fps)
codec.thread_count = 1
codec.gop_size = fps * 2
codec.max_b_frames = 0
codec.options = {
"preset": "ultrafast", "tune": "zerolatency", "profile": "baseline", "level": level, "forced-idr": "1",
# Bounded bitrate keeps Wi-Fi bursts small; periodic IDR bounds recovery from a lost frame.
"x264-params": ":".join((
"aud=1", "repeat-headers=1", "annexb=1", f"keyint={fps * 2}", f"min-keyint={fps}", "scenecut=0",
f"vbv-maxrate={bitrate_kbps}", f"vbv-bufsize={bitrate_kbps // 2}", "crf=23")),
}
codec.open()
self.codec = codec
self._frame = av.VideoFrame(width, height, "rgba")
if self._frame.planes[0].line_size != width * 4:
raise RuntimeError("PyAV RGBA frame is not tightly packed")
def encode_rgba(self, rgba, *, keyframe: bool = False) -> tuple[bytes, bool]:
"""Encode one frame; returns ``(access_unit, is_keyframe)``."""
if self.codec is None:
raise RuntimeError("Encoder has been closed")
started = time.monotonic()
self._frame.planes[0].update(rgba)
frame = self._frame.reformat(format="yuv420p")
frame.pts = self.frame_index
frame.time_base = self.codec.time_base
force = keyframe or self.frame_index == 0
if force:
frame.pict_type = self.av.video.frame.PictureType.I
packets = self.codec.encode(frame)
if len(packets) != 1:
raise RuntimeError(f"Low-latency encoder buffered or split a frame ({len(packets)} packets)")
data = bytes(packets[0])
types = nal_types(data)
if len(data) > MAX_ACCESS_UNIT or types.count(9) != 1:
raise RuntimeError("Encoder did not produce one bounded AUD-delimited access unit")
is_keyframe = {5, 7, 8}.issubset(types)
if force and not is_keyframe:
raise RuntimeError("Encoder did not produce an independently decodable keyframe")
self.frame_index += 1
self.last_encode_ms = (time.monotonic() - started) * 1000
return data, is_keyframe
def close(self) -> None:
codec, self.codec = self.codec, None
if codec is not None:
try:
codec.encode(None)
except Exception:
pass