diff --git a/backend/Dockerfile b/backend/Dockerfile index 93f978e..f94a4e1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,12 @@ FROM python:3.13 ENV PYTHONUNBUFFERED=1 +# ffmpeg for node code, e.g. the docs' recipe that re-encodes a clip for a +# wall panel that only decodes H.264 Baseline. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg \ + && rm -rf /var/lib/apt/lists/* + # Install uv # Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/ diff --git a/docs/code/nodes.md b/docs/code/nodes.md index 6dee16a..b76caad 100644 --- a/docs/code/nodes.md +++ b/docs/code/nodes.md @@ -110,6 +110,49 @@ ring and streams bytes to connected screens. New frames eventually replace old ones. A volatile artifact returned as a run result is copied to persistent storage; emitted frames alone are not recorded as run artifacts. +### Making clips playable on an LVGL panel + +A wall panel that renders dashboards natively, such as an ESP32-P4 running +openpanel, decodes only H.264 Constrained Baseline (in software, about 29 fps +at 640×360) and MJPEG. It refuses clips straight from a phone or camera, which +are usually Main or High profile at 1080p or more. Re-encode those with a node +that takes a `video` input port `clip` and has a `video` output port +`panel_clip`: + +```python +import os +import subprocess +import tempfile + +import fluksio + +# H.264 Constrained Baseline, at most 640x360 and 15 fps, mono AAC. +PANEL = [ + "-c:v", "libx264", "-profile:v", "baseline", "-level:v", "3.1", + "-pix_fmt", "yuv420p", "-fpsmax", "15", + "-vf", "scale=w=640:h=360:force_original_aspect_ratio=decrease:force_divisible_by=2", + "-preset", "veryfast", "-crf", "23", "-g", "30", + "-c:a", "aac", "-ac", "1", "-b:a", "64k", "-movflags", "+faststart", +] + + +def process(clip): + source = fluksio.load_artifact(clip) + with tempfile.TemporaryDirectory() as tmp: + target = os.path.join(tmp, "panel.mp4") + subprocess.run( + ["ffmpeg", "-nostdin", "-loglevel", "error", "-i", source, *PANEL, target], + check=True, + timeout=600, + ) + name = os.path.splitext(clip.get("name") or "clip")[0] + "-panel.mp4" + return {"panel_clip": fluksio.save_artifact(target, name, "video/mp4")} +``` + +Use it only for clips headed for such a panel. Browsers play the originals, +and the result plays there too, just smaller. The Docker image ships `ffmpeg`; +a [remote worker](workers.md) needs it on its `PATH`. + ## Printing `print` and `fluksio.logger.info(...)` are captured in the node logs. The