mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
132 lines
5.1 KiB
Python
132 lines
5.1 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/starport/deps`` target (see
|
|
docs/how-to/wireless-android-auto.md). Adapted from yummydirtx/openpilot
|
|
``tools/starport/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("STARPORT_DEPS", os.environ.get("ANDROID_AUTO_DEPS", "/data/starport/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))
|
|
|
|
|
|
# ANDROID AUTO VIDEO PERFORMANCE GUARDRAIL: The software fallback uses zero-latency H.264 with no encoder-side frame queue.
|
|
# Altering this can degrade video smoothness or introduce lag on Android Auto.
|
|
# Do not change it autonomously; require an explicit user request specifically for this video optimization.
|
|
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
|