A personal media player for single-board computers. Use it to replace Android TV with something not agonizing.
  • Go 80%
  • C 12.7%
  • Nix 5.3%
  • Shell 2%
Find a file
Eli Ribble b4ee95ff96
browser: try remote trailers in order
The first trailer URL Jellyfin stores is often a dead link, so playing
only trailers[0] failed for a working movie. Try up to three remote URLs
in order, stopping at the first that plays, and report the last error if
none do.
2026-09-24 21:41:31 -07:00
doc docs: update the board address and the browse-UI notes 2026-09-24 18:09:22 -07:00
keys Add basic support for sshd for fast iteration 2026-09-16 20:37:51 -07:00
submission submission: prepare upstream patch series for H3 HDMI audio 2026-09-19 10:28:10 -07:00
tools browser: try remote trailers in order 2026-09-24 21:41:31 -07:00
.gitignore Investigate cedrus nondeterministic decode truncation 2026-09-18 08:14:21 -07:00
AGENTS.md docs: update the board address and the browse-UI notes 2026-09-24 18:09:22 -07:00
build.sh Add basic support for sshd for fast iteration 2026-09-16 20:37:51 -07:00
cecctl.nix cec: add HDMI-CEC control, boot service and investigation 2026-09-22 12:23:07 -07:00
configuration.nix image: bundle yt-dlp so mpv can play remote trailers 2026-09-24 21:41:31 -07:00
deploy.sh Add basic support for sshd for fast iteration 2026-09-16 20:37:51 -07:00
faysa.nix image: package faysa and start it on the display at boot 2026-09-23 11:24:47 -07:00
flake.lock Initial working build of the SD card image 2026-09-16 17:53:57 -07:00
flake.nix Get build working again 2026-09-17 08:39:27 -07:00
glplayer.nix image: package glplayer and put glplay on faysa's PATH 2026-09-23 12:49:51 -07:00
libva-v4l2-request-cedrus-ordering.patch Fix driver ordering, CMA, and render-node discovery 2026-09-17 15:47:11 -07:00
libva-v4l2-request-format-reprogram.patch libva: make mpv --hwdec=vaapi work (V4L2 format reprogramming) 2026-09-18 15:47:21 -07:00
libva-v4l2-request-scaling-matrix.patch mpv: fix H.264 hardware playback (libva luma and NV12 chroma) 2026-09-19 08:25:17 -07:00
libva-v4l2-request.nix mpv: fix H.264 hardware playback (libva luma and NV12 chroma) 2026-09-19 08:25:17 -07:00
mesa-lima-linear-stride.patch mesa: also relax lima's linear import size check; rename the mpv fallback option 2026-09-21 22:27:59 -07:00
mpv-kmsro-render-node.patch Fix driver ordering, CMA, and render-node discovery 2026-09-17 15:47:11 -07:00
mpv-kmsro-vaapi-render-node.patch mpv: make VA-API zero-copy work under the libmpv render API 2026-09-19 17:43:34 -07:00
mpv-rg8-texture-rg.patch mpv: fix H.264 hardware playback (libva luma and NV12 chroma) 2026-09-19 08:25:17 -07:00
README.md image: run faysa as a mediauser user service 2026-09-23 12:12:32 -07:00
remotectl.nix input: recognise the USB wireless remote and add remotectl 2026-09-19 11:03:44 -07:00
sd-image.nix image: restart the UI on exit and keep the console lit 2026-09-24 18:09:25 -07:00
simple-card-tdm-slot-width.patch audio: request 32-bit I2S slots for HDMI; fix simple-card slot handling 2026-09-19 09:23:06 -07:00
uboot-sunxi-larger-kernel-region.patch Fix FDT image overlap 2026-09-16 18:49:24 -07:00

Faysa

NixOS SD-card image for the Orange Pi One (Allwinner H3, armv7l).

The image is cross-compiled from an x86_64 host. U-Boot (with the upstream orangepi_one_defconfig), the mainline kernel, the H3 device tree and an extlinux boot entry are all installed into a raw SD-card image.

Build

nix build .#nixosConfigurations.orangepi.config.system.build.sdImage
# or: ./build.sh

