deploy: bundle pre-built MPP libs for RK3326, update setup script and status
- deploy/arkos/mpp-libs/: add librockchip_mpp.so*, librockchip_vpu.so*, libgstrockchipmpp.so — built from source via Docker QEMU (arm64v8/ubuntu:focal) using rockchip-linux/mpp + JeffyCN/mirrors@gstreamer-rockchip (-Drga=disabled) - deploy/arkos/setup_hw_decode.sh: detect mpp-libs/ subdir and install from it automatically, no network required; apt fallback retained - deploy/arkos/mpp-libs/README.md: document origin, target SoC, install steps - tests/test_video_playback_device.py: on-device GStreamer diagnostic script - docs/development-status.md: mark MPP HW decode deployed, mppvideodec verified Verified on physical R36S: mppvideodec found by GStreamer registry with GST_PLUGIN_PATH=/usr/lib/aarch64-linux-gnu/gstreamer-1.0
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# mpp-libs — Pre-built Rockchip MPP libraries for R36S / ArkOS
|
||||
|
||||
These `.so` files are compiled for **aarch64 / Ubuntu focal** from:
|
||||
|
||||
- **librockchip-mpp** — [github.com/rockchip-linux/mpp](https://github.com/rockchip-linux/mpp) (commit c2c1ee5, 2026-03-10)
|
||||
- **gst-mpp** — [github.com/JeffyCN/mirrors, branch gstreamer-rockchip](https://github.com/JeffyCN/mirrors) (RGA disabled)
|
||||
|
||||
Built via Docker QEMU (`arm64v8/ubuntu:focal` + `qemu-user-static`).
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `librockchip_mpp.so{,.0,.1}` | Rockchip MPP runtime — hardware VPU abstraction |
|
||||
| `librockchip_vpu.so{,.0,.1}` | Rockchip VPU legacy wrapper |
|
||||
| `libgstrockchipmpp.so` | GStreamer plugin — exposes `mppvideodec` element |
|
||||
|
||||
## Install
|
||||
|
||||
Run the parent `setup_hw_decode.sh` as root — it picks up this directory automatically:
|
||||
|
||||
```bash
|
||||
sudo bash deploy/arkos/setup_hw_decode.sh
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
sudo cp librockchip_mpp.so* librockchip_vpu.so* /usr/lib/aarch64-linux-gnu/
|
||||
sudo cp libgstrockchipmpp.so /usr/lib/aarch64-linux-gnu/gstreamer-1.0/
|
||||
sudo ldconfig
|
||||
```
|
||||
|
||||
## Target
|
||||
|
||||
- SoC: RK3326 / PX30 (Cortex-A35)
|
||||
- Kernel: 4.4 BSP with `vpu_service` device node at `/dev/vpu_service`
|
||||
- ArkOS Ubuntu eoan arm64
|
||||
- Requires `/dev/vpu_service` accessible to `video` group (handled by `setup_hw_decode.sh` udev rule)
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,8 +4,11 @@
|
||||
# What this script does:
|
||||
# 1. Adds a udev rule so the 'video' group can access /dev/vpu_service
|
||||
# (the VPU device used by Rockchip MPP on the kernel 4.4 BSP).
|
||||
# 2. Tries to install librockchip-mpp0 and gstreamer1.0-rockchip-mpp.
|
||||
# If they are not in apt, it prints instructions for manual installation.
|
||||
# 2. Installs librockchip-mpp and the gst-mpp GStreamer plugin, in order:
|
||||
# a) If a 'mpp-libs/' subdirectory is present next to this script,
|
||||
# copies the pre-built .so files directly (no network required).
|
||||
# b) Otherwise tries apt-get.
|
||||
# c) If apt fails, prints manual installation instructions.
|
||||
#
|
||||
# Requirements:
|
||||
# - Must be run as root or via sudo.
|
||||
@@ -67,7 +70,22 @@ info "Verify with: ls -la /dev/vpu_service (should show crw-rw---- root video)"
|
||||
|
||||
MPP_PKGS="librockchip-mpp0 gstreamer1.0-rockchip-mpp"
|
||||
|
||||
if apt-cache show librockchip-mpp0 &>/dev/null; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MPP_LIBS_DIR="$SCRIPT_DIR/mpp-libs"
|
||||
|
||||
if [[ -d "$MPP_LIBS_DIR" ]]; then
|
||||
info "Found pre-built libraries in $MPP_LIBS_DIR — copying..."
|
||||
# Core MPP runtime
|
||||
for f in "$MPP_LIBS_DIR"/librockchip_mpp.so* "$MPP_LIBS_DIR"/librockchip_vpu.so*; do
|
||||
[[ -e "$f" ]] && cp -v "$f" /usr/lib/aarch64-linux-gnu/
|
||||
done
|
||||
# GStreamer plugin
|
||||
if [[ -f "$MPP_LIBS_DIR/libgstrockchipmpp.so" ]]; then
|
||||
cp -v "$MPP_LIBS_DIR/libgstrockchipmpp.so" /usr/lib/aarch64-linux-gnu/gstreamer-1.0/
|
||||
fi
|
||||
ldconfig
|
||||
info "MPP libraries installed from $MPP_LIBS_DIR."
|
||||
elif apt-cache show librockchip-mpp0 &>/dev/null; then
|
||||
info "Found librockchip-mpp0 in apt — installing..."
|
||||
apt-get install -y $MPP_PKGS
|
||||
info "MPP packages installed successfully."
|
||||
|
||||
@@ -50,10 +50,11 @@ Milestone 3 — SDL Video Viewport, HUD, and Wayland Compatibility
|
||||
|
||||
- Verify that the SDL-texture playback path is smooth enough on real host playback and on R36S hardware
|
||||
- Measure whether BGRA frame upload is acceptable on RK3326 or whether a future YUV texture path is needed
|
||||
- Validate real video playback on the physical R36S after adding the missing H.264 and AAC decoder plugins to the device conda env
|
||||
- Device deployment on the physical R36S is now wired through ArkOS `Ports -> MatHacks`, with the heavy runtime under `/home/ark` and only a lightweight stub launcher under `/roms/ports`
|
||||
- Device env bootstrap on the physical R36S reaches a clean `from r36s_dlna_browser.app import Application` inside `/home/ark/miniconda3/envs/r36s-dlna-browser`
|
||||
- ArkOS launcher asset added at `deploy/arkos/MatHacks.sh`; current launcher uses the `/home/ark/R36SHack` checkout plus verified `LD_LIBRARY_PATH`, `GST_PLUGIN_PATH`, and `LD_PRELOAD` exports needed to load the system `gstreamer1.0-libav` plugins from the conda runtime
|
||||
- **Rockchip MPP hardware decode now deployed**: `librockchip-mpp` and `gst-mpp` compiled from source via Docker QEMU (arm64v8/ubuntu:focal), installed on device, `mppvideodec` confirmed visible to GStreamer. The `gstreamer_backend.py` probe auto-boosts `mppvideodec` rank if `/dev/vpu_service` is accessible.
|
||||
- **Pre-built .so files bundled** in `deploy/arkos/mpp-libs/`; `setup_hw_decode.sh` installs them automatically without network access.
|
||||
|
||||
## Blockers Or Open Questions
|
||||
|
||||
@@ -76,9 +77,8 @@ Milestone 3 — SDL Video Viewport, HUD, and Wayland Compatibility
|
||||
|
||||
## Next Recommended Actions
|
||||
|
||||
1. Run a visual playback smoke test and confirm SDL-rendered video plus HUD eliminates flashing on host playback.
|
||||
2. Validate the SDL-texture playback path on the target R36S `kmsdrm` backend.
|
||||
3. Measure CPU/load on RK3326 hardware during audio and video playback.
|
||||
4. Test real MiniDLNA `.mkv` H.264/AAC media end-to-end from the ArkOS `Ports -> MatHacks` launcher using `deploy/run.sh`.
|
||||
5. If RGBA upload cost is too high, add a follow-up YUV texture upload path using `SDL_UpdateYUVTexture`.
|
||||
6. `avdec_hevc` is still missing (HEVC decoders not in system apt `gstreamer1.0-libav 1.16.1`); AV1 excluded intentionally as too heavy for RK3326 — this is acceptable for the H.264/AAC primary codec target.
|
||||
1. Run a real H.264 video end-to-end via app and observe whether `mppvideodec` is selected (logs will show "HW decode: boosted rank of mppvideodec") and whether playback is smooth.
|
||||
2. Measure CPU/load on RK3326 hardware during HW-accelerated video playback vs. `avdec_h264` software path.
|
||||
3. Run a visual playback smoke test and confirm SDL-rendered video plus HUD eliminates flashing on host playback.
|
||||
4. If RGBA upload cost is too high, add a follow-up YUV texture upload path using `SDL_UpdateYUVTexture`.
|
||||
5. `avdec_hevc` is still missing (HEVC decoders not in system apt `gstreamer1.0-libav 1.16.1`); `mppvideodec` covers H.264/H.265/VP8/VP9 via HW so this is less critical now.
|
||||
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Video playback diagnostic for R36S / ArkOS.
|
||||
|
||||
Tests GStreamer availability, codec coverage, and a short live-playback loop
|
||||
using the same pipeline the app uses (playbin → appsink, BGRA frames).
|
||||
|
||||
Run directly on the device:
|
||||
/home/ark/miniconda3/envs/r36s-dlna-browser/bin/python \
|
||||
/home/ark/R36SHack/tests/test_video_playback_device.py
|
||||
|
||||
Accepts an optional URL/path to test real playback:
|
||||
... test_video_playback_device.py http://server/video.mkv
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import textwrap
|
||||
|
||||
# ── pretty output helpers ───────────────────────────────────────────────────
|
||||
|
||||
def _ok(msg: str) -> None:
|
||||
print(f" [OK] {msg}")
|
||||
|
||||
def _warn(msg: str) -> None:
|
||||
print(f" [WRN] {msg}")
|
||||
|
||||
def _fail(msg: str) -> None:
|
||||
print(f" [ERR] {msg}")
|
||||
|
||||
def _section(title: str) -> None:
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {title}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
# ── 1. Python env ───────────────────────────────────────────────────────────
|
||||
|
||||
_section("1. Python environment")
|
||||
import platform
|
||||
print(f" Python {sys.version}")
|
||||
print(f" Platform: {platform.machine()} / {platform.system()}")
|
||||
|
||||
|
||||
# ── 2. GI / GStreamer core ──────────────────────────────────────────────────
|
||||
|
||||
_section("2. GStreamer core")
|
||||
|
||||
try:
|
||||
import gi
|
||||
_ok(f"PyGObject (gi) {gi.__version__}")
|
||||
except ImportError as exc:
|
||||
_fail(f"PyGObject not found: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
gi.require_version("Gst", "1.0")
|
||||
from gi.repository import Gst
|
||||
Gst.init(None)
|
||||
v = Gst.version()
|
||||
_ok(f"GStreamer {v.major}.{v.minor}.{v.micro}.{v.nano}")
|
||||
except Exception as exc:
|
||||
_fail(f"GStreamer init failed: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
gi.require_version("GstApp", "1.0")
|
||||
gi.require_version("GstVideo", "1.0")
|
||||
from gi.repository import GstApp, GstVideo
|
||||
_ok("GstApp + GstVideo bindings present")
|
||||
except Exception as exc:
|
||||
_fail(f"GstApp/GstVideo bindings missing: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ── 3. Plugin registry ──────────────────────────────────────────────────────
|
||||
|
||||
_section("3. Plugin registry")
|
||||
|
||||
REQUIRED_ELEMENTS = {
|
||||
"playbin": "core orchestrator",
|
||||
"appsink": "app-side frame sink",
|
||||
"videoconvert": "pixel format conversion",
|
||||
"videoscale": "frame scaling",
|
||||
"typefind": "format detection",
|
||||
"decodebin": "auto demux/decode",
|
||||
"uridecodebin": "URI decode helper",
|
||||
}
|
||||
|
||||
CODEC_ELEMENTS = {
|
||||
# demuxers
|
||||
"matroskademux": "MKV/WebM demuxer",
|
||||
"qtdemux": "MP4/MOV demuxer",
|
||||
"flvdemux": "FLV demuxer",
|
||||
"tsdemux": "MPEG-TS demuxer",
|
||||
# video decoders
|
||||
"avdec_h264": "H.264 software decoder (libav)",
|
||||
"avdec_hevc": "H.265/HEVC software decoder (libav)",
|
||||
"avdec_mpeg2video": "MPEG-2 video decoder",
|
||||
"avdec_vp8": "VP8 decoder",
|
||||
"avdec_vp9": "VP9 decoder",
|
||||
"v4l2h264dec": "H.264 V4L2 HW decoder",
|
||||
"v4l2h265dec": "H.265 V4L2 HW decoder",
|
||||
# audio decoders
|
||||
"avdec_aac": "AAC decoder (libav)",
|
||||
"avdec_mp3": "MP3 decoder (libav)",
|
||||
"avdec_ac3": "AC-3 decoder (libav)",
|
||||
"vorbisdec": "Vorbis decoder",
|
||||
"opusdec": "Opus decoder",
|
||||
# audio output
|
||||
"autoaudiosink": "Auto audio sink",
|
||||
"alsasink": "ALSA sink",
|
||||
"pulsesink": "PulseAudio sink",
|
||||
}
|
||||
|
||||
missing_required = []
|
||||
registry = Gst.Registry.get()
|
||||
|
||||
for element, desc in REQUIRED_ELEMENTS.items():
|
||||
feat = registry.find_feature(element, Gst.ElementFactory.__gtype__)
|
||||
if feat:
|
||||
_ok(f"{element:20s} ({desc})")
|
||||
else:
|
||||
missing_required.append(element)
|
||||
_fail(f"{element:20s} ({desc}) ← MISSING")
|
||||
|
||||
print()
|
||||
|
||||
missing_codecs = []
|
||||
present_codecs = []
|
||||
for element, desc in CODEC_ELEMENTS.items():
|
||||
feat = registry.find_feature(element, Gst.ElementFactory.__gtype__)
|
||||
if feat:
|
||||
present_codecs.append(element)
|
||||
_ok(f"{element:20s} ({desc})")
|
||||
else:
|
||||
missing_codecs.append(element)
|
||||
_warn(f"{element:20s} ({desc}) ← not found")
|
||||
|
||||
if missing_required:
|
||||
print(f"\n CRITICAL: {len(missing_required)} required element(s) missing: {missing_required}")
|
||||
|
||||
print(f"\n Codecs present: {len(present_codecs)} / {len(CODEC_ELEMENTS)}")
|
||||
|
||||
|
||||
# ── 4. Caps negotiation: can we build a BGRA appsink pipeline? ──────────────
|
||||
|
||||
_section("4. BGRA appsink pipeline negotiation")
|
||||
|
||||
TEST_PIPE = (
|
||||
"videotestsrc num-buffers=5 ! "
|
||||
"videoconvert ! "
|
||||
"video/x-raw,format=BGRA,width=64,height=64 ! "
|
||||
"appsink name=sink emit-signals=true max-buffers=1 drop=true"
|
||||
)
|
||||
|
||||
frames_received = 0
|
||||
negotiation_ok = False
|
||||
|
||||
try:
|
||||
pipe = Gst.parse_launch(TEST_PIPE)
|
||||
sink = pipe.get_by_name("sink")
|
||||
|
||||
def _on_sample(sink, *_):
|
||||
global frames_received
|
||||
sample = sink.emit("pull-sample")
|
||||
if sample:
|
||||
frames_received += 1
|
||||
return Gst.FlowReturn.OK
|
||||
|
||||
sink.connect("new-sample", _on_sample)
|
||||
pipe.set_state(Gst.State.PLAYING)
|
||||
time.sleep(1.5)
|
||||
pipe.set_state(Gst.State.NULL)
|
||||
|
||||
if frames_received > 0:
|
||||
negotiation_ok = True
|
||||
_ok(f"BGRA appsink pipeline: received {frames_received} frame(s)")
|
||||
else:
|
||||
_fail("BGRA appsink pipeline ran but produced 0 frames")
|
||||
|
||||
except Exception as exc:
|
||||
_fail(f"Could not build BGRA appsink pipeline: {exc}")
|
||||
|
||||
|
||||
# ── 5. Audio output probe ───────────────────────────────────────────────────
|
||||
|
||||
_section("5. Audio output probe")
|
||||
|
||||
AUDIO_PIPE = "audiotestsrc num-buffers=10 freq=440 ! autoaudiosink"
|
||||
audio_ok = False
|
||||
try:
|
||||
p = Gst.parse_launch(AUDIO_PIPE)
|
||||
p.set_state(Gst.State.PLAYING)
|
||||
time.sleep(0.8)
|
||||
p.set_state(Gst.State.NULL)
|
||||
audio_ok = True
|
||||
_ok("autoaudiosink: played 440 Hz tone without error")
|
||||
except Exception as exc:
|
||||
_fail(f"autoaudiosink failed: {exc}")
|
||||
|
||||
if not audio_ok:
|
||||
ALSA_PIPE = "audiotestsrc num-buffers=10 freq=440 ! alsasink device=hw:0"
|
||||
try:
|
||||
p = Gst.parse_launch(ALSA_PIPE)
|
||||
p.set_state(Gst.State.PLAYING)
|
||||
time.sleep(0.8)
|
||||
p.set_state(Gst.State.NULL)
|
||||
audio_ok = True
|
||||
_ok("alsasink hw:0: played 440 Hz tone")
|
||||
except Exception as exc:
|
||||
_warn(f"alsasink also failed: {exc}")
|
||||
|
||||
|
||||
# ── 6. Live URL playback (optional) ────────────────────────────────────────
|
||||
|
||||
test_url = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
_section("6. Live URL / file playback")
|
||||
|
||||
if not test_url:
|
||||
_warn("No URL provided — skipping live playback test.")
|
||||
print(" Pass a media URL or path as the first argument to test real decode.")
|
||||
else:
|
||||
print(f" URL: {test_url}")
|
||||
live_frames = 0
|
||||
live_error = None
|
||||
|
||||
LIVE_PIPE = (
|
||||
f"playbin uri=\"{test_url}\" "
|
||||
f"video-sink=\"videoconvert ! video/x-raw,format=BGRA ! appsink name=vsink emit-signals=true max-buffers=2 drop=true\""
|
||||
)
|
||||
|
||||
try:
|
||||
pipe = Gst.parse_launch(LIVE_PIPE)
|
||||
vsink = pipe.get_by_name("vsink")
|
||||
|
||||
def _on_live_sample(sink, *_):
|
||||
global live_frames
|
||||
sample = sink.emit("pull-sample")
|
||||
if sample:
|
||||
buf = sample.get_buffer()
|
||||
info = buf.map(Gst.MapFlags.READ)
|
||||
if info.size > 0:
|
||||
live_frames += 1
|
||||
buf.unmap(info)
|
||||
return Gst.FlowReturn.OK
|
||||
|
||||
vsink.connect("new-sample", _on_live_sample)
|
||||
|
||||
bus = pipe.get_bus()
|
||||
pipe.set_state(Gst.State.PLAYING)
|
||||
|
||||
deadline = time.monotonic() + 15
|
||||
while time.monotonic() < deadline:
|
||||
msg = bus.timed_pop_filtered(
|
||||
200 * Gst.MSECOND,
|
||||
Gst.MessageType.ERROR | Gst.MessageType.WARNING | Gst.MessageType.EOS,
|
||||
)
|
||||
if msg:
|
||||
if msg.type == Gst.MessageType.ERROR:
|
||||
err, debug = msg.parse_error()
|
||||
live_error = f"{err.message} | debug: {debug}"
|
||||
break
|
||||
if msg.type == Gst.MessageType.WARNING:
|
||||
w, d = msg.parse_warning()
|
||||
_warn(f"GStreamer warning: {w.message}")
|
||||
if msg.type == Gst.MessageType.EOS:
|
||||
break
|
||||
if live_frames >= 10:
|
||||
break
|
||||
|
||||
pipe.set_state(Gst.State.NULL)
|
||||
|
||||
if live_error:
|
||||
_fail(f"Playback error: {live_error}")
|
||||
elif live_frames == 0:
|
||||
_fail("Playback ran but decoded 0 video frames (audio-only or decode failure)")
|
||||
else:
|
||||
_ok(f"Decoded {live_frames} video frame(s) successfully")
|
||||
|
||||
except Exception as exc:
|
||||
_fail(f"Could not start live playback pipeline: {exc}")
|
||||
|
||||
|
||||
# ── 7. Summary ─────────────────────────────────────────────────────────────
|
||||
|
||||
_section("7. Summary")
|
||||
|
||||
issues = []
|
||||
if missing_required:
|
||||
issues.append(f"Missing required elements: {', '.join(missing_required)}")
|
||||
if not negotiation_ok:
|
||||
issues.append("BGRA appsink pipeline negotiation failed")
|
||||
if not audio_ok:
|
||||
issues.append("No working audio sink found")
|
||||
|
||||
key_missing = [e for e in ("avdec_h264", "avdec_aac", "matroskademux", "qtdemux") if e in missing_codecs]
|
||||
if key_missing:
|
||||
issues.append(f"Key codecs missing (install gst-libav): {', '.join(key_missing)}")
|
||||
|
||||
if not issues:
|
||||
print(" All checks passed — video playback should work.")
|
||||
else:
|
||||
print(" Issues found:")
|
||||
for issue in issues:
|
||||
print(f" • {issue}")
|
||||
print()
|
||||
print(" Suggested fix:")
|
||||
print(textwrap.dedent("""\
|
||||
/home/ark/miniconda3/bin/conda install -n r36s-dlna-browser \\
|
||||
-c conda-forge gst-libav gst-plugins-good gst-plugins-bad gst-plugins-ugly
|
||||
"""))
|
||||
|
||||
print()
|
||||
Reference in New Issue
Block a user