diff --git a/deploy/arkos/mpp-libs/README.md b/deploy/arkos/mpp-libs/README.md new file mode 100644 index 0000000..afda435 --- /dev/null +++ b/deploy/arkos/mpp-libs/README.md @@ -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) diff --git a/deploy/arkos/mpp-libs/libgstrockchipmpp.so b/deploy/arkos/mpp-libs/libgstrockchipmpp.so new file mode 100755 index 0000000..fa3b654 Binary files /dev/null and b/deploy/arkos/mpp-libs/libgstrockchipmpp.so differ diff --git a/deploy/arkos/mpp-libs/librockchip_mpp.so b/deploy/arkos/mpp-libs/librockchip_mpp.so new file mode 100644 index 0000000..9b022c3 Binary files /dev/null and b/deploy/arkos/mpp-libs/librockchip_mpp.so differ diff --git a/deploy/arkos/mpp-libs/librockchip_mpp.so.0 b/deploy/arkos/mpp-libs/librockchip_mpp.so.0 new file mode 100644 index 0000000..9b022c3 Binary files /dev/null and b/deploy/arkos/mpp-libs/librockchip_mpp.so.0 differ diff --git a/deploy/arkos/mpp-libs/librockchip_mpp.so.1 b/deploy/arkos/mpp-libs/librockchip_mpp.so.1 new file mode 100644 index 0000000..9b022c3 Binary files /dev/null and b/deploy/arkos/mpp-libs/librockchip_mpp.so.1 differ diff --git a/deploy/arkos/mpp-libs/librockchip_vpu.so b/deploy/arkos/mpp-libs/librockchip_vpu.so new file mode 100644 index 0000000..97e0b4e Binary files /dev/null and b/deploy/arkos/mpp-libs/librockchip_vpu.so differ diff --git a/deploy/arkos/mpp-libs/librockchip_vpu.so.0 b/deploy/arkos/mpp-libs/librockchip_vpu.so.0 new file mode 100644 index 0000000..97e0b4e Binary files /dev/null and b/deploy/arkos/mpp-libs/librockchip_vpu.so.0 differ diff --git a/deploy/arkos/mpp-libs/librockchip_vpu.so.1 b/deploy/arkos/mpp-libs/librockchip_vpu.so.1 new file mode 100644 index 0000000..97e0b4e Binary files /dev/null and b/deploy/arkos/mpp-libs/librockchip_vpu.so.1 differ diff --git a/deploy/arkos/setup_hw_decode.sh b/deploy/arkos/setup_hw_decode.sh index da9101f..e26d142 100644 --- a/deploy/arkos/setup_hw_decode.sh +++ b/deploy/arkos/setup_hw_decode.sh @@ -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." diff --git a/docs/development-status.md b/docs/development-status.md index de7e714..9bdfd94 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/tests/test_video_playback_device.py b/tests/test_video_playback_device.py new file mode 100644 index 0000000..76ce385 --- /dev/null +++ b/tests/test_video_playback_device.py @@ -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()