The raw image lands in result/sd-image/*.img (~2.9 GiB). On first boot the root partition is grown to fill the SD card.

Flakes only see Git-tracked files. If you add or edit a .nix file and Nix complains that a path "is not tracked by Git", run git add <file>.

How the cross-compilation works

flake.nix sets:

nixpkgs.hostPlatform  = "armv7l-linux";
nixpkgs.buildPlatform = "x86_64-linux";

Nixpkgs then builds a cross toolchain (runs on x86_64, emits armv7l). The toolchain, glibc, systemd, rustc/cargo, etc. are available from cache.nixos.org, so only a few hundred packages (kernel, U-Boot, NetworkManager, …) are built locally. A from-scratch build took roughly an hour on 16 cores.

Do not use system = "armv7l-linux" on its own: that asks for a native armv7l build, which your x86_64 machine cannot execute. The alternative is boot.binfmt.emulatedSystems = [ "armv7l-linux" ], which runs armv7l build tools through QEMU — reliable, but there is no armv7l binary cache, so everything (including the C toolchain) bootstraps from source under emulation. That is dramatically slower.

Flash

Write the whole image to the raw device, not to a partition:

sudo dd if=result/sd-image/nixos-image-sd-card-*.img of=/dev/sdX bs=4M status=progress conv=fsync
sync

Replace /dev/sdX with the SD card device (check carefully with lsblk).

First boot

  • Serial console on ttyS0 at 115200 8N1 (also tty0/HDMI).
  • Login: user pi, password orangepi (change it!).
  • SSH is enabled; PermitRootLogin is no, sudo needs no password for wheel.
  • Any keys/*.pub files added before the build are authorised for pi.
  • Wired Ethernet is managed by NetworkManager.
  • The device is named faysa and advertises itself with Avahi, so faysa.local resolves on the LAN.
  • faysa starts automatically on the HDMI display (faysa.service, a mediauser user service) and shows the setup checks: network connectivity, then a Jellyfin sign-in by remote (Quick Connect) or by scanning a QR code from a phone. It needs no SSH and no hand-written config. Running inside the mediauser login session is what gives it the session's PipeWire and D-Bus for playback. The getty autologin shell stays underneath; systemctl --user stop faysa (as mediauser) hands the console back to it.

Fast iteration: deploy over SSH

Once the device is running you do not need to reflash to test a change. The image runs sshd and makes the default user a Nix trusted user, so the closure can be cross-built on this host and pushed straight to the Pi:

./deploy.sh pi@<device-ip>

deploy.sh is a thin wrapper around:

nixos-rebuild switch \
  --flake .#orangepi \
  --target-host pi@<device-ip> \
  --sudo

It builds on this x86_64 host by default (omit --build-host, or it will try to SSH to that host instead), nix copys the resulting closure to the device and runs its activation script there. No image, no SD-card swap, no reboot.

One-time setup

SSH key auth is expected — otherwise the push has to prompt for a password on every connection. Drop your public key into keys/ before building:

cp ~/.ssh/id_ed25519.pub keys/
git add keys/id_ed25519.pub   # flakes only see Git-tracked files

Every keys/*.pub file is installed as an authorised key for pi (see keys/README.md). If the device still runs the previous image, flash once more so it gains the key and the trusted-users setting; after that, updates are pushed in place.

deploy.sh defaults to pi@orangepi.local and passes -o StrictHostKeyChecking=accept-new (the host key changes on every reflash).

Why the push is allowed

nixos-rebuild --target-host uploads store paths that were built locally and are therefore unsigned. Nix refuses to import unsigned paths unless the uploading user is trusted, so configuration.nix sets:

nix.settings.trusted-users = [ "root" "@wheel" ];

pi is in wheel. Activation (switch-to-configuration, the system profile update) then runs as root through sudo, which is passwordless for wheel. This is convenient for a throwaway dev image; on a shared machine note that trusted users can change Nix daemon settings, so scope the list down if that matters.

Video playback (mpv)

mpv is installed for the mediauser account and is the baseline test for whether the image can put video on screen. There is no X server and no Wayland compositor, so mpv is cross-built with only the DRM/KMS backends (plus VA-API and a headless ffmpeg).

Log in on the serial console or over SSH as mediauser, then:

# GPU output through mesa's lima + the sun4i DRM/KMS driver, over HDMI.
# mpv picks the DRM/EGL context automatically when X11/Wayland are absent.
mpv file.mp4

# CPU-only output straight to a DRM dumb buffer, as a fallback.
mpv --vo=drm file.mp4

hardware.graphics.enable installs Mesa (with the lima gallium driver for the H3's Mali-400) into /run/opengl-driver, which is where mpv's RUNPATH looks for EGL/GBM. Mesa is the largest single addition to the closure (~1 GiB), so the image is now ~4.7 GiB rather than ~2.9 GiB.

Hardware decoding

Status: H.264 decodes correctly with both stacks and mpv --hwdec=vaapi plays it correctly with zero-copy dmabuf import. After the kernel fix and the libva fixes described below, H.264 decodes byte-for-byte identically to software with no nondeterministic failures, in both ffmpeg -hwaccel vaapi and GStreamer. mpv's zero-copy NV12 display had a chroma bug (blue rendered magenta) because mpv sampled the imported DRM_FORMAT_GR88 chroma plane as luminance-alpha on its GL 2.1/ES2 context; mpv-rg8-texture-rg.patch fixes it by enabling rg8 via GL_ARB/EXT_texture_rg. A minimal lima reproducer (tools/cedrus/lima-r8g8-probe.c) proved lima itself was fine. HEVC has a small residual difference and MPEG-2 has a separate GStreamer-only problem; see doc/cedrus-investigation.md §13–14. The GStreamer zero-copy KMS path needs one extra kmssink option to be visible on this board — the kernel console owns the plane above the video layer; see below.

These pieces are in place:

  • Kernel. sd-image.nix turns on CONFIG_VIDEO_SUNXI_CEDRUS (gated behind CONFIG_VIDEO_SUNXI), so mainline's sunxi-cedrus driver registers /dev/video0 as a stateless V4L2 decoder. It is loaded explicitly via boot.kernelModules.
  • CMA. cedrus allocates every decoded frame from the contiguous memory pool (~3 MiB for 1080p). The 32 MiB default fills (and fragments) almost immediately, which shows up as cedrus ... dma alloc of size N failed and dropped frames, so boot.kernelParams sets cma=256M.
  • VA-API driver. libva-v4l2-request.nix packages the driver that turns VA-API calls into V4L2 request API calls. It installs through hardware.graphics.extraPackages and is selected with LIBVA_DRIVER_NAME=v4l2_request (set in configuration.nix).
  • mpv. built with the DRM/EGL GPU context, plus a patch for kmsro render-node discovery.

What works, verified on an Orange Pi One:

v4l2-ctl --list-devices        # cedrus /dev/video0, H264+HEVC+MPEG2 formats
vainfo                         # driver v4l2-request, H264/HEVC/MPEG2 profiles

Frames decode byte-for-byte identically to software (mean-absolute-difference 0.00 for both Y and UV), and repeated runs give identical output.

The failures this section used to describe turned out to be a nondeterministic VPU decode error, not bad reference handling. The H3 VPU is not cache-coherent, but cedrus allocated its auxiliary buffers with DMA_ATTR_NO_KERNEL_MAPPING; on 32-bit ARM that flag leaves the CMA pages cacheable in the kernel linear map, so stale cache lines corrupted the buffers and the VPU failed to decode the first slice of a stream with VE_H264_STATUS_DECODE_ERR_INT. tools/cedrus/cedrus-aux-coherency.patch (wired in via boot.kernelPatches in sd-image.nix) allocates those buffers coherently and zeroes them. Full write-up: doc/cedrus-investigation.md §13.

hwdec is requested explicitly (it is off by default):

mpv --hwdec=vaapi file.mp4        # zero-copy dmabuf import via lima
mpv --hwdec=vaapi-copy file.mp4   # cedrus decodes, frames copied back

--hwdec=vaapi fixes the pacing completely (a 1080p24 sample that dropped ~135 frames in software drops 1-3), and the libva decode is now bit-exact: the wrong-luma bug was the fork's Inter-Y 8x8 scaling-list mapping, fixed by libva-v4l2-request-scaling-matrix.patch (§14.9). mpv's zero-copy NV12 chroma was then wrong (blue rendered magenta) because, on a GL 2.1 context without GL_ARB_texture_float, mpv picked its GL_LUMINANCE_ALPHA fallback for the two-component chroma plane and sampled .ra, while the imported DRM_FORMAT_GR88 binds as GL_RG. mpv-rg8-texture-rg.patch enables mpv's rg8 format via GL_ARB/EXT_texture_rg (§14.12). The full sample plays with hardware decode, zero-copy and 1-3 dropped frames (the display-sync artefact).

The equivalent GStreamer KMS path (also correct and zero-copy):

gst-launch-1.0 -e filesrc location=file.mkv ! matroskademux ! h264parse ! \
  v4l2slh264dec ! kmssink plane-properties=s,zpos=3

plane-properties=s,zpos=3 is required to make the picture appear, not just cosmetic: sun4i-drm exposes the mixer's RGB "UI" layer as the DRM primary plane and the YUV/NV12 "VI" layer as an overlay, the kernel framebuffer console occupies the primary, and the driver composites the primary above the overlay. kmssink defaults to the VI overlay (the only plane that accepts the decoder's NV12 dmabufs), so without the zpos bump the console hides the video and the pipeline runs to EOS with no picture. Raising the VI plane above the UI plane keeps the path zero-copy. The syntax needs a structure name — s,zpos=3, not zpos=3. A full 1080p24 clip then plays in real time with a couple of seconds of CPU total and no dropped buffers; see doc/cedrus-investigation.md §14.8.

HEVC still shows a small residual and MPEG-2 is still wrong under GStreamer.

cedrus only covers H.264/H.265/MPEG-2; anything else falls back to software.

Cross-checking with GStreamer

gstreamer and gst-plugins-base/good/bad are installed, providing the mainline v4l2codecs plugin (v4l2slh264dec, v4l2slh265dec, v4l2slmpeg2dec) and kmssink. It is an independent implementation of the same stateless V4L2 API, which is what lets us tell a kernel problem apart from a userspace-driver problem.

Gotcha: gst_all_1.gstreamer's default output is bin, and the core plugins (filesrc, fakesink, …) live in out, so the GST_PLUGIN_SYSTEM_PATH_1_0 in configuration.nix names gstreamer.out explicitly. Without that, gst-launch finds the codec plugins but not the core elements.

# Decode-only smoke test.
gst-launch-1.0 -e filesrc location=file.mp4 ! qtdemux ! h264parse ! \
  v4l2slh264dec ! fakesink

# Zero-copy to the screen: the decoder emits NV12_32L32 (cedrus's tiled format)
# backed by dmabufs, which kmssink imports directly.  plane-properties=s,zpos=3
# raises the video plane above the kernel console plane so the picture is
# actually visible (see "Hardware decoding" and §14.8 of the investigation doc).
gst-launch-1.0 -e filesrc location=file.mp4 ! qtdemux ! h264parse ! \
  v4l2slh264dec ! kmssink plane-properties=s,zpos=3

# Dump frames to compare against a software decode. Forcing linear NV12 avoids
# having to de-tile; the native NV12_32L32 also works.
gst-launch-1.0 -e filesrc location=file.mp4 ! qtdemux ! h264parse ! \
  v4l2slh264dec ! video/x-raw,format=NV12 ! filesink location=out.nv12

Patches carried

mainline cedrus and the libva fork do not agree on call ordering, the fork's format handling is too conservative for mpv, it mis-maps one H.264 scaling list, and mpv mishandles the two-plane NV12 format the fork exports, so five patches are carried (three on the fork, two on mpv):

  • libva-v4l2-request-cedrus-ordering.patch. The driver programs the OUTPUT (compressed) format in vaCreateContext, i.e. after vaCreateSurfaces has already allocated CAPTURE buffers. cedrus derives the CAPTURE format from OUTPUT and rejects VIDIOC_S_FMT on OUTPUT once CAPTURE buffers exist (vb2_is_busy(peer_vq) → -EBUSY), so decoding never starts. The patch moves the OUTPUT format program to vaCreateSurfaces, where the picture dimensions are available.
  • libva-v4l2-request-format-reprogram.patch. The fork programs the V4L2 formats only on the first vaCreateSurfaces call, so mpv's surface-format probe (128x128, with the MPEG-2 profile) latched the device to MG2S 128x128 and the real H.264 decode failed with V4L2_BUF_FLAG_ERROR, making mpv fall back to software. The patch re-programs the OUTPUT/CAPTURE formats when the codec changes or the requested surface grows, and gates the driver's per-frame request_log() behind V4L2_REQUEST_DEBUG.
  • libva-v4l2-request-scaling-matrix.patch. The fork copied H.264's Inter-Y 8x8 scaling list to scaling_list_8x8[3] (Inter Cb) instead of [1], leaving the Inter-Y slot zero. cedrus only reads lists 0 and 1, so High-profile inter macroblocks reconstructed wrong luma while chroma (which uses the 4x4 lists) stayed exact — the "wrong luma on B-frame streams" symptom. The patch places the list at index 1.
  • mpv-rg8-texture-rg.patch. mpv's VA-API dmabuf interop imports the chroma plane with DRM_FORMAT_GR88 (which binds as GL_RG), but on its GL 2.1 context mpv's rg8 format is gated behind GL3/ES3/F_GL2F, so it falls back to the GL_LUMINANCE_ALPHA format and samples .ra — whose .a is 1.0 for the imported GL_RG texture. The patch enables rg8 via GL_ARB/EXT_texture_rg, which lima provides. See doc/cedrus-investigation.md §14.12.
  • mpv-kmsro-render-node.patch. The H3 is a kmsro setup: display is sun4i-drm (the display card) and the GPU is lima (renderD128), two separate DRM devices. mpv's DRM/EGL windower uses drmGetRenderDeviceNameFromFd(), which only finds a render node belonging to the primary device, so it comes up empty and the VA-API interop cannot create a display. The patch falls back to the first usable render node.
  • mesa-lima-linear-stride.patch. lima validates an imported linear buffer against ALIGN(width, 16) for both the stride and the size. The 16-texel alignment is a tiled-layout rule, but applying it to linear buffers rejects the chroma plane of any NV12 frame whose width is not a multiple of 32: the plane is width/2 texels wide and shares the luma stride, so at 720x356 its 720 B stride is below lima's computed 736 B minimum, and at 720x480 its 174592-byte buffer is below the computed 176640-byte minimum. Either way the EGL import fails on every frame (linear imported buffer stride is smaller than minimal / imported bo size is smaller than expected). The patch uses the unaligned width and the buffer's real stride for the linear checks, so 720-wide video can use zero-copy. See doc/cedrus-investigation.md §14.13.

Notes on the driver package

  • Upstream Bootlin libva-v4l2-request is unmaintained (last commit 2019) and still uses the pre-upstream private V4L2_CID_MPEG_VIDEO_H264_* control ABI, so it does not even compile against current kernel headers. The package here tracks the Cn8001 fork, which uses the stable V4L2_CID_STATELESS_* controls (Linux >= 6.0) that mainline cedrus implements.
  • ffmpeg's v4l2request hwaccel is not in mainline ffmpeg (n9.0 only ships the stateful v4l2_m2m, which cedrus does not implement), so there is no --enable-v4l2-request build flag to turn on. VA-API is the way to drive cedrus from mpv.
  • Audio over HDMI now works; see "Audio playback (HDMI)" below and doc/audio-investigation.md.

Audio playback (HDMI)

Status: working. ALSA card 0 is named HDMI and plays 48 kHz stereo over HDMI (verified audibly). mpv uses it automatically:

mpv file.mp4                 # audio + video
mpv --no-video file.flac     # audio only

The Orange Pi One has no analogue audio jack, so HDMI is the only onboard output. Mainline ships no Allwinner board with an HDMI sound card, so sd-image.nix adds one with hardware.deviceTree.overlays:

  • enable &i2s2 (the H3 I2S instance wired to the HDMI; the Allwinner BSP drives HDMI audio from the same register base, 0x01c22800),
  • add #sound-dai-cells = <0> to &hdmi, so the DW-HDMI core's already auto-registered hdmi-codec can be referenced as sound-dai = <&hdmi>,
  • link them with a playback-only simple-audio-card.

The DW-HDMI I2S input expects a 64·fs BCLK, but sun4i-i2s derives BCLK from the physical sample width (16-bit ⇒ 32·fs). The overlay requests 32-bit slots (dai-tdm-slot-width = <32>), which keeps 16-bit samples — and therefore IEC958/AC3/DTS passthrough — intact while raising BCLK to 64·fs. simple-card parses those properties but drops them unless a slot-width map is present, so simple-card-tdm-slot-width.patch is carried. Full write-up, evidence (including the I2S clock-divider register readback) and next steps: doc/audio-investigation.md.

services.pipewire (ALSA and PulseAudio compatibility) is enabled and mpv is built with native PipeWire output, so media playback and UI cues can mix; two simultaneous streams work. WirePlumber selects the HDMI card as the default sink. security.rtkit is enabled for real-time scheduling.

Input / wireless remote

Status: working. A 2.4 GHz air-mouse remote (XING WEI, 1915:1025) enumerates as a USB composite device and creates five evdev nodes under /dev/input/. With no X server or Wayland compositor, raw evdev is the control interface; mediauser can read the nodes (input group), and remotectl reads and decodes them all at once:

sudo evtest        # interactive picker, one device at a time
remotectl          # decoded events from every remote node

The remote is split across several nodes: a keyboard (typing, arrows, OK is KEY_ENTER), a relative pointer (REL_X/REL_Y only), a Consumer Control node (volume/mute/back/home/voice) and a System Control node (power). The pointer node emits BTN_LEFT/BTN_RIGHT and relative motion but no wheel events, and a fifth, unnamed node never emits anything at all. eventN numbers are assigned in registration order and move between boots, so remotectl matches devices by name; the full signal map, capability dumps and the permission/policy details are in doc/input-investigation.md.

Two shapes of this remote are deliberately neutered:

  • services.logind.settings.Login.Handle{Power,Suspend,Hibernate}Key = "ignore" stops the power/sleep buttons from acting on the headless box.
  • systemd.ctrlAltDelUnit points at an empty ctrl-alt-del-blocked.target, so the Fn combination that synthesises Ctrl+Alt+Del no longer reboots the board. The keystrokes are still delivered to applications.

remotectl is a small libevdev reader (tools/input/remotectl.c) that matches the remote by device name, polls all matching nodes and prints decoded events; it is the seed of the real control layer.

HDMI-CEC

Status: working end to end against a Denon AVR-S960H. The Pi sits behind a Denon AVR-S960H on the way to a TV; the receiver answers CEC fully (Audio System, logical address 5) and discovery, power on/off, volume/mute, audio status, system-audio mode and input routing are all verified. The TV itself ACKs polls but answers no queries, and the two PC monitors tested implement no CEC. Full write-up, protocol background and evidence: doc/cec-investigation.md.

The H3's DesignWare HDMI block has an integrated CEC controller; mainline's dw-hdmi-cec registers it as /dev/cec0 and needs no device-tree change. cecctl (tools/cec/cecctl.c) claims a logical address and sends/receives messages:

cecctl info                       # adapter, physical + logical address
cecctl topology                   # which logical addresses answer
cecctl discover 0                 # version, PA, vendor, OSD name, power, features
cecctl power on|off|toggle|status # TV power
cecctl volume up|down|mute|status # TV / audio-system volume
cecctl input 2.0.0.0              # switch the TV to a physical address
cecctl system-audio on|off|status # AVR audio routing; `on` powers it on
cecctl ui play|up|enter|...       # any User Control button (`ui list`)
cecctl send 40 04                 # raw message

systemd.services.cec-adapter waits for the sink EDID (cecctl wait) and claims a playback LA named Faysa with RC passthrough, retrying until the HDMI link is up. With passthrough on, the TV remote's buttons are decoded by the kernel (rc-cec) and arrive on a dw_hdmi evdev node, which the frontend can read with input.Open("dw_hdmi") to drive the UI just like the USB remote.

The Go frontend drives the same control path directly: tools/ui/cec speaks the CEC UAPI with no cgo or helper binary, and the browser routes the USB remote's volume/mute buttons to the audio system (falling back to mpv) and its power button to a system power toggle, while playback start performs One Touch Play and a System Audio Mode Request so a receiver powers on and takes over audio. See tools/ui/README.md.

Layout

Path Purpose
flake.nix Inputs, host/build platform, nixosConfigurations.orangepi
configuration.nix Hostname, users, SSH, NetworkManager, packages
sd-image.nix Orange Pi One board support: U-Boot offset, DTB, extlinux, cedrus
libva-v4l2-request.nix VA-API driver for the stateless V4L2 decoder (cedrus)
libva-v4l2-request-cedrus-ordering.patch Fixes the VA driver's OUTPUT/CAPTURE format ordering
libva-v4l2-request-scaling-matrix.patch Fixes the Inter-Y 8x8 scaling-list mapping (wrong luma)
mpv-rg8-texture-rg.patch Enables mpv's rg8 format on GL2.1/ES2 via GL_ARB/EXT_texture_rg
mpv-kmsro-render-node.patch Makes mpv find the lima render node on kmsro
mesa-lima-linear-stride.patch Lets lima import the NV12 chroma plane of non-32-aligned widths (720)
mpv-kmsro-vaapi-render-node.patch Makes VA-API zero-copy work under the libmpv render API on kmsro
simple-card-tdm-slot-width.patch Makes simple-card apply dai-tdm-slot-* so HDMI gets 64·fs BCLK
remotectl.nix libevdev package for the remote's input-validation tool
cecctl.nix HDMI-CEC control tool (kernel UAPI, no dependencies)
tools/input/remotectl.c Multi-device evdev reader/decoder for the remote
tools/cec/cecctl.c Userspace CEC: claim, discover, power, volume, input, monitor
doc/cec-investigation.md CEC protocol, discovery/error model, monitor verdict, usage
doc/frontend-investigation.md Go full-screen frontend options (KMS, software 2D, video handoff)
doc/jellyfin-investigation.md Jellyfin discovery, auth, listing, streaming and transcode fallback
tools/ui/ Pure-Go frontend toolkit: DRM/KMS (page flips), evdev input, mpv handoff
tools/ui/jellyfin/ Jellyfin API client: discovery, Quick Connect/password auth, sessions, listing, stream URLs
tools/ui/cmd/jellyfin/ CLI to discover servers, authenticate, list items and resolve streams
uboot-sunxi-larger-kernel-region.patch Moves U-Boot's FDT/ramdisk load addresses clear of the kernel
build.sh Convenience wrapper around nix build
deploy.sh Cross-build and push the system to a running device over SSH
submission/ Prepared upstream patch series (ASoC fix + Allwinner DT) and sending instructions
keys/ Public keys authorised to log in as pi (see keys/README.md)

What sd-image.nix adds

  • Reuses ubootOrangePiPc with the orangepi_one_defconfig defconfig (nixpkgs ships no Orange Pi One package).
  • Writes u-boot-sunxi-with-spl.bin at byte offset 8192 (bs=1024 seek=8), where the Allwinner BROM expects the eGON.BT0 SPL.
  • Sets hardware.deviceTree.name = "sun8i-h3-orangepi-one.dtb" so extlinux emits an explicit FDT line.
  • Enables generic-extlinux-compatible, which U-Boot's distro-boot reads from /boot/extlinux/extlinux.conf on the ext4 root partition.
  • Patches U-Boot's sunxi memory layout (see below).
  • Enables the H3 VPU (CONFIG_VIDEO_SUNXI_CEDRUS) and loads it via boot.kernelModules (see "Hardware decoding" above).
  • Adds the HDMI sound card via a device-tree overlay, plus the simple-card-tdm-slot-width.patch kernel fix (see "Audio playback (HDMI)").

Why U-Boot is patched

U-Boot's default layout for sunxi boards with at least 256 MiB of DRAM loads the kernel at 0x42000000 and the device tree at 0x43000000 — only 16 MiB apart. The armv7l kernel Image is larger than that, so U-Boot aborts before booting:

ERROR: FDT image overlaps OS image (OS=42000000..4313f200)
EXTLINUX FAILED: continuing...

uboot-sunxi-larger-kernel-region.patch moves the FDT, script, PXE, overlay and ramdisk addresses up to 0x4B000000+ (mirroring the ARM64 sunxi layout), giving the kernel ~144 MiB of room. The addresses stay below 0x50000000, so they are valid on 256 MiB boards too. The patch is applied through the ubootOrangePiOne overlay in sd-image.nix.

Notes / possible tweaks

  • The 30 MiB FAT "FIRMWARE" partition is unused on sunxi (it exists only for Raspberry Pi firmware). Shrink it via sdImage.firmwareSize if desired.
  • sdImage.compressImage = false keeps iteration fast; set it to true to get a .img.zst.
  • sd-image.nix pulls in hardware.enableAllHardware (via the base SD module), which drags in a lot of firmware. Set hardware.enableAllHardware = false plus the specific firmware you need to slim the closure.
  • Enable services.avahi.nssmdns4 = true if you want orangepi.local to resolve without having to look up the DHCP-assigned IP.