diff --git a/.gitmodules b/.gitmodules
index dce0c5a..b6aa044 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "vendor/libdatachannel"]
path = vendor/libdatachannel
url = https://github.com/paullouisageneau/libdatachannel.git
+[submodule "vendor/mpp"]
+ path = vendor/mpp
+ url = https://github.com/rockchip-linux/mpp.git
diff --git a/README.md b/README.md
index 1aa879a..fe7ad6c 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,8 @@
GreenOvercast is a native Xbox Cloud Gaming client for small ARM64 Linux
-handhelds. It streams at 720p and uses the H700 video decoder.
+handhelds. It streams at 720p and uses hardware video decoding on supported
+H700 and Rockchip firmware.
Built for the Anbernic RG35XX-H (muOS) and validated on the RG40XX-H (Knulli).
@@ -24,7 +25,7 @@ The first launch shows a Microsoft device code. Open
[microsoft.com/link](https://www.microsoft.com/link) on another device and enter
the code to sign in. Knulli asks you to sign in again after a reboot.
-GreenOvercast is experimental and has only been tested on the devices below.
+GreenOvercast is experimental.
## Controls
@@ -54,8 +55,9 @@ support 16:9.
| RG35XX-H | muOS 2508.4 | Tested |
| RG40XX-H | Knulli (Batocera 42) | Tested |
-Hardware decoding currently only works on H700 devices. Other devices fall
-back to software decoding, which is too slow for normal gameplay.
+Rockchip MPP decoding also works on ROCKNIX, but support depends on the device
+and firmware. Other devices fall back to software decoding, which is too slow
+for normal gameplay.
The release requires glibc 2.38 or newer. ArkOS ships glibc 2.30 and is not
supported.
diff --git a/THIRDPARTY.md b/THIRDPARTY.md
index 6f2fdda..c7f2112 100644
--- a/THIRDPARTY.md
+++ b/THIRDPARTY.md
@@ -13,6 +13,7 @@ release. Full license texts for the bundled libraries ship alongside the port in
| [nlohmann/json](https://github.com/nlohmann/json) | MIT | JSON support used by libdatachannel (header-only) |
| [plog](https://github.com/SergiusTheBest/plog) | MIT | Logging used by libdatachannel (header-only) |
| [CedarX](https://github.com/allwinner-zh/media-codec) (Allwinner H.264 decoder) | LGPLv2.1-or-later | Hardware H.264 decode (shared) |
+| [Rockchip MPP](https://github.com/rockchip-linux/mpp) (v1.1.0) | Apache-2.0/MIT | Optional Rockchip H.264 decode (shared) |
| [FFmpeg](https://ffmpeg.org/) (libavcodec, libavutil, libswscale) | LGPLv2.1-or-later | Software H.264 fallback and color conversion (shared) |
| [SDL2](https://www.libsdl.org/) | Zlib | Window, renderer, audio, and controller input (device) |
| [OpenSSL](https://www.openssl.org/) | Apache-2.0 | TLS, WebRTC DTLS, and credential encryption (static) |
diff --git a/build.zig b/build.zig
index e605d27..a4ac410 100644
--- a/build.zig
+++ b/build.zig
@@ -1,29 +1,78 @@
const std = @import("std");
-// H700 target profile: both devices run vendor kernel 4.9.170 with glibc
-// 2.38 (muOS) / 2.40 (Knulli). Targeting the 2.38 ceiling covers both.
-const h700_query: std.Target.Query = .{
+// glibc 2.38 is the oldest userspace supported by the packaged release.
+const aarch64_linux_query: std.Target.Query = .{
.cpu_arch = .aarch64,
- .cpu_model = .{ .explicit = &std.Target.aarch64.cpu.cortex_a53 },
.os_tag = .linux,
.abi = .gnu,
.glibc_version = .{ .major = 2, .minor = 38, .patch = 0 },
};
+const c_test_flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror" };
+
+fn addHostZigObject(b: *std.Build, name: []const u8, source: []const u8) *std.Build.Step.Compile {
+ const module = b.createModule(.{
+ .root_source_file = b.path(source),
+ .target = b.graph.host,
+ .optimize = .Debug,
+ .link_libc = true,
+ });
+ module.addIncludePath(b.path("src/media/video"));
+ return b.addObject(.{ .name = name, .root_module = module });
+}
+
+fn addHostCExecutable(
+ b: *std.Build,
+ name: []const u8,
+ sources: []const []const u8,
+ objects: []const *std.Build.Step.Compile,
+ link_dl: bool,
+) *std.Build.Step.Compile {
+ const executable = b.addExecutable(.{
+ .name = name,
+ .root_source_file = null,
+ .target = b.graph.host,
+ .optimize = .Debug,
+ });
+ executable.addIncludePath(b.path("src/media/video"));
+ executable.addCSourceFiles(.{ .files = sources, .flags = c_test_flags });
+ for (objects) |object| executable.addObject(object);
+ executable.linkLibC();
+ if (link_dl) executable.linkSystemLibrary("dl");
+ return executable;
+}
+
+fn addHostCFakeLibrary(
+ b: *std.Build,
+ name: []const u8,
+ source: []const u8,
+) *std.Build.Step.Compile {
+ const library = b.addSharedLibrary(.{
+ .name = name,
+ .root_source_file = null,
+ .target = b.graph.host,
+ .optimize = .Debug,
+ });
+ library.addIncludePath(b.path("src/media/video"));
+ library.addCSourceFile(.{ .file = b.path(source), .flags = c_test_flags });
+ library.linkLibC();
+ return library;
+}
+
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe });
- const h700 = b.resolveTargetQuery(h700_query);
+ const aarch64_linux = b.resolveTargetQuery(aarch64_linux_query);
const stat_compat = b.createModule(.{
.root_source_file = b.path("src/platform/linux/stat_compat.zig"),
- .target = h700,
+ .target = aarch64_linux,
.optimize = optimize,
});
const smoke = b.addExecutable(.{
.name = "greenovercast-smoke",
.root_source_file = b.path("src/smoke/abi_smoke.zig"),
- .target = h700,
+ .target = aarch64_linux,
.optimize = optimize,
.link_libc = true,
});
@@ -46,6 +95,9 @@ pub fn build(b: *std.Build) void {
.{ .name = "greenovercast-cloud", .path = "src/session/cloud_session.zig" },
.{ .name = "greenovercast-controller", .path = "src/input/controller.zig" },
.{ .name = "greenovercast-ui", .path = "src/ui/handheld_ui.zig" },
+ .{ .name = "greenovercast-video-decoder", .path = "src/media/video/video_decoder.zig" },
+ .{ .name = "greenovercast-video-decoder-selection", .path = "src/media/video/video_decoder_selection.zig" },
+ .{ .name = "greenovercast-video-frame-copy", .path = "src/media/video/video_frame_copy.zig" },
};
const product_include_paths = [_][]const u8{
"vendor/headers",
@@ -63,7 +115,7 @@ pub fn build(b: *std.Build) void {
for (product_roots) |root| {
const module = b.createModule(.{
.root_source_file = b.path(root.path),
- .target = h700,
+ .target = aarch64_linux,
.optimize = .ReleaseSafe,
.link_libc = true,
});
@@ -97,4 +149,95 @@ pub fn build(b: *std.Build) void {
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
+
+ const video_decoder_object =
+ addHostZigObject(b, "video-decoder", "src/media/video/video_decoder.zig");
+ const video_frame_copy_object =
+ addHostZigObject(b, "video-frame-copy", "src/media/video/video_frame_copy.zig");
+ const video_decoder_selection_object = addHostZigObject(
+ b,
+ "video-decoder-selection",
+ "src/media/video/video_decoder_selection.zig",
+ );
+
+ const decoder_contract_test = addHostCExecutable(
+ b,
+ "video-decoder-test",
+ &.{"tests/video_decoder_test.c"},
+ &.{video_decoder_object},
+ false,
+ );
+ test_step.dependOn(&b.addRunArtifact(decoder_contract_test).step);
+
+ const decoder_selection_test = addHostCExecutable(
+ b,
+ "video-decoder-selection-test",
+ &.{"tests/video_decoder_selection_test.c"},
+ &.{ video_decoder_object, video_decoder_selection_object },
+ false,
+ );
+ test_step.dependOn(&b.addRunArtifact(decoder_selection_test).step);
+
+ const frame_copy_test = addHostCExecutable(
+ b,
+ "video-frame-copy-test",
+ &.{"tests/video_frame_copy_test.c"},
+ &.{video_frame_copy_object},
+ false,
+ );
+ test_step.dependOn(&b.addRunArtifact(frame_copy_test).step);
+
+ const fake_cedar_valid =
+ addHostCFakeLibrary(b, "fake-cedar-valid", "tests/cedar_fake_valid.c");
+ const cedar_decoder_test = addHostCExecutable(
+ b,
+ "video-decoder-cedar-test",
+ &.{
+ "src/media/video/cedar_loader.c",
+ "src/media/video/video_decoder_cedar.c",
+ "tests/video_decoder_cedar_test.c",
+ },
+ &.{video_decoder_object},
+ true,
+ );
+ const run_cedar_decoder_test = b.addRunArtifact(cedar_decoder_test);
+ run_cedar_decoder_test.addArtifactArg(fake_cedar_valid);
+ test_step.dependOn(&run_cedar_decoder_test.step);
+
+ const fake_mpp_valid = addHostCFakeLibrary(b, "fake-mpp-valid", "tests/mpp_fake_valid.c");
+ const fake_mpp_wrong_abi =
+ addHostCFakeLibrary(b, "fake-mpp-wrong-abi", "tests/mpp_fake_wrong_abi.c");
+ const fake_mpp_missing_symbol =
+ addHostCFakeLibrary(b, "fake-mpp-missing-symbol", "tests/mpp_fake_missing_symbol.c");
+
+ const mpp_loader_test = addHostCExecutable(
+ b,
+ "mpp-loader-test",
+ &.{
+ "src/media/video/mpp_loader.c",
+ "tests/mpp_loader_test.c",
+ },
+ &.{},
+ true,
+ );
+ const run_mpp_loader_test = b.addRunArtifact(mpp_loader_test);
+ run_mpp_loader_test.addArtifactArg(fake_mpp_valid);
+ run_mpp_loader_test.addArtifactArg(fake_mpp_wrong_abi);
+ run_mpp_loader_test.addArtifactArg(fake_mpp_missing_symbol);
+ test_step.dependOn(&run_mpp_loader_test.step);
+
+ const mpp_decoder_test = addHostCExecutable(
+ b,
+ "video-decoder-mpp-test",
+ &.{
+ "src/media/video/mpp_loader.c",
+ "src/media/video/video_decoder_mpp.c",
+ "tests/video_decoder_mpp_test.c",
+ },
+ &.{video_decoder_object},
+ true,
+ );
+ const run_mpp_decoder_test = b.addRunArtifact(mpp_decoder_test);
+ run_mpp_decoder_test.addArtifactArg(fake_mpp_valid);
+ test_step.dependOn(&run_mpp_decoder_test.step);
}
diff --git a/packaging/portmaster/greenovercast/GreenOvercast.sh b/packaging/portmaster/greenovercast/GreenOvercast.sh
index 51450ae..a61335f 100644
--- a/packaging/portmaster/greenovercast/GreenOvercast.sh
+++ b/packaging/portmaster/greenovercast/GreenOvercast.sh
@@ -57,9 +57,26 @@ export GREENOVERCAST_TOKEN_KEY_FILE="$credential_key_file"
export GREENOVERCAST_H264_BOOTSTRAP_FILE="$video_bootstrap_file"
export GREENOVERCAST_CATALOG_FILE="$catalog_file"
export GREENOVERCAST_CEDAR_LIBRARY="$GAMEDIR/libgreenovercast-cedar.so"
+export GREENOVERCAST_MPP_LIBRARY="$GAMEDIR/libgreenovercast-mpp.so"
export LD_LIBRARY_PATH="$GAMEDIR:${LD_LIBRARY_PATH:-}"
export SDL_GAMECONTROLLERCONFIG="$sdl_controllerconfig"
+if [ -e /dev/vpu_service ] && { [ ! -r /dev/vpu_service ] || [ ! -w /dev/vpu_service ]; }; then
+ $ESUDO chgrp video /dev/vpu_service && $ESUDO chmod 660 /dev/vpu_service
+fi
+
+for compatible_file in /proc/device-tree/compatible /sys/firmware/devicetree/base/compatible; do
+ if [ -r "$compatible_file" ] && tr '\0' '\n' <"$compatible_file" | grep -q '^rockchip,'; then
+ $ESUDO chmod +x "$GAMEDIR/rockchip/greenovercast-mpp-probe.aarch64"
+ if ! GREENOVERCAST_MPP_LIBRARY="$GREENOVERCAST_MPP_LIBRARY" \
+ LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \
+ "$GAMEDIR/rockchip/greenovercast-mpp-probe.aarch64" >/dev/null 2>&1; then
+ export LD_LIBRARY_PATH="$GAMEDIR/rockchip:$LD_LIBRARY_PATH"
+ fi
+ break
+ fi
+done
+
if [ -S /var/run/pipewire-0 ]; then
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/var/run}"
export PIPEWIRE_RUNTIME_DIR="${PIPEWIRE_RUNTIME_DIR:-/var/run}"
diff --git a/src/app/release.zig b/src/app/release.zig
index e84a439..2f2f866 100644
--- a/src/app/release.zig
+++ b/src/app/release.zig
@@ -66,10 +66,23 @@ pub const Release = struct {
fn initializeMedia(self: *Release) bool {
const bootstrap_path = std.posix.getenv("GREENOVERCAST_H264_BOOTSTRAP_FILE");
- self.video = c.go_video_pipeline_create(
- c.go_sdl_platform_renderer(self.platform),
- if (bootstrap_path) |path| path.ptr else null,
- );
+ const decoder_value = std.posix.getenv("GREENOVERCAST_VIDEO_DECODER");
+ var decoder_preference: c.GoVideoDecoderPreference = c.GO_VIDEO_DECODER_PREFERENCE_AUTO;
+ if (c.go_video_decoder_preference_parse(
+ if (decoder_value) |value| value.ptr else null,
+ &decoder_preference,
+ ) != 0) {
+ std.debug.print("Invalid GREENOVERCAST_VIDEO_DECODER value\n", .{});
+ return false;
+ }
+ const config = c.GoVideoPipelineConfig{
+ .renderer = c.go_sdl_platform_renderer(self.platform),
+ .bootstrap_path = if (bootstrap_path) |path| path.ptr else null,
+ .max_width = 1280,
+ .max_height = 720,
+ .decoder_preference = decoder_preference,
+ };
+ self.video = c.go_video_pipeline_create(&config);
if (self.video == null or c.go_video_pipeline_start(self.video) < 0) {
std.debug.print("H.264 pipeline initialization failed\n", .{});
_ = c.go_video_pipeline_destroy(self.video);
@@ -379,6 +392,8 @@ pub const Release = struct {
std.debug.print("Cloud game ended\n", .{});
return .session_ended;
}
+ if (c.go_video_pipeline_failed(self.video) != 0)
+ return .failed;
c.go_webrtc_session_send_gamepad(self.webrtc);
c.go_video_pipeline_render(self.video);
@@ -417,8 +432,8 @@ pub const Release = struct {
std.debug.print(
"[{d}s] video_rtp={d} payload={d} rejected={d}/pt{d} aus={d} frames={d}/{d} " ++
"nals={d}/{d}/{d}/{d} ts={d} synced={d} gaps={d} missing={d} late_rtp={d} " ++
- "decode_errors={d}/{d}/{d} keyframes={d} queue={d}/{d} audio_rtp={d} decoded={d} " ++
- "dropped={d} late={d} pending={d} queued_ms={d} resets={d} keepalive={d}/{d}\n",
+ "decoder={d} init_failures={d} fallbacks={d} backpressure={d} corrupt={d} " ++
+ "info_changes={d} decode_errors={d}/{d}/{d} keyframes={d} queue={d}/{d}\n",
.{
elapsed_ms / 1000,
video.rtp_packets,
@@ -437,12 +452,25 @@ pub const Release = struct {
video.discontinuities,
video.missing_packets,
video.late_packets,
+ video.decoder_backend,
+ video.decoder_init_failures,
+ video.decoder_runtime_fallbacks,
+ video.decoder_backpressure_events,
+ video.decoder_corrupt_frames,
+ video.decoder_info_changes,
video.decoder_send_errors,
video.decoder_receive_errors,
video.last_decoder_error,
video.keyframe_requests,
video.pending_packets,
video.dropped_packets,
+ },
+ );
+ std.debug.print(
+ "[{d}s] audio_rtp={d} decoded={d} dropped={d} late={d} pending={d} " ++
+ "queued_ms={d} resets={d} keepalive={d}/{d}\n",
+ .{
+ elapsed_ms / 1000,
audio.rtp_packets,
audio.decoded_packets,
audio.dropped_packets,
diff --git a/src/media/rtp/h264_depacketizer.h b/src/media/rtp/h264_depacketizer.h
index 6dab85d..d87f484 100644
--- a/src/media/rtp/h264_depacketizer.h
+++ b/src/media/rtp/h264_depacketizer.h
@@ -40,9 +40,10 @@ int go_h264_depacketizer_set_bootstrap(GoH264Depacketizer* depacketizer, const u
size_t length);
int go_h264_depacketizer_take_bootstrap(GoH264Depacketizer* depacketizer, uint8_t* output,
size_t capacity, size_t* length);
-GoH264FeedResult go_h264_depacketizer_feed(GoH264Depacketizer* depacketizer,
- const uint8_t* packet, size_t length,
- GoH264AccessUnitCallback callback, void* context);
+void go_h264_depacketizer_restart_decode_epoch(GoH264Depacketizer* depacketizer);
+GoH264FeedResult go_h264_depacketizer_feed(GoH264Depacketizer* depacketizer, const uint8_t* packet,
+ size_t length, GoH264AccessUnitCallback callback,
+ void* context);
#ifdef __cplusplus
}
diff --git a/src/media/rtp/h264_depacketizer.zig b/src/media/rtp/h264_depacketizer.zig
index b331485..7eac1cc 100644
--- a/src/media/rtp/h264_depacketizer.zig
+++ b/src/media/rtp/h264_depacketizer.zig
@@ -132,6 +132,13 @@ pub const Depacketizer = struct {
return offset;
}
+ pub fn restartDecodeEpoch(self: *Depacketizer) void {
+ self.resetAccessUnit();
+ self.access_unit_timestamp = null;
+ self.decode_epoch_ready = false;
+ self.bootstrap_pending = self.have_sps and self.have_pps;
+ }
+
pub fn feed(
self: *Depacketizer,
packet: []const u8,
@@ -196,8 +203,8 @@ pub const Depacketizer = struct {
self.countNal(nal_type, result);
if ((nal_type == 7 or nal_type == 8) and
!self.observeParameterSet(nal_type, nal, result)) return;
- if ((nal_type == 1 or nal_type == 5) and !self.prependBootstrap(result)) return;
if (!self.shouldAppend(nal_type, result)) return;
+ if ((nal_type == 1 or nal_type == 5) and !self.prependBootstrap(result)) return;
if (!self.appendNal(nal)) self.discardAccessUnit(result);
}
@@ -245,9 +252,9 @@ pub const Depacketizer = struct {
if (is_start) {
if (self.fragment_state != .idle) self.discardAccessUnit(result);
self.countNal(nal_type, result);
- if ((nal_type == 1 or nal_type == 5) and !self.prependBootstrap(result)) {
+ if (!self.shouldAppend(nal_type, result)) {
self.fragment_state = .dropping;
- } else if (!self.shouldAppend(nal_type, result)) {
+ } else if ((nal_type == 1 or nal_type == 5) and !self.prependBootstrap(result)) {
self.fragment_state = .dropping;
} else {
const nal_header = (payload[0] & 0x60) | @as(u8, nal_type);
@@ -426,7 +433,7 @@ pub const Depacketizer = struct {
}
fn discardAccessUnit(self: *Depacketizer, result: *FeedResult) void {
- self.resetAccessUnit();
+ self.restartDecodeEpoch();
result.discontinuities += 1;
result.requires_keyframe = 1;
}
@@ -491,6 +498,12 @@ pub export fn go_h264_depacketizer_take_bootstrap(
return if (written > 0) 1 else 0;
}
+pub export fn go_h264_depacketizer_restart_decode_epoch(
+ depacketizer: ?*Depacketizer,
+) void {
+ if (depacketizer) |state| state.restartDecodeEpoch();
+}
+
pub export fn go_h264_depacketizer_feed(
depacketizer: ?*Depacketizer,
packet: ?[*]const u8,
@@ -698,3 +711,91 @@ test "invalid bootstrap does not replace the active parameter sets" {
try std.testing.expectEqualSlices(u8, &bootstrap, output[0..length]);
}
+
+test "decode epoch restart preserves parameter sets and waits for IDR" {
+ var depacketizer = Depacketizer.init(102);
+ try std.testing.expect(depacketizer.setBootstrap(&bootstrap));
+ var collector = TestCollector{};
+ const idr = [_]u8{ 0x65, 0xbb };
+ const slice = [_]u8{ 0x61, 0xaa };
+
+ const first_idr = makeRtp(1, 3600, true, &idr);
+ const first_slice = makeRtp(2, 3960, true, &slice);
+ _ = depacketizer.feed(first_idr[0 .. 12 + idr.len], collectAccessUnit, &collector);
+ _ = depacketizer.feed(first_slice[0 .. 12 + slice.len], collectAccessUnit, &collector);
+
+ depacketizer.restartDecodeEpoch();
+
+ const dropped_slice = makeRtp(3, 4320, true, &slice);
+ const dropped = depacketizer.feed(
+ dropped_slice[0 .. 12 + slice.len],
+ collectAccessUnit,
+ &collector,
+ );
+ try std.testing.expectEqual(@as(u32, 1), dropped.requires_keyframe);
+ try std.testing.expectEqual(@as(usize, 2), collector.calls);
+
+ const recovery_idr = makeRtp(4, 4680, true, &idr);
+ const recovered = depacketizer.feed(
+ recovery_idr[0 .. 12 + idr.len],
+ collectAccessUnit,
+ &collector,
+ );
+ try std.testing.expectEqual(@as(u32, 0), recovered.requires_keyframe);
+ try std.testing.expectEqual(@as(usize, 3), collector.calls);
+ try std.testing.expectEqualSlices(u8, &bootstrap, collector.data[0..bootstrap.len]);
+ try std.testing.expectEqualSlices(
+ u8,
+ &.{ 0, 0, 0, 1, 0x65, 0xbb },
+ collector.data[bootstrap.len..collector.data_length],
+ );
+
+ const recovered_slice = makeRtp(5, 5040, true, &slice);
+ _ = depacketizer.feed(
+ recovered_slice[0 .. 12 + slice.len],
+ collectAccessUnit,
+ &collector,
+ );
+ try std.testing.expectEqual(@as(usize, 4), collector.calls);
+ try std.testing.expectEqualSlices(
+ u8,
+ &.{ 0, 0, 0, 1, 0x61, 0xaa },
+ collector.data[0..collector.data_length],
+ );
+}
+
+test "decode epoch restart discards incomplete fragmentation" {
+ var depacketizer = Depacketizer.init(102);
+ try std.testing.expect(depacketizer.setBootstrap(&bootstrap));
+ var collector = TestCollector{};
+ const fragment_start = [_]u8{ 0x7c, 0x85, 0xde, 0xad };
+ const fragment_end = [_]u8{ 0x7c, 0x45, 0xbe, 0xef };
+ const first = makeRtp(1, 3600, false, &fragment_start);
+ const second = makeRtp(2, 3600, true, &fragment_end);
+
+ _ = depacketizer.feed(first[0 .. 12 + fragment_start.len], collectAccessUnit, &collector);
+ depacketizer.restartDecodeEpoch();
+ const result = depacketizer.feed(
+ second[0 .. 12 + fragment_end.len],
+ collectAccessUnit,
+ &collector,
+ );
+
+ try std.testing.expectEqual(@as(u32, 1), result.discontinuities);
+ try std.testing.expectEqual(@as(u32, 1), result.requires_keyframe);
+ try std.testing.expectEqual(@as(usize, 0), collector.calls);
+}
+
+test "decode epoch restart without parameter sets rejects IDR" {
+ var depacketizer = Depacketizer.init(102);
+ var collector = TestCollector{};
+ depacketizer.restartDecodeEpoch();
+ depacketizer.restartDecodeEpoch();
+ const idr = [_]u8{ 0x65, 0xbb };
+ const packet = makeRtp(1, 3600, true, &idr);
+
+ const result = depacketizer.feed(packet[0 .. 12 + idr.len], collectAccessUnit, &collector);
+
+ try std.testing.expectEqual(@as(u32, 1), result.requires_keyframe);
+ try std.testing.expectEqual(@as(usize, 0), collector.calls);
+}
diff --git a/src/media/video/mpp_bridge.c b/src/media/video/mpp_bridge.c
new file mode 100644
index 0000000..34c7bd9
--- /dev/null
+++ b/src/media/video/mpp_bridge.c
@@ -0,0 +1,302 @@
+#include "mpp_decoder.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define GO_MPP_FRAME_LIMIT 24
+
+static _Thread_local char creation_error[256];
+
+struct GoMppDecoder {
+ MppCtx context;
+ MppApi* api;
+ MppBufferGroup frame_group;
+ int initialized;
+ int max_width;
+ int max_height;
+ int width;
+ int height;
+ int horizontal_stride;
+ int vertical_stride;
+ size_t buffer_size;
+ char error[256];
+};
+
+static void set_error(GoMppDecoder* decoder, const char* message) {
+ snprintf(decoder->error, sizeof(decoder->error), "%s", message);
+}
+
+static void set_mpp_error(GoMppDecoder* decoder, const char* operation, MPP_RET result) {
+ snprintf(decoder->error, sizeof(decoder->error), "%s (MPP error %d)", operation, result);
+}
+
+static int valid_output(GoMppDecoder* decoder, MppFrame frame) {
+ RK_U32 width = mpp_frame_get_width(frame);
+ RK_U32 height = mpp_frame_get_height(frame);
+ RK_U32 horizontal_stride = mpp_frame_get_hor_stride(frame);
+ RK_U32 vertical_stride = mpp_frame_get_ver_stride(frame);
+ MppFrameFormat format = mpp_frame_get_fmt(frame);
+ if (width == 0 || height == 0 || width > (RK_U32)decoder->max_width ||
+ height > (RK_U32)decoder->max_height || horizontal_stride < width ||
+ vertical_stride < height || width > INT_MAX || height > INT_MAX ||
+ horizontal_stride > INT_MAX || vertical_stride > INT_MAX) {
+ set_error(decoder, "MPP returned invalid output dimensions");
+ return 0;
+ }
+ if (MPP_FRAME_FMT_IS_FBC(format) || MPP_FRAME_FMT_IS_TILE(format) ||
+ (format & MPP_FRAME_FMT_MASK) != MPP_FMT_YUV420SP) {
+ set_error(decoder, "MPP returned an unsupported output format");
+ return 0;
+ }
+ if ((size_t)horizontal_stride > SIZE_MAX / (size_t)vertical_stride) {
+ set_error(decoder, "MPP output stride calculation overflowed");
+ return 0;
+ }
+ size_t y_size = (size_t)horizontal_stride * (size_t)vertical_stride;
+ size_t uv_rows = ((size_t)height + 1u) / 2u;
+ if (uv_rows > (SIZE_MAX - y_size) / (size_t)horizontal_stride) {
+ set_error(decoder, "MPP output buffer calculation overflowed");
+ return 0;
+ }
+ size_t required = y_size + uv_rows * (size_t)horizontal_stride;
+ size_t buffer_size = mpp_frame_get_buf_size(frame);
+ if (buffer_size < required) {
+ set_error(decoder, "MPP output buffer is smaller than its visible frame");
+ return 0;
+ }
+ return 1;
+}
+
+static int handle_info_change(GoMppDecoder* decoder, MppFrame frame) {
+ if (!valid_output(decoder, frame))
+ return -1;
+ size_t buffer_size = mpp_frame_get_buf_size(frame);
+ if (!decoder->frame_group) {
+ MPP_RET result = mpp_buffer_group_get_internal(&decoder->frame_group, MPP_BUFFER_TYPE_DRM);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP frame-buffer group creation failed", result);
+ return -1;
+ }
+ } else {
+ MPP_RET result = mpp_buffer_group_clear(decoder->frame_group);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP frame-buffer group reset failed", result);
+ return -1;
+ }
+ }
+ MPP_RET result =
+ mpp_buffer_group_limit_config(decoder->frame_group, buffer_size, GO_MPP_FRAME_LIMIT);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP frame-buffer limit failed", result);
+ return -1;
+ }
+ result =
+ decoder->api->control(decoder->context, MPP_DEC_SET_EXT_BUF_GROUP, decoder->frame_group);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP frame-buffer group attachment failed", result);
+ return -1;
+ }
+ result = decoder->api->control(decoder->context, MPP_DEC_SET_INFO_CHANGE_READY, NULL);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP info-change acknowledgement failed", result);
+ return -1;
+ }
+
+ decoder->width = (int)mpp_frame_get_width(frame);
+ decoder->height = (int)mpp_frame_get_height(frame);
+ decoder->horizontal_stride = (int)mpp_frame_get_hor_stride(frame);
+ decoder->vertical_stride = (int)mpp_frame_get_ver_stride(frame);
+ decoder->buffer_size = buffer_size;
+ fprintf(stderr, "MPP output: NV12 %dx%d, stride %dx%d, pool limit %d\n", decoder->width,
+ decoder->height, decoder->horizontal_stride, decoder->vertical_stride,
+ GO_MPP_FRAME_LIMIT);
+ return 0;
+}
+
+uint32_t go_mpp_decoder_abi_version(void) {
+ return GO_MPP_DECODER_ABI_VERSION;
+}
+
+GoMppDecoder* go_mpp_decoder_create(int max_width, int max_height) {
+ creation_error[0] = 0;
+ if (max_width <= 0 || max_height <= 0 || max_width > 8192 || max_height > 8192) {
+ snprintf(creation_error, sizeof(creation_error), "invalid MPP decoder dimensions");
+ return NULL;
+ }
+ GoMppDecoder* decoder = calloc(1, sizeof(*decoder));
+ if (!decoder) {
+ snprintf(creation_error, sizeof(creation_error), "MPP decoder allocation failed");
+ return NULL;
+ }
+ decoder->max_width = max_width;
+ decoder->max_height = max_height;
+
+ MPP_RET result = mpp_check_support_format(MPP_CTX_DEC, MPP_VIDEO_CodingAVC);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP does not support H.264 decoding", result);
+ goto fail;
+ }
+ result = mpp_create(&decoder->context, &decoder->api);
+ if (result != MPP_OK || !decoder->context || !decoder->api) {
+ set_mpp_error(decoder, "MPP context creation failed", result);
+ goto fail;
+ }
+ MppPollType timeout = MPP_POLL_NON_BLOCK;
+ result = decoder->api->control(decoder->context, MPP_SET_INPUT_TIMEOUT, &timeout);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP input timeout configuration failed", result);
+ goto fail;
+ }
+ result = decoder->api->control(decoder->context, MPP_SET_OUTPUT_TIMEOUT, &timeout);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP output timeout configuration failed", result);
+ goto fail;
+ }
+ result = mpp_init(decoder->context, MPP_CTX_DEC, MPP_VIDEO_CodingAVC);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP H.264 initialization failed", result);
+ goto fail;
+ }
+ decoder->initialized = 1;
+ MppFrameFormat format = MPP_FMT_YUV420SP;
+ result = decoder->api->control(decoder->context, MPP_DEC_SET_OUTPUT_FORMAT, &format);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP NV12 output configuration failed", result);
+ goto fail;
+ }
+ return decoder;
+
+fail:
+ snprintf(creation_error, sizeof(creation_error), "%s",
+ decoder->error[0] ? decoder->error : "MPP H.264 initialization failed");
+ if (decoder->context) {
+ if (decoder->initialized && decoder->api)
+ decoder->api->reset(decoder->context);
+ mpp_destroy(decoder->context);
+ }
+ if (decoder->frame_group)
+ mpp_buffer_group_put(decoder->frame_group);
+ free(decoder);
+ return NULL;
+}
+
+int go_mpp_decoder_submit(GoMppDecoder* decoder, const uint8_t* data, size_t length) {
+ if (!decoder || !decoder->context || !decoder->api || !data || length == 0)
+ return -1;
+ MppPacket packet = NULL;
+ MPP_RET result = mpp_packet_init(&packet, (void*)data, length);
+ if (result != MPP_OK || !packet) {
+ set_mpp_error(decoder, "MPP packet creation failed", result);
+ return -1;
+ }
+ result = decoder->api->decode_put_packet(decoder->context, packet);
+ mpp_packet_deinit(&packet);
+ if (result == MPP_OK)
+ return 1;
+ if (result == MPP_ERR_TIMEOUT || result == MPP_ERR_BUFFER_FULL)
+ return 0;
+ set_mpp_error(decoder, "MPP packet submission failed", result);
+ return -1;
+}
+
+int go_mpp_decoder_receive(GoMppDecoder* decoder, GoMppFrame* output) {
+ if (!decoder || !decoder->context || !decoder->api || !output)
+ return -1;
+ memset(output, 0, sizeof(*output));
+ for (int attempt = 0; attempt < 8; ++attempt) {
+ MppFrame frame = NULL;
+ MPP_RET result = decoder->api->decode_get_frame(decoder->context, &frame);
+ if (result == MPP_ERR_TIMEOUT)
+ return 0;
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP frame receive failed", result);
+ return -1;
+ }
+ if (!frame)
+ return 0;
+ if (mpp_frame_get_info_change(frame)) {
+ int change_result = handle_info_change(decoder, frame);
+ mpp_frame_deinit(&frame);
+ if (change_result != 0)
+ return -1;
+ continue;
+ }
+ if (mpp_frame_get_errinfo(frame) || mpp_frame_get_discard(frame)) {
+ mpp_frame_deinit(&frame);
+ continue;
+ }
+ if (!valid_output(decoder, frame)) {
+ mpp_frame_deinit(&frame);
+ return -1;
+ }
+ MppBuffer buffer = mpp_frame_get_buffer(frame);
+ uint8_t* base = buffer ? mpp_buffer_get_ptr(buffer) : NULL;
+ size_t buffer_size = buffer ? mpp_buffer_get_size(buffer) : 0;
+ size_t uv_offset =
+ (size_t)mpp_frame_get_hor_stride(frame) * (size_t)mpp_frame_get_ver_stride(frame);
+ size_t uv_size = ((size_t)mpp_frame_get_height(frame) + 1u) / 2u *
+ (size_t)mpp_frame_get_hor_stride(frame);
+ if (!base || uv_size > SIZE_MAX - uv_offset || buffer_size < uv_offset + uv_size) {
+ set_error(decoder, "MPP output buffer is not CPU-readable");
+ mpp_frame_deinit(&frame);
+ return -1;
+ }
+ output->y = base;
+ output->uv = base + uv_offset;
+ output->width = (int)mpp_frame_get_width(frame);
+ output->height = (int)mpp_frame_get_height(frame);
+ output->y_stride = (int)mpp_frame_get_hor_stride(frame);
+ output->uv_stride = output->y_stride;
+ output->owner = frame;
+ return 1;
+ }
+ return 0;
+}
+
+void go_mpp_decoder_release_frame(GoMppDecoder* decoder, GoMppFrame* frame) {
+ (void)decoder;
+ if (!frame || !frame->owner)
+ return;
+ MppFrame owner = (MppFrame)frame->owner;
+ mpp_frame_deinit(&owner);
+ memset(frame, 0, sizeof(*frame));
+}
+
+int go_mpp_decoder_reset(GoMppDecoder* decoder) {
+ if (!decoder || !decoder->context || !decoder->api)
+ return -1;
+ MPP_RET result = decoder->api->reset(decoder->context);
+ if (result != MPP_OK) {
+ set_mpp_error(decoder, "MPP decoder reset failed", result);
+ return -1;
+ }
+ return 0;
+}
+
+const char* go_mpp_decoder_last_error(GoMppDecoder* decoder) {
+ if (decoder && decoder->error[0])
+ return decoder->error;
+ return creation_error[0] ? creation_error : "MPP decoder failure";
+}
+
+void go_mpp_decoder_destroy(GoMppDecoder* decoder) {
+ if (!decoder)
+ return;
+ if (decoder->context) {
+ if (decoder->api)
+ decoder->api->reset(decoder->context);
+ mpp_destroy(decoder->context);
+ }
+ if (decoder->frame_group)
+ mpp_buffer_group_put(decoder->frame_group);
+ free(decoder);
+}
diff --git a/src/media/video/mpp_decoder.h b/src/media/video/mpp_decoder.h
new file mode 100644
index 0000000..b2001ed
--- /dev/null
+++ b/src/media/video/mpp_decoder.h
@@ -0,0 +1,38 @@
+#ifndef GREENOVERCAST_MPP_DECODER_H
+#define GREENOVERCAST_MPP_DECODER_H
+
+#include
+#include
+
+#if defined(__GNUC__)
+#define GO_MPP_API __attribute__((visibility("default")))
+#else
+#define GO_MPP_API
+#endif
+
+#define GO_MPP_DECODER_ABI_VERSION 1u
+
+typedef struct GoMppDecoder GoMppDecoder;
+
+typedef struct {
+ const uint8_t* y;
+ const uint8_t* uv;
+ int width;
+ int height;
+ int y_stride;
+ int uv_stride;
+ void* owner;
+} GoMppFrame;
+
+GO_MPP_API uint32_t go_mpp_decoder_abi_version(void);
+GO_MPP_API GoMppDecoder* go_mpp_decoder_create(int max_width, int max_height);
+GO_MPP_API int go_mpp_decoder_submit(GoMppDecoder* decoder, const uint8_t* data, size_t length);
+/* A successful receive transfers frame->owner until release_frame is called. */
+GO_MPP_API int go_mpp_decoder_receive(GoMppDecoder* decoder, GoMppFrame* frame);
+GO_MPP_API void go_mpp_decoder_release_frame(GoMppDecoder* decoder, GoMppFrame* frame);
+GO_MPP_API int go_mpp_decoder_reset(GoMppDecoder* decoder);
+/* decoder may be NULL after create fails and must return that thread's creation error. */
+GO_MPP_API const char* go_mpp_decoder_last_error(GoMppDecoder* decoder);
+GO_MPP_API void go_mpp_decoder_destroy(GoMppDecoder* decoder);
+
+#endif
diff --git a/src/media/video/mpp_loader.c b/src/media/video/mpp_loader.c
new file mode 100644
index 0000000..e57c743
--- /dev/null
+++ b/src/media/video/mpp_loader.c
@@ -0,0 +1,155 @@
+#include "mpp_loader.h"
+
+#include
+#include
+#include
+#include
+
+typedef uint32_t (*AbiVersion)(void);
+typedef GoMppDecoder* (*CreateDecoder)(int max_width, int max_height);
+typedef int (*SubmitDecoder)(GoMppDecoder* decoder, const uint8_t* data, size_t length);
+typedef int (*ReceiveDecoder)(GoMppDecoder* decoder, GoMppFrame* frame);
+typedef void (*ReleaseFrame)(GoMppDecoder* decoder, GoMppFrame* frame);
+typedef int (*ResetDecoder)(GoMppDecoder* decoder);
+typedef const char* (*DecoderError)(GoMppDecoder* decoder);
+typedef void (*DestroyDecoder)(GoMppDecoder* decoder);
+
+struct GoMppLibrary {
+ void* handle;
+ GoMppDecoder* decoder;
+ SubmitDecoder submit;
+ ReceiveDecoder receive;
+ ReleaseFrame release_frame;
+ ResetDecoder reset;
+ DecoderError decoder_error;
+ DestroyDecoder destroy;
+ char path[512];
+ char error[256];
+};
+
+static void set_loader_error(GoMppLibrary* library, const char* message) {
+ snprintf(library->error, sizeof(library->error), "%s", message ? message : "unknown error");
+}
+
+static void* load_symbol(GoMppLibrary* library, const char* name) {
+ dlerror();
+ void* symbol = dlsym(library->handle, name);
+ const char* error = dlerror();
+ if (error || !symbol)
+ set_loader_error(library, error ? error : "MPP plugin symbol is missing");
+ return symbol;
+}
+
+GoMppLibrary* go_mpp_library_open(int max_width, int max_height) {
+ GoMppLibrary* library = calloc(1, sizeof(*library));
+ if (!library)
+ return NULL;
+
+ const char* path = getenv("GREENOVERCAST_MPP_LIBRARY");
+ if (!path || !path[0])
+ path = "libgreenovercast-mpp.so";
+ if (snprintf(library->path, sizeof(library->path), "%s", path) >= (int)sizeof(library->path)) {
+ set_loader_error(library, "MPP plugin path is too long");
+ return library;
+ }
+
+ int flags = RTLD_NOW | RTLD_LOCAL;
+#ifdef RTLD_NODELETE
+ /* Older Rockchip MPP builds can run process-lifetime cleanup after decoder teardown. */
+ flags |= RTLD_NODELETE;
+#endif
+ library->handle = dlopen(path, flags);
+ if (!library->handle) {
+ set_loader_error(library, dlerror());
+ return library;
+ }
+
+ AbiVersion abi_version = (AbiVersion)load_symbol(library, "go_mpp_decoder_abi_version");
+ if (!abi_version)
+ goto fail;
+ uint32_t version = abi_version();
+ if (version != GO_MPP_DECODER_ABI_VERSION) {
+ snprintf(library->error, sizeof(library->error),
+ "MPP plugin ABI %u is incompatible with required ABI %u", version,
+ GO_MPP_DECODER_ABI_VERSION);
+ goto fail;
+ }
+
+ CreateDecoder create = (CreateDecoder)load_symbol(library, "go_mpp_decoder_create");
+ library->submit = (SubmitDecoder)load_symbol(library, "go_mpp_decoder_submit");
+ library->receive = (ReceiveDecoder)load_symbol(library, "go_mpp_decoder_receive");
+ library->release_frame = (ReleaseFrame)load_symbol(library, "go_mpp_decoder_release_frame");
+ library->reset = (ResetDecoder)load_symbol(library, "go_mpp_decoder_reset");
+ library->decoder_error = (DecoderError)load_symbol(library, "go_mpp_decoder_last_error");
+ library->destroy = (DestroyDecoder)load_symbol(library, "go_mpp_decoder_destroy");
+ if (!create || !library->submit || !library->receive || !library->release_frame ||
+ !library->reset || !library->decoder_error || !library->destroy)
+ goto fail;
+
+ library->decoder = create(max_width, max_height);
+ if (!library->decoder) {
+ set_loader_error(library, library->decoder_error(NULL));
+ return library;
+ }
+ return library;
+
+fail:
+ dlclose(library->handle);
+ library->handle = NULL;
+ return library;
+}
+
+int go_mpp_library_ready(const GoMppLibrary* library) {
+ return library && library->decoder && library->submit && library->receive &&
+ library->release_frame && library->reset && library->decoder_error && library->destroy;
+}
+
+int go_mpp_library_submit(GoMppLibrary* library, const uint8_t* data, size_t length) {
+ if (!go_mpp_library_ready(library))
+ return -1;
+ int result = library->submit(library->decoder, data, length);
+ if (result < 0)
+ set_loader_error(library, library->decoder_error(library->decoder));
+ return result;
+}
+
+int go_mpp_library_receive(GoMppLibrary* library, GoMppFrame* frame) {
+ if (!go_mpp_library_ready(library))
+ return -1;
+ int result = library->receive(library->decoder, frame);
+ if (result < 0)
+ set_loader_error(library, library->decoder_error(library->decoder));
+ return result;
+}
+
+void go_mpp_library_release_frame(GoMppLibrary* library, GoMppFrame* frame) {
+ if (go_mpp_library_ready(library) && frame)
+ library->release_frame(library->decoder, frame);
+}
+
+int go_mpp_library_reset(GoMppLibrary* library) {
+ if (!go_mpp_library_ready(library))
+ return -1;
+ int result = library->reset(library->decoder);
+ if (result < 0)
+ set_loader_error(library, library->decoder_error(library->decoder));
+ return result;
+}
+
+const char* go_mpp_library_error(const GoMppLibrary* library) {
+ return library && library->error[0] ? library->error : "MPP decoder unavailable";
+}
+
+const char* go_mpp_library_path(const GoMppLibrary* library) {
+ return library && library->path[0] ? library->path : "libgreenovercast-mpp.so";
+}
+
+void go_mpp_library_close(GoMppLibrary* library) {
+ if (!library)
+ return;
+ if (library->decoder && library->destroy)
+ library->destroy(library->decoder);
+ if (library->handle)
+ dlclose(library->handle);
+ free(library);
+}
diff --git a/src/media/video/mpp_loader.h b/src/media/video/mpp_loader.h
new file mode 100644
index 0000000..1fdccc3
--- /dev/null
+++ b/src/media/video/mpp_loader.h
@@ -0,0 +1,18 @@
+#ifndef GREENOVERCAST_MPP_LOADER_H
+#define GREENOVERCAST_MPP_LOADER_H
+
+#include "mpp_decoder.h"
+
+typedef struct GoMppLibrary GoMppLibrary;
+
+GoMppLibrary* go_mpp_library_open(int max_width, int max_height);
+int go_mpp_library_ready(const GoMppLibrary* library);
+int go_mpp_library_submit(GoMppLibrary* library, const uint8_t* data, size_t length);
+int go_mpp_library_receive(GoMppLibrary* library, GoMppFrame* frame);
+void go_mpp_library_release_frame(GoMppLibrary* library, GoMppFrame* frame);
+int go_mpp_library_reset(GoMppLibrary* library);
+const char* go_mpp_library_error(const GoMppLibrary* library);
+const char* go_mpp_library_path(const GoMppLibrary* library);
+void go_mpp_library_close(GoMppLibrary* library);
+
+#endif
diff --git a/src/media/video/video_decoder.h b/src/media/video/video_decoder.h
new file mode 100644
index 0000000..c135737
--- /dev/null
+++ b/src/media/video/video_decoder.h
@@ -0,0 +1,121 @@
+#ifndef GREENOVERCAST_VIDEO_DECODER_H
+#define GREENOVERCAST_VIDEO_DECODER_H
+
+#include
+#include
+
+typedef enum {
+ GO_VIDEO_DECODER_RESULT_FATAL = -1,
+ GO_VIDEO_DECODER_RESULT_AGAIN = 0,
+ GO_VIDEO_DECODER_RESULT_OK = 1,
+} GoVideoDecoderResult;
+
+typedef enum {
+ GO_VIDEO_DECODER_BACKEND_NONE,
+ GO_VIDEO_DECODER_BACKEND_SOFTWARE,
+ GO_VIDEO_DECODER_BACKEND_CEDAR,
+ GO_VIDEO_DECODER_BACKEND_MPP,
+} GoVideoDecoderBackend;
+
+typedef enum {
+ GO_VIDEO_DECODER_PREFERENCE_AUTO,
+ GO_VIDEO_DECODER_PREFERENCE_MPP,
+ GO_VIDEO_DECODER_PREFERENCE_CEDAR,
+ GO_VIDEO_DECODER_PREFERENCE_SOFTWARE,
+} GoVideoDecoderPreference;
+
+typedef enum {
+ GO_VIDEO_PLATFORM_ROCKCHIP,
+ GO_VIDEO_PLATFORM_ALLWINNER,
+ GO_VIDEO_PLATFORM_OTHER_ARM,
+ GO_VIDEO_PLATFORM_NON_ARM,
+} GoVideoPlatform;
+
+typedef enum {
+ GO_VIDEO_PIXEL_FORMAT_NV12,
+ GO_VIDEO_PIXEL_FORMAT_YUV420P,
+} GoVideoPixelFormat;
+
+typedef enum {
+ GO_VIDEO_COLOR_RANGE_UNSPECIFIED,
+ GO_VIDEO_COLOR_RANGE_LIMITED,
+ GO_VIDEO_COLOR_RANGE_FULL,
+} GoVideoColorRange;
+
+typedef struct {
+ GoVideoPixelFormat format;
+ GoVideoColorRange color_range;
+ int width;
+ int height;
+ int coded_width;
+ int coded_height;
+ const uint8_t* planes[3];
+ int strides[3];
+ int corrupt;
+ int info_changed;
+ void* backend_frame;
+} GoDecodedVideoFrame;
+
+typedef struct GoVideoDecoder GoVideoDecoder;
+
+typedef struct {
+ const char* name;
+ GoVideoDecoderBackend backend;
+ GoVideoDecoderResult (*submit_access_unit)(GoVideoDecoder* decoder, const uint8_t* data,
+ size_t length);
+ GoVideoDecoderResult (*receive_frame)(GoVideoDecoder* decoder, GoDecodedVideoFrame* frame);
+ void (*release_frame)(GoVideoDecoder* decoder, GoDecodedVideoFrame* frame);
+ int (*reset)(GoVideoDecoder* decoder);
+ const char* (*last_error)(const GoVideoDecoder* decoder);
+ void (*destroy)(GoVideoDecoder* decoder);
+} GoVideoDecoderOps;
+
+struct GoVideoDecoder {
+ const GoVideoDecoderOps* ops;
+};
+
+typedef struct {
+ int max_width;
+ int max_height;
+ GoVideoDecoderPreference preference;
+} GoVideoDecoderSelectionConfig;
+
+typedef struct {
+ GoVideoDecoder* active;
+ GoVideoDecoder* software;
+ int allow_runtime_fallback;
+ uint64_t init_failures;
+} GoVideoDecoderSelection;
+
+void go_video_decoder_initialize(GoVideoDecoder* decoder, const GoVideoDecoderOps* ops);
+int go_video_decoder_preference_parse(const char* value, GoVideoDecoderPreference* preference);
+GoVideoPlatform go_video_decoder_platform(const uint8_t* compatible, size_t length, int is_arm);
+size_t go_video_decoder_candidate_order(GoVideoDecoderPreference preference,
+ GoVideoPlatform platform, GoVideoDecoderBackend* output,
+ size_t capacity);
+const char* go_video_decoder_name(const GoVideoDecoder* decoder);
+GoVideoDecoderBackend go_video_decoder_backend(const GoVideoDecoder* decoder);
+GoVideoDecoderResult go_video_decoder_submit_access_unit(GoVideoDecoder* decoder,
+ const uint8_t* data, size_t length);
+GoVideoDecoderResult go_video_decoder_receive_frame(GoVideoDecoder* decoder,
+ GoDecodedVideoFrame* frame);
+/* A successful receive owns one backend frame until this function is called. */
+void go_video_decoder_release_frame(GoVideoDecoder* decoder, GoDecodedVideoFrame* frame);
+int go_video_decoder_reset(GoVideoDecoder* decoder);
+const char* go_video_decoder_last_error(const GoVideoDecoder* decoder);
+void go_video_decoder_destroy(GoVideoDecoder* decoder);
+
+int go_video_decoder_selection_create(const GoVideoDecoderSelectionConfig* config,
+ GoVideoDecoderSelection* selection, char* error,
+ size_t error_capacity);
+int go_video_decoder_selection_fallback(GoVideoDecoderSelection* selection, char* error,
+ size_t error_capacity);
+void go_video_decoder_selection_destroy(GoVideoDecoderSelection* selection);
+
+GoVideoDecoder* go_video_decoder_ffmpeg_create(char* error, size_t error_capacity);
+GoVideoDecoder* go_video_decoder_cedar_create(int max_width, int max_height, char* error,
+ size_t error_capacity);
+GoVideoDecoder* go_video_decoder_mpp_create(int max_width, int max_height, char* error,
+ size_t error_capacity);
+
+#endif
diff --git a/src/media/video/video_decoder.zig b/src/media/video/video_decoder.zig
new file mode 100644
index 0000000..f5b5d81
--- /dev/null
+++ b/src/media/video/video_decoder.zig
@@ -0,0 +1,174 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("video_decoder.h");
+});
+
+const none_name: [*:0]const u8 = "none";
+const unavailable_error: [*:0]const u8 = "video decoder unavailable";
+
+pub export fn go_video_decoder_initialize(
+ decoder: ?*c.GoVideoDecoder,
+ ops: ?*const c.GoVideoDecoderOps,
+) void {
+ if (decoder) |value| value.ops = ops;
+}
+
+pub export fn go_video_decoder_preference_parse(
+ value: ?[*:0]const u8,
+ preference: ?*c.GoVideoDecoderPreference,
+) c_int {
+ const output = preference orelse return -1;
+ const name = if (value) |text| std.mem.span(text) else "";
+ output.* = if (name.len == 0 or std.mem.eql(u8, name, "auto"))
+ c.GO_VIDEO_DECODER_PREFERENCE_AUTO
+ else if (std.mem.eql(u8, name, "mpp"))
+ c.GO_VIDEO_DECODER_PREFERENCE_MPP
+ else if (std.mem.eql(u8, name, "cedar"))
+ c.GO_VIDEO_DECODER_PREFERENCE_CEDAR
+ else if (std.mem.eql(u8, name, "software"))
+ c.GO_VIDEO_DECODER_PREFERENCE_SOFTWARE
+ else
+ return -1;
+ return 0;
+}
+
+pub export fn go_video_decoder_platform(
+ compatible: ?[*]const u8,
+ length: usize,
+ is_arm: c_int,
+) c.GoVideoPlatform {
+ if (is_arm == 0) return c.GO_VIDEO_PLATFORM_NON_ARM;
+ const bytes = if (compatible) |data| data[0..length] else return c.GO_VIDEO_PLATFORM_OTHER_ARM;
+
+ var offset: usize = 0;
+ while (offset < bytes.len) {
+ const remainder = bytes[offset..];
+ const token_length = std.mem.indexOfScalar(u8, remainder, 0) orelse remainder.len;
+ const token = remainder[0..token_length];
+ if (std.mem.startsWith(u8, token, "rockchip,")) return c.GO_VIDEO_PLATFORM_ROCKCHIP;
+ if (std.mem.startsWith(u8, token, "allwinner,")) return c.GO_VIDEO_PLATFORM_ALLWINNER;
+ offset += token_length;
+ if (offset < bytes.len) offset += 1;
+ }
+ return c.GO_VIDEO_PLATFORM_OTHER_ARM;
+}
+
+pub export fn go_video_decoder_candidate_order(
+ preference: c.GoVideoDecoderPreference,
+ platform: c.GoVideoPlatform,
+ output: ?[*]c.GoVideoDecoderBackend,
+ capacity: usize,
+) usize {
+ var candidates: [3]c.GoVideoDecoderBackend = undefined;
+ var count: usize = 0;
+
+ if (preference == c.GO_VIDEO_DECODER_PREFERENCE_MPP) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_MPP;
+ count += 1;
+ } else if (preference == c.GO_VIDEO_DECODER_PREFERENCE_CEDAR) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_CEDAR;
+ count += 1;
+ } else if (preference == c.GO_VIDEO_DECODER_PREFERENCE_SOFTWARE) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
+ count += 1;
+ } else if (platform == c.GO_VIDEO_PLATFORM_ROCKCHIP) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_MPP;
+ count += 1;
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
+ count += 1;
+ } else if (platform == c.GO_VIDEO_PLATFORM_ALLWINNER) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_CEDAR;
+ count += 1;
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
+ count += 1;
+ } else if (platform == c.GO_VIDEO_PLATFORM_NON_ARM) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
+ count += 1;
+ } else {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_MPP;
+ count += 1;
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_CEDAR;
+ count += 1;
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
+ count += 1;
+ }
+
+ if (output) |destination| {
+ const written = @min(count, capacity);
+ @memcpy(destination[0..written], candidates[0..written]);
+ }
+ return count;
+}
+
+pub export fn go_video_decoder_name(decoder: ?*const c.GoVideoDecoder) [*:0]const u8 {
+ const value = decoder orelse return none_name;
+ const ops = value.ops orelse return none_name;
+ return ops.*.name orelse none_name;
+}
+
+pub export fn go_video_decoder_backend(decoder: ?*const c.GoVideoDecoder) c.GoVideoDecoderBackend {
+ const value = decoder orelse return c.GO_VIDEO_DECODER_BACKEND_NONE;
+ const ops = value.ops orelse return c.GO_VIDEO_DECODER_BACKEND_NONE;
+ return ops.*.backend;
+}
+
+pub export fn go_video_decoder_submit_access_unit(
+ decoder: ?*c.GoVideoDecoder,
+ data: ?[*]const u8,
+ length: usize,
+) c.GoVideoDecoderResult {
+ const value = decoder orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ const ops = value.ops orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ const submit = ops.*.submit_access_unit orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ const bytes = data orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ if (length == 0) return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ return submit(value, bytes, length);
+}
+
+pub export fn go_video_decoder_receive_frame(
+ decoder: ?*c.GoVideoDecoder,
+ frame: ?*c.GoDecodedVideoFrame,
+) c.GoVideoDecoderResult {
+ const value = decoder orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ const ops = value.ops orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ const receive = ops.*.receive_frame orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ const output = frame orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ output.* = std.mem.zeroes(c.GoDecodedVideoFrame);
+ return receive(value, output);
+}
+
+pub export fn go_video_decoder_release_frame(
+ decoder: ?*c.GoVideoDecoder,
+ frame: ?*c.GoDecodedVideoFrame,
+) void {
+ const value = decoder orelse return;
+ const ops = value.ops orelse return;
+ const release = ops.*.release_frame orelse return;
+ const output = frame orelse return;
+ release(value, output);
+ output.* = std.mem.zeroes(c.GoDecodedVideoFrame);
+}
+
+pub export fn go_video_decoder_reset(decoder: ?*c.GoVideoDecoder) c_int {
+ const value = decoder orelse return -1;
+ const ops = value.ops orelse return -1;
+ const reset = ops.*.reset orelse return -1;
+ return reset(value);
+}
+
+pub export fn go_video_decoder_last_error(
+ decoder: ?*const c.GoVideoDecoder,
+) [*:0]const u8 {
+ const value = decoder orelse return unavailable_error;
+ const ops = value.ops orelse return unavailable_error;
+ const last_error = ops.*.last_error orelse return unavailable_error;
+ return last_error(value) orelse unavailable_error;
+}
+
+pub export fn go_video_decoder_destroy(decoder: ?*c.GoVideoDecoder) void {
+ const value = decoder orelse return;
+ const ops = value.ops orelse return;
+ const destroy = ops.*.destroy orelse return;
+ destroy(value);
+}
diff --git a/src/media/video/video_decoder_cedar.c b/src/media/video/video_decoder_cedar.c
new file mode 100644
index 0000000..37d3221
--- /dev/null
+++ b/src/media/video/video_decoder_cedar.c
@@ -0,0 +1,147 @@
+#include "video_decoder.h"
+
+#include
+#include
+#include
+
+#include "cedar_loader.h"
+
+typedef struct {
+ GoVideoDecoder base;
+ GoCedarLibrary* library;
+ GoCedarFrame pending_frame;
+ int max_width;
+ int max_height;
+ int frame_pending;
+ int frame_outstanding;
+ int last_width;
+ int last_height;
+ int last_y_stride;
+ int last_uv_stride;
+ char error[256];
+} GoCedarVideoDecoder;
+
+static int open_cedar(GoCedarVideoDecoder* decoder) {
+ decoder->library = go_cedar_library_open(decoder->max_width, decoder->max_height);
+ if (!go_cedar_library_ready(decoder->library)) {
+ snprintf(decoder->error, sizeof(decoder->error), "%s",
+ go_cedar_library_error(decoder->library));
+ go_cedar_library_close(decoder->library);
+ decoder->library = NULL;
+ return -1;
+ }
+ decoder->error[0] = '\0';
+ return 0;
+}
+
+static GoVideoDecoderResult cedar_submit(GoVideoDecoder* base, const uint8_t* data, size_t length) {
+ GoCedarVideoDecoder* decoder = (GoCedarVideoDecoder*)base;
+ if (decoder->frame_pending || decoder->frame_outstanding)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ int result = go_cedar_library_feed(decoder->library, data, length, &decoder->pending_frame);
+ if (result < 0) {
+ snprintf(decoder->error, sizeof(decoder->error), "%s",
+ go_cedar_library_error(decoder->library));
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ decoder->frame_pending = result > 0;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static GoVideoDecoderResult cedar_receive(GoVideoDecoder* base, GoDecodedVideoFrame* output) {
+ GoCedarVideoDecoder* decoder = (GoCedarVideoDecoder*)base;
+ if (!decoder->frame_pending)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ GoCedarFrame* frame = &decoder->pending_frame;
+ output->format = GO_VIDEO_PIXEL_FORMAT_NV12;
+ output->color_range = GO_VIDEO_COLOR_RANGE_LIMITED;
+ output->width = frame->width;
+ output->height = frame->height;
+ output->coded_width = frame->width;
+ output->coded_height = frame->height;
+ output->planes[0] = frame->y;
+ output->planes[1] = frame->uv;
+ output->strides[0] = frame->y_stride;
+ output->strides[1] = frame->uv_stride;
+ output->info_changed = decoder->last_width != output->width ||
+ decoder->last_height != output->height ||
+ decoder->last_y_stride != output->strides[0] ||
+ decoder->last_uv_stride != output->strides[1];
+ decoder->last_width = output->width;
+ decoder->last_height = output->height;
+ decoder->last_y_stride = output->strides[0];
+ decoder->last_uv_stride = output->strides[1];
+ output->backend_frame = decoder;
+ decoder->frame_pending = 0;
+ decoder->frame_outstanding = 1;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static void cedar_release(GoVideoDecoder* base, GoDecodedVideoFrame* frame) {
+ GoCedarVideoDecoder* decoder = (GoCedarVideoDecoder*)base;
+ if (frame->backend_frame == decoder)
+ decoder->frame_outstanding = 0;
+}
+
+static int cedar_reset(GoVideoDecoder* base) {
+ GoCedarVideoDecoder* decoder = (GoCedarVideoDecoder*)base;
+ if (decoder->frame_outstanding) {
+ snprintf(decoder->error, sizeof(decoder->error),
+ "Cedar reset refused with a frame outstanding");
+ return -1;
+ }
+ go_cedar_library_close(decoder->library);
+ decoder->library = NULL;
+ decoder->frame_pending = 0;
+ decoder->frame_outstanding = 0;
+ decoder->last_width = 0;
+ decoder->last_height = 0;
+ decoder->last_y_stride = 0;
+ decoder->last_uv_stride = 0;
+ memset(&decoder->pending_frame, 0, sizeof(decoder->pending_frame));
+ return open_cedar(decoder);
+}
+
+static const char* cedar_last_error(const GoVideoDecoder* base) {
+ const GoCedarVideoDecoder* decoder = (const GoCedarVideoDecoder*)base;
+ return decoder->error[0] ? decoder->error : "Cedar H.264 decoder failure";
+}
+
+static void cedar_destroy(GoVideoDecoder* base) {
+ GoCedarVideoDecoder* decoder = (GoCedarVideoDecoder*)base;
+ go_cedar_library_close(decoder->library);
+ free(decoder);
+}
+
+static const GoVideoDecoderOps cedar_ops = {
+ .name = "cedar-h616",
+ .backend = GO_VIDEO_DECODER_BACKEND_CEDAR,
+ .submit_access_unit = cedar_submit,
+ .receive_frame = cedar_receive,
+ .release_frame = cedar_release,
+ .reset = cedar_reset,
+ .last_error = cedar_last_error,
+ .destroy = cedar_destroy,
+};
+
+GoVideoDecoder* go_video_decoder_cedar_create(int max_width, int max_height, char* error,
+ size_t error_capacity) {
+ if (max_width <= 0 || max_height <= 0) {
+ if (error && error_capacity > 0)
+ snprintf(error, error_capacity, "invalid Cedar decoder dimensions");
+ return NULL;
+ }
+ GoCedarVideoDecoder* decoder = calloc(1, sizeof(*decoder));
+ if (!decoder)
+ return NULL;
+ go_video_decoder_initialize(&decoder->base, &cedar_ops);
+ decoder->max_width = max_width;
+ decoder->max_height = max_height;
+ if (open_cedar(decoder) != 0) {
+ if (error && error_capacity > 0)
+ snprintf(error, error_capacity, "%s", decoder->error);
+ cedar_destroy(&decoder->base);
+ return NULL;
+ }
+ return &decoder->base;
+}
diff --git a/src/media/video/video_decoder_ffmpeg.c b/src/media/video/video_decoder_ffmpeg.c
new file mode 100644
index 0000000..f296c72
--- /dev/null
+++ b/src/media/video/video_decoder_ffmpeg.c
@@ -0,0 +1,166 @@
+#include "video_decoder.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+typedef struct {
+ GoVideoDecoder base;
+ const AVCodec* codec;
+ AVCodecContext* context;
+ AVPacket* packet;
+ AVFrame* frame;
+ int last_width;
+ int last_height;
+ int last_format;
+ int last_strides[3];
+ char error[256];
+} GoFfmpegVideoDecoder;
+
+static void set_ffmpeg_error(GoFfmpegVideoDecoder* decoder, const char* operation, int result) {
+ char detail[128];
+ if (av_strerror(result, detail, sizeof(detail)) < 0)
+ snprintf(detail, sizeof(detail), "error %d", result);
+ snprintf(decoder->error, sizeof(decoder->error), "%s: %s", operation, detail);
+}
+
+static GoVideoDecoderResult ffmpeg_submit(GoVideoDecoder* base, const uint8_t* data,
+ size_t length) {
+ GoFfmpegVideoDecoder* decoder = (GoFfmpegVideoDecoder*)base;
+ if (length > INT_MAX) {
+ snprintf(decoder->error, sizeof(decoder->error), "H.264 access unit is too large");
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ decoder->packet->data = (uint8_t*)data;
+ decoder->packet->size = (int)length;
+ int result = avcodec_send_packet(decoder->context, decoder->packet);
+ decoder->packet->data = NULL;
+ decoder->packet->size = 0;
+ if (result == 0)
+ return GO_VIDEO_DECODER_RESULT_OK;
+ if (result == AVERROR(EAGAIN))
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ set_ffmpeg_error(decoder, "FFmpeg packet submission failed", result);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+}
+
+static GoVideoDecoderResult ffmpeg_receive(GoVideoDecoder* base, GoDecodedVideoFrame* output) {
+ GoFfmpegVideoDecoder* decoder = (GoFfmpegVideoDecoder*)base;
+ int result = avcodec_receive_frame(decoder->context, decoder->frame);
+ if (result == AVERROR(EAGAIN) || result == AVERROR_EOF)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ if (result < 0) {
+ set_ffmpeg_error(decoder, "FFmpeg frame receive failed", result);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+
+ if (decoder->frame->format == AV_PIX_FMT_NV12)
+ output->format = GO_VIDEO_PIXEL_FORMAT_NV12;
+ else if (decoder->frame->format == AV_PIX_FMT_YUV420P)
+ output->format = GO_VIDEO_PIXEL_FORMAT_YUV420P;
+ else {
+ snprintf(decoder->error, sizeof(decoder->error), "unsupported FFmpeg pixel format %d",
+ decoder->frame->format);
+ av_frame_unref(decoder->frame);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+
+ output->color_range = decoder->frame->color_range == AVCOL_RANGE_JPEG
+ ? GO_VIDEO_COLOR_RANGE_FULL
+ : GO_VIDEO_COLOR_RANGE_LIMITED;
+ output->width = decoder->frame->width;
+ output->height = decoder->frame->height;
+ output->coded_width = decoder->context->coded_width;
+ output->coded_height = decoder->context->coded_height;
+ for (int plane = 0; plane < 3; ++plane) {
+ output->planes[plane] = decoder->frame->data[plane];
+ output->strides[plane] = decoder->frame->linesize[plane];
+ }
+ output->info_changed =
+ decoder->last_width != output->width || decoder->last_height != output->height ||
+ decoder->last_format != decoder->frame->format ||
+ memcmp(decoder->last_strides, output->strides, sizeof(decoder->last_strides)) != 0;
+ decoder->last_width = output->width;
+ decoder->last_height = output->height;
+ decoder->last_format = decoder->frame->format;
+ memcpy(decoder->last_strides, output->strides, sizeof(decoder->last_strides));
+ output->corrupt = (decoder->frame->flags & AV_FRAME_FLAG_CORRUPT) != 0;
+ output->backend_frame = decoder->frame;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static void ffmpeg_release(GoVideoDecoder* base, GoDecodedVideoFrame* frame) {
+ GoFfmpegVideoDecoder* decoder = (GoFfmpegVideoDecoder*)base;
+ if (frame->backend_frame == decoder->frame)
+ av_frame_unref(decoder->frame);
+}
+
+static int ffmpeg_reset(GoVideoDecoder* base) {
+ GoFfmpegVideoDecoder* decoder = (GoFfmpegVideoDecoder*)base;
+ avcodec_flush_buffers(decoder->context);
+ av_frame_unref(decoder->frame);
+ decoder->last_width = 0;
+ decoder->last_height = 0;
+ decoder->last_format = 0;
+ memset(decoder->last_strides, 0, sizeof(decoder->last_strides));
+ return 0;
+}
+
+static const char* ffmpeg_last_error(const GoVideoDecoder* base) {
+ const GoFfmpegVideoDecoder* decoder = (const GoFfmpegVideoDecoder*)base;
+ return decoder->error[0] ? decoder->error : "FFmpeg H.264 decoder failure";
+}
+
+static void ffmpeg_destroy(GoVideoDecoder* base) {
+ GoFfmpegVideoDecoder* decoder = (GoFfmpegVideoDecoder*)base;
+ av_frame_free(&decoder->frame);
+ av_packet_free(&decoder->packet);
+ avcodec_free_context(&decoder->context);
+ free(decoder);
+}
+
+static const GoVideoDecoderOps ffmpeg_ops = {
+ .name = "ffmpeg-software",
+ .backend = GO_VIDEO_DECODER_BACKEND_SOFTWARE,
+ .submit_access_unit = ffmpeg_submit,
+ .receive_frame = ffmpeg_receive,
+ .release_frame = ffmpeg_release,
+ .reset = ffmpeg_reset,
+ .last_error = ffmpeg_last_error,
+ .destroy = ffmpeg_destroy,
+};
+
+GoVideoDecoder* go_video_decoder_ffmpeg_create(char* error, size_t error_capacity) {
+ GoFfmpegVideoDecoder* decoder = calloc(1, sizeof(*decoder));
+ if (!decoder)
+ return NULL;
+ go_video_decoder_initialize(&decoder->base, &ffmpeg_ops);
+
+ decoder->codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ if (decoder->codec)
+ decoder->context = avcodec_alloc_context3(decoder->codec);
+ if (decoder->context) {
+ decoder->context->thread_count = 3;
+ decoder->context->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
+ decoder->context->skip_frame = AVDISCARD_NONREF;
+ if (avcodec_open2(decoder->context, decoder->codec, NULL) < 0) {
+ snprintf(decoder->error, sizeof(decoder->error), "FFmpeg H.264 initialization failed");
+ } else {
+ decoder->packet = av_packet_alloc();
+ decoder->frame = av_frame_alloc();
+ }
+ }
+ if (!decoder->codec || !decoder->context || !decoder->packet || !decoder->frame) {
+ if (!decoder->error[0])
+ snprintf(decoder->error, sizeof(decoder->error), "FFmpeg H.264 decoder unavailable");
+ if (error && error_capacity > 0)
+ snprintf(error, error_capacity, "%s", decoder->error);
+ ffmpeg_destroy(&decoder->base);
+ return NULL;
+ }
+ return &decoder->base;
+}
diff --git a/src/media/video/video_decoder_mpp.c b/src/media/video/video_decoder_mpp.c
new file mode 100644
index 0000000..573f7b8
--- /dev/null
+++ b/src/media/video/video_decoder_mpp.c
@@ -0,0 +1,141 @@
+#include "video_decoder.h"
+
+#include
+#include
+#include
+
+#include "mpp_loader.h"
+
+typedef struct {
+ GoVideoDecoder base;
+ GoMppLibrary* library;
+ GoMppFrame active_frame;
+ int frame_outstanding;
+ int last_width;
+ int last_height;
+ int last_y_stride;
+ int last_uv_stride;
+ char error[256];
+} GoMppVideoDecoder;
+
+static void copy_mpp_error(GoMppVideoDecoder* decoder) {
+ snprintf(decoder->error, sizeof(decoder->error), "%s", go_mpp_library_error(decoder->library));
+}
+
+static GoVideoDecoderResult mpp_submit(GoVideoDecoder* base, const uint8_t* data, size_t length) {
+ GoMppVideoDecoder* decoder = (GoMppVideoDecoder*)base;
+ int result = go_mpp_library_submit(decoder->library, data, length);
+ if (result > 0)
+ return GO_VIDEO_DECODER_RESULT_OK;
+ if (result == 0)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ copy_mpp_error(decoder);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+}
+
+static GoVideoDecoderResult mpp_receive(GoVideoDecoder* base, GoDecodedVideoFrame* output) {
+ GoMppVideoDecoder* decoder = (GoMppVideoDecoder*)base;
+ if (decoder->frame_outstanding) {
+ snprintf(decoder->error, sizeof(decoder->error), "MPP frame was not released");
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ int result = go_mpp_library_receive(decoder->library, &decoder->active_frame);
+ if (result == 0)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ if (result < 0) {
+ copy_mpp_error(decoder);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ output->format = GO_VIDEO_PIXEL_FORMAT_NV12;
+ output->color_range = GO_VIDEO_COLOR_RANGE_LIMITED;
+ output->width = decoder->active_frame.width;
+ output->height = decoder->active_frame.height;
+ output->coded_width = decoder->active_frame.width;
+ output->coded_height = decoder->active_frame.height;
+ output->planes[0] = decoder->active_frame.y;
+ output->planes[1] = decoder->active_frame.uv;
+ output->strides[0] = decoder->active_frame.y_stride;
+ output->strides[1] = decoder->active_frame.uv_stride;
+ output->info_changed = decoder->last_width != output->width ||
+ decoder->last_height != output->height ||
+ decoder->last_y_stride != output->strides[0] ||
+ decoder->last_uv_stride != output->strides[1];
+ decoder->last_width = output->width;
+ decoder->last_height = output->height;
+ decoder->last_y_stride = output->strides[0];
+ decoder->last_uv_stride = output->strides[1];
+ output->backend_frame = &decoder->active_frame;
+ decoder->frame_outstanding = 1;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static void mpp_release(GoVideoDecoder* base, GoDecodedVideoFrame* frame) {
+ GoMppVideoDecoder* decoder = (GoMppVideoDecoder*)base;
+ if (decoder->frame_outstanding && frame->backend_frame == &decoder->active_frame) {
+ go_mpp_library_release_frame(decoder->library, &decoder->active_frame);
+ memset(&decoder->active_frame, 0, sizeof(decoder->active_frame));
+ decoder->frame_outstanding = 0;
+ }
+}
+
+static int mpp_reset(GoVideoDecoder* base) {
+ GoMppVideoDecoder* decoder = (GoMppVideoDecoder*)base;
+ if (decoder->frame_outstanding) {
+ snprintf(decoder->error, sizeof(decoder->error),
+ "MPP reset refused with a frame outstanding");
+ return -1;
+ }
+ int result = go_mpp_library_reset(decoder->library);
+ if (result < 0)
+ copy_mpp_error(decoder);
+ else {
+ decoder->last_width = 0;
+ decoder->last_height = 0;
+ decoder->last_y_stride = 0;
+ decoder->last_uv_stride = 0;
+ decoder->error[0] = '\0';
+ }
+ return result;
+}
+
+static const char* mpp_last_error(const GoVideoDecoder* base) {
+ const GoMppVideoDecoder* decoder = (const GoMppVideoDecoder*)base;
+ return decoder->error[0] ? decoder->error : "MPP H.264 decoder failure";
+}
+
+static void mpp_destroy(GoVideoDecoder* base) {
+ GoMppVideoDecoder* decoder = (GoMppVideoDecoder*)base;
+ if (decoder->frame_outstanding)
+ go_mpp_library_release_frame(decoder->library, &decoder->active_frame);
+ go_mpp_library_close(decoder->library);
+ free(decoder);
+}
+
+static const GoVideoDecoderOps mpp_ops = {
+ .name = "rockchip-mpp",
+ .backend = GO_VIDEO_DECODER_BACKEND_MPP,
+ .submit_access_unit = mpp_submit,
+ .receive_frame = mpp_receive,
+ .release_frame = mpp_release,
+ .reset = mpp_reset,
+ .last_error = mpp_last_error,
+ .destroy = mpp_destroy,
+};
+
+GoVideoDecoder* go_video_decoder_mpp_create(int max_width, int max_height, char* error,
+ size_t error_capacity) {
+ GoMppVideoDecoder* decoder = calloc(1, sizeof(*decoder));
+ if (!decoder)
+ return NULL;
+ go_video_decoder_initialize(&decoder->base, &mpp_ops);
+ decoder->library = go_mpp_library_open(max_width, max_height);
+ if (!go_mpp_library_ready(decoder->library)) {
+ copy_mpp_error(decoder);
+ if (error && error_capacity > 0)
+ snprintf(error, error_capacity, "%s", decoder->error);
+ mpp_destroy(&decoder->base);
+ return NULL;
+ }
+ fprintf(stderr, "MPP plugin: %s\n", go_mpp_library_path(decoder->library));
+ return &decoder->base;
+}
diff --git a/src/media/video/video_decoder_selection.zig b/src/media/video/video_decoder_selection.zig
new file mode 100644
index 0000000..a81298e
--- /dev/null
+++ b/src/media/video/video_decoder_selection.zig
@@ -0,0 +1,183 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+const c = @cImport({
+ @cInclude("video_decoder.h");
+});
+
+fn writeError(destination: ?[*]u8, capacity: usize, message: []const u8) void {
+ const output = destination orelse return;
+ if (capacity == 0) return;
+ const length = @min(message.len, capacity - 1);
+ @memcpy(output[0..length], message[0..length]);
+ output[length] = 0;
+}
+
+fn readCompatible(output: []u8) usize {
+ const paths = [_][]const u8{
+ "/proc/device-tree/compatible",
+ "/sys/firmware/devicetree/base/compatible",
+ };
+ for (paths) |path| {
+ const file = std.fs.openFileAbsolute(path, .{}) catch continue;
+ defer file.close();
+ const length = file.readAll(output) catch continue;
+ if (length > 0) return length;
+ }
+ return 0;
+}
+
+fn isArm() c_int {
+ return switch (builtin.cpu.arch) {
+ .arm, .armeb, .aarch64, .aarch64_be, .thumb, .thumbeb => 1,
+ else => 0,
+ };
+}
+
+fn backendName(backend: c.GoVideoDecoderBackend) []const u8 {
+ return if (backend == c.GO_VIDEO_DECODER_BACKEND_MPP)
+ "mpp"
+ else if (backend == c.GO_VIDEO_DECODER_BACKEND_CEDAR)
+ "cedar"
+ else if (backend == c.GO_VIDEO_DECODER_BACKEND_SOFTWARE)
+ "software"
+ else
+ "none";
+}
+
+fn createBackend(
+ config: *const c.GoVideoDecoderSelectionConfig,
+ backend: c.GoVideoDecoderBackend,
+ error_buffer: *[256]u8,
+) ?*c.GoVideoDecoder {
+ @memset(error_buffer, 0);
+ if (backend == c.GO_VIDEO_DECODER_BACKEND_MPP) {
+ return c.go_video_decoder_mpp_create(
+ config.max_width,
+ config.max_height,
+ error_buffer,
+ error_buffer.len,
+ );
+ }
+ if (backend == c.GO_VIDEO_DECODER_BACKEND_CEDAR) {
+ return c.go_video_decoder_cedar_create(
+ config.max_width,
+ config.max_height,
+ error_buffer,
+ error_buffer.len,
+ );
+ }
+ if (backend == c.GO_VIDEO_DECODER_BACKEND_SOFTWARE) {
+ return c.go_video_decoder_ffmpeg_create(error_buffer, error_buffer.len);
+ }
+ return null;
+}
+
+fn bufferString(buffer: *const [256]u8) []const u8 {
+ return std.mem.sliceTo(buffer, 0);
+}
+
+pub export fn go_video_decoder_selection_create(
+ config: ?*const c.GoVideoDecoderSelectionConfig,
+ selection: ?*c.GoVideoDecoderSelection,
+ error_output: ?[*]u8,
+ error_capacity: usize,
+) c_int {
+ const settings = config orelse return -1;
+ const output = selection orelse return -1;
+ output.* = std.mem.zeroes(c.GoVideoDecoderSelection);
+ if (settings.max_width <= 0 or settings.max_height <= 0 or
+ settings.max_width > 8192 or settings.max_height > 8192 or
+ settings.preference > c.GO_VIDEO_DECODER_PREFERENCE_SOFTWARE)
+ {
+ writeError(error_output, error_capacity, "invalid video decoder configuration");
+ return -1;
+ }
+
+ var compatible: [4096]u8 = undefined;
+ const compatible_length = readCompatible(&compatible);
+ const platform = c.go_video_decoder_platform(&compatible, compatible_length, isArm());
+ var candidates: [3]c.GoVideoDecoderBackend = undefined;
+ const candidate_count = c.go_video_decoder_candidate_order(
+ settings.preference,
+ platform,
+ &candidates,
+ candidates.len,
+ );
+ const automatic = settings.preference == c.GO_VIDEO_DECODER_PREFERENCE_AUTO;
+ var backend_error = [_]u8{0} ** 256;
+
+ if (automatic) {
+ output.software = createBackend(settings, c.GO_VIDEO_DECODER_BACKEND_SOFTWARE, &backend_error);
+ if (output.software == null) {
+ const detail = bufferString(&backend_error);
+ std.debug.print("Software video decoder unavailable: {s}\n", .{detail});
+ writeError(error_output, error_capacity, detail);
+ return -1;
+ }
+ }
+
+ for (candidates[0..candidate_count]) |backend| {
+ std.debug.print("Trying video decoder: {s}\n", .{backendName(backend)});
+ const candidate = if (backend == c.GO_VIDEO_DECODER_BACKEND_SOFTWARE and
+ output.software != null)
+ output.software
+ else
+ createBackend(settings, backend, &backend_error);
+ if (candidate) |decoder| {
+ output.active = decoder;
+ if (backend == c.GO_VIDEO_DECODER_BACKEND_SOFTWARE) output.software = decoder;
+ break;
+ }
+ output.init_failures += 1;
+ std.debug.print("{s} video decoder unavailable: {s}\n", .{
+ backendName(backend),
+ bufferString(&backend_error),
+ });
+ }
+
+ if (output.active == null) {
+ writeError(error_output, error_capacity, bufferString(&backend_error));
+ go_video_decoder_selection_destroy(output);
+ return -1;
+ }
+ output.allow_runtime_fallback = @intFromBool(automatic and output.active != output.software);
+ std.debug.print("Selected video decoder: {s}\n", .{
+ std.mem.span(c.go_video_decoder_name(output.active)),
+ });
+ return 0;
+}
+
+pub export fn go_video_decoder_selection_fallback(
+ selection: ?*c.GoVideoDecoderSelection,
+ error_output: ?[*]u8,
+ error_capacity: usize,
+) c_int {
+ const output = selection orelse return -1;
+ if (output.allow_runtime_fallback == 0 or output.software == null or
+ output.active == output.software)
+ {
+ return -1;
+ }
+
+ c.go_video_decoder_destroy(output.active);
+ output.active = output.software;
+ output.allow_runtime_fallback = 0;
+ if (c.go_video_decoder_reset(output.active) != 0) {
+ writeError(
+ error_output,
+ error_capacity,
+ std.mem.span(c.go_video_decoder_last_error(output.active)),
+ );
+ return -1;
+ }
+ return 0;
+}
+
+pub export fn go_video_decoder_selection_destroy(selection: ?*c.GoVideoDecoderSelection) void {
+ const output = selection orelse return;
+ if (output.active != null and output.active != output.software)
+ c.go_video_decoder_destroy(output.active);
+ if (output.software != null) c.go_video_decoder_destroy(output.software);
+ output.* = std.mem.zeroes(c.GoVideoDecoderSelection);
+}
diff --git a/src/media/video/video_frame_copy.h b/src/media/video/video_frame_copy.h
new file mode 100644
index 0000000..3235be3
--- /dev/null
+++ b/src/media/video/video_frame_copy.h
@@ -0,0 +1,10 @@
+#ifndef GREENOVERCAST_VIDEO_FRAME_COPY_H
+#define GREENOVERCAST_VIDEO_FRAME_COPY_H
+
+#include "video_decoder.h"
+
+int go_video_frame_validate(const GoDecodedVideoFrame* frame, int max_width, int max_height);
+int go_video_frame_copy(const GoDecodedVideoFrame* source, int max_width, int max_height,
+ uint8_t* destination_planes[3], const int destination_strides[3]);
+
+#endif
diff --git a/src/media/video/video_frame_copy.zig b/src/media/video/video_frame_copy.zig
new file mode 100644
index 0000000..dcb2672
--- /dev/null
+++ b/src/media/video/video_frame_copy.zig
@@ -0,0 +1,111 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("video_frame_copy.h");
+});
+
+fn validPlane(plane: ?[*]const u8, stride: c_int, row_bytes: c_int, rows: c_int) bool {
+ if (plane == null or stride < row_bytes or row_bytes <= 0 or rows <= 0) return false;
+ const byte_stride: usize = @intCast(stride);
+ const visible_bytes: usize = @intCast(row_bytes);
+ const row_count: usize = @intCast(rows);
+ return row_count - 1 <= (std.math.maxInt(usize) - visible_bytes) / byte_stride;
+}
+
+fn validate(frame: *const c.GoDecodedVideoFrame, max_width: c_int, max_height: c_int) bool {
+ if (max_width <= 0 or max_height <= 0 or frame.width <= 0 or frame.height <= 0 or
+ frame.width > max_width or frame.height > max_height or
+ !validPlane(frame.planes[0], frame.strides[0], frame.width, frame.height))
+ {
+ return false;
+ }
+
+ const chroma_height = @divTrunc(frame.height + 1, 2);
+ if (frame.format == c.GO_VIDEO_PIXEL_FORMAT_NV12) {
+ const chroma_width = frame.width + @mod(frame.width, 2);
+ return validPlane(frame.planes[1], frame.strides[1], chroma_width, chroma_height);
+ }
+ if (frame.format == c.GO_VIDEO_PIXEL_FORMAT_YUV420P) {
+ const chroma_width = @divTrunc(frame.width + 1, 2);
+ return validPlane(frame.planes[1], frame.strides[1], chroma_width, chroma_height) and
+ validPlane(frame.planes[2], frame.strides[2], chroma_width, chroma_height);
+ }
+ return false;
+}
+
+pub export fn go_video_frame_validate(
+ frame: ?*const c.GoDecodedVideoFrame,
+ max_width: c_int,
+ max_height: c_int,
+) c_int {
+ const source = frame orelse return -1;
+ return if (validate(source, max_width, max_height)) 0 else -1;
+}
+
+fn copyPlane(
+ destination: [*]u8,
+ destination_stride: usize,
+ source: [*]const u8,
+ source_stride: usize,
+ row_bytes: usize,
+ rows: usize,
+) void {
+ for (0..rows) |row| {
+ const destination_offset = row * destination_stride;
+ const source_offset = row * source_stride;
+ @memcpy(
+ destination[destination_offset .. destination_offset + row_bytes],
+ source[source_offset .. source_offset + row_bytes],
+ );
+ }
+}
+
+pub export fn go_video_frame_copy(
+ source: ?*const c.GoDecodedVideoFrame,
+ max_width: c_int,
+ max_height: c_int,
+ destination_planes: ?[*]?[*]u8,
+ destination_strides: ?[*]const c_int,
+) c_int {
+ const frame = source orelse return -1;
+ const planes = destination_planes orelse return -1;
+ const strides = destination_strides orelse return -1;
+ if (!validate(frame, max_width, max_height) or
+ !validPlane(planes[0], strides[0], frame.width, frame.height))
+ {
+ return -1;
+ }
+
+ const chroma_height: c_int = @divTrunc(frame.height + 1, 2);
+ const chroma_width: c_int = if (frame.format == c.GO_VIDEO_PIXEL_FORMAT_NV12)
+ frame.width + @mod(frame.width, 2)
+ else
+ @divTrunc(frame.width + 1, 2);
+ if (!validPlane(planes[1], strides[1], chroma_width, chroma_height) or
+ (frame.format == c.GO_VIDEO_PIXEL_FORMAT_YUV420P and
+ !validPlane(planes[2], strides[2], chroma_width, chroma_height)))
+ {
+ return -1;
+ }
+
+ copyPlane(
+ planes[0].?,
+ @intCast(strides[0]),
+ frame.planes[0].?,
+ @intCast(frame.strides[0]),
+ @intCast(frame.width),
+ @intCast(frame.height),
+ );
+ const plane_count: usize = if (frame.format == c.GO_VIDEO_PIXEL_FORMAT_NV12) 2 else 3;
+ for (1..plane_count) |plane| {
+ copyPlane(
+ planes[plane].?,
+ @intCast(strides[plane]),
+ frame.planes[plane].?,
+ @intCast(frame.strides[plane]),
+ @intCast(chroma_width),
+ @intCast(chroma_height),
+ );
+ }
+ return 0;
+}
diff --git a/src/media/video/video_pipeline.c b/src/media/video/video_pipeline.c
index 9c22b52..2368fc0 100644
--- a/src/media/video/video_pipeline.c
+++ b/src/media/video/video_pipeline.c
@@ -1,7 +1,8 @@
#include "video_pipeline.h"
#include
-#include
+#include
+#include
#include
#include
#include
@@ -13,12 +14,14 @@
#include
#include "../../util/log.h"
-#include "cedar_loader.h"
#include "h264_depacketizer.h"
+#include "video_decoder.h"
+#include "video_frame_copy.h"
#define H264_BOOTSTRAP_MAX_SIZE 4104
#define VIDEO_PACKET_MAX_SIZE 4096
#define VIDEO_PACKET_QUEUE_CAPACITY 128
+#define VIDEO_DECODER_BACKPRESSURE_ATTEMPTS 64
typedef struct {
uint16_t length;
@@ -44,13 +47,14 @@ struct GoVideoPipeline {
int thread_started;
atomic_int stop;
atomic_int accepting_packets;
+ atomic_int restart_epoch_pending;
- const AVCodec* codec;
- AVCodecContext* decoder;
- AVPacket* decoder_packet;
- AVFrame* decoder_frame;
- AVFrame* cedar_frame;
- GoCedarLibrary* cedar;
+ GoVideoDecoderSelection decoders;
+ AVFrame* decoded_frame;
+ int max_width;
+ int max_height;
+ char decoder_failure[256];
+ atomic_int failed;
SDL_Texture* texture;
struct SwsContext* scaler;
@@ -90,6 +94,12 @@ struct GoVideoPipeline {
atomic_int decoder_send_errors;
atomic_int decoder_receive_errors;
atomic_int last_decoder_error;
+ atomic_int decoder_backend;
+ atomic_int decoder_init_failures;
+ atomic_int decoder_runtime_fallbacks;
+ atomic_int decoder_backpressure_events;
+ atomic_int decoder_corrupt_frames;
+ atomic_int decoder_info_changes;
atomic_int keyframe_requests;
atomic_int dropped_packets;
};
@@ -155,14 +165,24 @@ static void publish_frame(GoVideoPipeline* pipeline, AVFrame* frame, const char*
pthread_mutex_unlock(&pipeline->frame_lock);
}
-static int publish_cedar_frame(GoVideoPipeline* pipeline, const GoCedarFrame* frame) {
- if (!pipeline->cedar_frame || frame->width <= 0 || frame->height <= 0)
+static int publish_decoded_frame(GoVideoPipeline* pipeline, const GoDecodedVideoFrame* frame) {
+ if (!pipeline->decoded_frame ||
+ go_video_frame_validate(frame, pipeline->max_width, pipeline->max_height) != 0)
return -1;
- AVFrame* target = pipeline->cedar_frame;
- if (target->format != AV_PIX_FMT_NV12 || target->width != frame->width ||
+ enum AVPixelFormat format;
+ if (frame->format == GO_VIDEO_PIXEL_FORMAT_NV12) {
+ format = AV_PIX_FMT_NV12;
+ } else if (frame->format == GO_VIDEO_PIXEL_FORMAT_YUV420P) {
+ format = AV_PIX_FMT_YUV420P;
+ } else {
+ return -1;
+ }
+
+ AVFrame* target = pipeline->decoded_frame;
+ if (target->format != format || target->width != frame->width ||
target->height != frame->height) {
av_frame_unref(target);
- target->format = AV_PIX_FMT_NV12;
+ target->format = format;
target->width = frame->width;
target->height = frame->height;
if (av_frame_get_buffer(target, 32) < 0)
@@ -170,54 +190,129 @@ static int publish_cedar_frame(GoVideoPipeline* pipeline, const GoCedarFrame* fr
}
if (av_frame_make_writable(target) < 0)
return -1;
- for (int y = 0; y < frame->height; y++)
- memcpy(target->data[0] + (size_t)y * (size_t)target->linesize[0],
- frame->y + (size_t)y * (size_t)frame->y_stride, (size_t)frame->width);
- for (int y = 0; y < frame->height / 2; y++)
- memcpy(target->data[1] + (size_t)y * (size_t)target->linesize[1],
- frame->uv + (size_t)y * (size_t)frame->uv_stride, (size_t)frame->width);
- target->color_range = AVCOL_RANGE_MPEG;
+ if (go_video_frame_copy(frame, pipeline->max_width, pipeline->max_height, target->data,
+ target->linesize) != 0)
+ return -1;
+ target->color_range =
+ frame->color_range == GO_VIDEO_COLOR_RANGE_FULL ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG;
target->colorspace = AVCOL_SPC_BT709;
- publish_frame(pipeline, target, "cedar-h616");
+ publish_frame(pipeline, target, go_video_decoder_name(pipeline->decoders.active));
return 0;
}
-static void feed_software_decoder(GoVideoPipeline* pipeline, const uint8_t* data, int length) {
- pipeline->decoder_packet->data = (uint8_t*)data;
- pipeline->decoder_packet->size = length;
- if (avcodec_send_packet(pipeline->decoder, pipeline->decoder_packet) < 0)
- atomic_fetch_add(&pipeline->decoder_send_errors, 1);
- int result;
- while ((result = avcodec_receive_frame(pipeline->decoder, pipeline->decoder_frame)) == 0) {
- publish_frame(pipeline, pipeline->decoder_frame, "ffmpeg-software");
- av_frame_unref(pipeline->decoder_frame);
+static int drain_decoder_frames(GoVideoPipeline* pipeline) {
+ for (;;) {
+ GoDecodedVideoFrame frame;
+ GoVideoDecoderResult result =
+ go_video_decoder_receive_frame(pipeline->decoders.active, &frame);
+ if (result == GO_VIDEO_DECODER_RESULT_AGAIN)
+ return 0;
+ if (result == GO_VIDEO_DECODER_RESULT_FATAL) {
+ atomic_fetch_add(&pipeline->decoder_receive_errors, 1);
+ atomic_store(&pipeline->last_decoder_error, -1);
+ snprintf(pipeline->decoder_failure, sizeof(pipeline->decoder_failure), "%s",
+ go_video_decoder_last_error(pipeline->decoders.active));
+ return -1;
+ }
+ if (frame.corrupt) {
+ atomic_fetch_add(&pipeline->decoder_corrupt_frames, 1);
+ go_video_decoder_release_frame(pipeline->decoders.active, &frame);
+ continue;
+ }
+ if (frame.info_changed)
+ atomic_fetch_add(&pipeline->decoder_info_changes, 1);
+ int publish_result = publish_decoded_frame(pipeline, &frame);
+ go_video_decoder_release_frame(pipeline->decoders.active, &frame);
+ if (publish_result != 0) {
+ snprintf(pipeline->decoder_failure, sizeof(pipeline->decoder_failure),
+ "decoder returned an invalid frame");
+ return -1;
+ }
}
- if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
- atomic_fetch_add(&pipeline->decoder_receive_errors, 1);
- atomic_store(&pipeline->last_decoder_error, result);
+}
+
+static int switch_to_software_decoder(GoVideoPipeline* pipeline) {
+ if (!pipeline->decoders.allow_runtime_fallback || !pipeline->decoders.software ||
+ pipeline->decoders.active == pipeline->decoders.software)
+ return -1;
+ fprintf(stderr, "%s decoder disabled: %s\n",
+ go_video_decoder_name(pipeline->decoders.active),
+ pipeline->decoder_failure);
+ if (go_video_decoder_selection_fallback(&pipeline->decoders, pipeline->decoder_failure,
+ sizeof(pipeline->decoder_failure)) != 0)
+ return -1;
+ atomic_store(&pipeline->decoder_backend,
+ (int)go_video_decoder_backend(pipeline->decoders.active));
+ atomic_fetch_add(&pipeline->decoder_runtime_fallbacks, 1);
+ go_h264_depacketizer_restart_decode_epoch(pipeline->depacketizer);
+ atomic_store(&pipeline->keyframe_pending, 1);
+ atomic_store(&pipeline->synced, 0);
+ fprintf(stderr, "Selected video decoder: %s\n",
+ go_video_decoder_name(pipeline->decoders.active));
+ return 0;
+}
+
+static void mark_decoder_failed(GoVideoPipeline* pipeline) {
+ if (atomic_exchange(&pipeline->failed, 1))
+ return;
+ atomic_store(&pipeline->accepting_packets, 0);
+ fprintf(stderr, "Video decoder failed: %s\n", pipeline->decoder_failure);
+}
+
+static int decode_with_active_backend(GoVideoPipeline* pipeline, const uint8_t* data,
+ size_t length) {
+ for (int attempt = 0; attempt < VIDEO_DECODER_BACKPRESSURE_ATTEMPTS; ++attempt) {
+ if (atomic_load(&pipeline->stop))
+ return 0;
+ GoVideoDecoderResult result =
+ go_video_decoder_submit_access_unit(pipeline->decoders.active, data, length);
+ if (result == GO_VIDEO_DECODER_RESULT_OK)
+ return drain_decoder_frames(pipeline);
+ if (result == GO_VIDEO_DECODER_RESULT_FATAL) {
+ atomic_fetch_add(&pipeline->decoder_send_errors, 1);
+ atomic_store(&pipeline->last_decoder_error, -1);
+ snprintf(pipeline->decoder_failure, sizeof(pipeline->decoder_failure), "%s",
+ go_video_decoder_last_error(pipeline->decoders.active));
+ return -1;
+ }
+ atomic_fetch_add(&pipeline->decoder_backpressure_events, 1);
+ if (drain_decoder_frames(pipeline) != 0)
+ return -1;
+ if (attempt + 1 < VIDEO_DECODER_BACKPRESSURE_ATTEMPTS)
+ SDL_Delay(1);
}
+ snprintf(pipeline->decoder_failure, sizeof(pipeline->decoder_failure),
+ "decoder made no progress after %d submit attempts",
+ VIDEO_DECODER_BACKPRESSURE_ATTEMPTS);
+ return -1;
+}
+
+static int select_decoder(GoVideoPipeline* pipeline, GoVideoDecoderPreference preference) {
+ GoVideoDecoderSelectionConfig config = {
+ .max_width = pipeline->max_width,
+ .max_height = pipeline->max_height,
+ .preference = preference,
+ };
+ if (go_video_decoder_selection_create(&config, &pipeline->decoders,
+ pipeline->decoder_failure,
+ sizeof(pipeline->decoder_failure)) != 0)
+ return -1;
+ atomic_store(&pipeline->decoder_init_failures, pipeline->decoders.init_failures);
+ atomic_store(&pipeline->decoder_backend,
+ (int)go_video_decoder_backend(pipeline->decoders.active));
+ return 0;
}
static void decode_access_unit(void* context, const uint8_t* data, size_t length,
const GoH264AccessUnit* info) {
(void)info;
GoVideoPipeline* pipeline = context;
- if (length > INT32_MAX || !pipeline->decoder)
+ if (!pipeline->decoders.active || atomic_load(&pipeline->failed))
return;
- if (pipeline->cedar) {
- GoCedarFrame frame;
- int result = go_cedar_library_feed(pipeline->cedar, data, length, &frame);
- if (result > 0 && publish_cedar_frame(pipeline, &frame) == 0)
- return;
- if (result == 0)
- return;
- fprintf(stderr, "Cedar decoder disabled: %s\n", go_cedar_library_error(pipeline->cedar));
- go_cedar_library_close(pipeline->cedar);
- pipeline->cedar = NULL;
- atomic_store(&pipeline->keyframe_pending, 1);
- atomic_store(&pipeline->synced, 0);
- }
- feed_software_decoder(pipeline, data, (int)length);
+ if (decode_with_active_backend(pipeline, data, length) == 0)
+ return;
+ if (switch_to_software_decoder(pipeline) != 0)
+ mark_decoder_failed(pipeline);
}
static void process_packet(GoVideoPipeline* pipeline, const uint8_t* packet, size_t length) {
@@ -272,6 +367,8 @@ static void* video_worker(void* context) {
(pipeline->packet_queue_head + 1) % VIDEO_PACKET_QUEUE_CAPACITY;
pipeline->packet_queue_count--;
pthread_mutex_unlock(&pipeline->packet_lock);
+ if (atomic_exchange(&pipeline->restart_epoch_pending, 0))
+ go_h264_depacketizer_restart_decode_epoch(pipeline->depacketizer);
process_packet(pipeline, packet.data, packet.length);
}
return NULL;
@@ -367,19 +464,25 @@ static int upload_frame(GoVideoPipeline* pipeline, const AVFrame* frame) {
return SDL_UpdateTexture(pipeline->texture, NULL, pipeline->rgb_buffer, pipeline->rgb_linesize);
}
-GoVideoPipeline* go_video_pipeline_create(SDL_Renderer* renderer, const char* bootstrap_path) {
- if (!renderer)
+GoVideoPipeline* go_video_pipeline_create(const GoVideoPipelineConfig* config) {
+ if (!config || !config->renderer || config->max_width <= 0 || config->max_height <= 0 ||
+ config->max_width > 8192 || config->max_height > 8192 ||
+ config->decoder_preference < GO_VIDEO_DECODER_PREFERENCE_AUTO ||
+ config->decoder_preference > GO_VIDEO_DECODER_PREFERENCE_SOFTWARE)
return NULL;
GoVideoPipeline* pipeline = calloc(1, sizeof(*pipeline));
if (!pipeline)
return NULL;
- pipeline->renderer = renderer;
+ pipeline->renderer = config->renderer;
+ pipeline->max_width = config->max_width;
+ pipeline->max_height = config->max_height;
pipeline->direct_nv12_available = 1;
pipeline->texture_format = SDL_PIXELFORMAT_UNKNOWN;
atomic_store(&pipeline->keyframe_pending, 1);
atomic_store(&pipeline->last_rejected_payload_type, -1);
- if (bootstrap_path && snprintf(pipeline->bootstrap_path, sizeof(pipeline->bootstrap_path), "%s",
- bootstrap_path) >= (int)sizeof(pipeline->bootstrap_path)) {
+ if (config->bootstrap_path &&
+ snprintf(pipeline->bootstrap_path, sizeof(pipeline->bootstrap_path), "%s",
+ config->bootstrap_path) >= (int)sizeof(pipeline->bootstrap_path)) {
free(pipeline);
return NULL;
}
@@ -399,41 +502,24 @@ GoVideoPipeline* go_video_pipeline_create(SDL_Renderer* renderer, const char* bo
}
pipeline->frame_lock_initialized = 1;
pipeline->depacketizer = go_h264_depacketizer_create(GO_VIDEO_PAYLOAD_TYPE);
- pipeline->codec = avcodec_find_decoder(AV_CODEC_ID_H264);
- if (!pipeline->depacketizer || !pipeline->codec) {
- go_video_pipeline_destroy(pipeline);
- return NULL;
- }
- pipeline->decoder = avcodec_alloc_context3(pipeline->codec);
- if (!pipeline->decoder) {
+ if (!pipeline->depacketizer) {
go_video_pipeline_destroy(pipeline);
return NULL;
}
- pipeline->decoder->thread_count = 3;
- pipeline->decoder->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
- pipeline->decoder->skip_frame = AVDISCARD_NONREF;
- if (load_bootstrap(pipeline) != 0 ||
- avcodec_open2(pipeline->decoder, pipeline->codec, NULL) < 0) {
+ if (load_bootstrap(pipeline) != 0) {
go_video_pipeline_destroy(pipeline);
return NULL;
}
- pipeline->decoder_packet = av_packet_alloc();
- pipeline->decoder_frame = av_frame_alloc();
- pipeline->cedar_frame = av_frame_alloc();
+ pipeline->decoded_frame = av_frame_alloc();
pipeline->display_frame = av_frame_alloc();
pipeline->render_frame = av_frame_alloc();
- if (!pipeline->decoder_packet || !pipeline->decoder_frame || !pipeline->cedar_frame ||
- !pipeline->display_frame || !pipeline->render_frame) {
+ if (!pipeline->decoded_frame || !pipeline->display_frame || !pipeline->render_frame) {
go_video_pipeline_destroy(pipeline);
return NULL;
}
- pipeline->cedar = go_cedar_library_open(1280, 720);
- if (go_cedar_library_ready(pipeline->cedar)) {
- go_dbg("H.264 decoder: Cedar hardware acceleration\n");
- } else {
- go_dbg("H.264 decoder: software fallback (%s)\n", go_cedar_library_error(pipeline->cedar));
- go_cedar_library_close(pipeline->cedar);
- pipeline->cedar = NULL;
+ if (select_decoder(pipeline, config->decoder_preference) != 0) {
+ go_video_pipeline_destroy(pipeline);
+ return NULL;
}
return pipeline;
}
@@ -484,6 +570,7 @@ void go_video_pipeline_push_rtp(GoVideoPipeline* pipeline, const uint8_t* packet
if (length > VIDEO_PACKET_MAX_SIZE) {
atomic_fetch_add(&pipeline->dropped_packets, 1);
atomic_store(&pipeline->keyframe_pending, 1);
+ atomic_store(&pipeline->restart_epoch_pending, 1);
return;
}
pthread_mutex_lock(&pipeline->packet_lock);
@@ -493,6 +580,7 @@ void go_video_pipeline_push_rtp(GoVideoPipeline* pipeline, const uint8_t* packet
pipeline->packet_queue_count--;
atomic_fetch_add(&pipeline->dropped_packets, 1);
atomic_store(&pipeline->keyframe_pending, 1);
+ atomic_store(&pipeline->restart_epoch_pending, 1);
}
VideoPacket* target = &pipeline->packet_queue[pipeline->packet_queue_tail];
target->length = (uint16_t)length;
@@ -558,6 +646,10 @@ int go_video_pipeline_has_media(const GoVideoPipeline* pipeline) {
return pipeline && atomic_load(&pipeline->rtp_packets) > 0;
}
+int go_video_pipeline_failed(const GoVideoPipeline* pipeline) {
+ return pipeline && atomic_load(&pipeline->failed);
+}
+
void go_video_pipeline_note_keyframe_request(GoVideoPipeline* pipeline) {
if (pipeline)
atomic_fetch_add(&pipeline->keyframe_requests, 1);
@@ -586,6 +678,12 @@ GoVideoStats go_video_pipeline_stats(GoVideoPipeline* pipeline) {
stats.decoder_send_errors = atomic_load(&pipeline->decoder_send_errors);
stats.decoder_receive_errors = atomic_load(&pipeline->decoder_receive_errors);
stats.last_decoder_error = atomic_load(&pipeline->last_decoder_error);
+ stats.decoder_backend = atomic_load(&pipeline->decoder_backend);
+ stats.decoder_init_failures = atomic_load(&pipeline->decoder_init_failures);
+ stats.decoder_runtime_fallbacks = atomic_load(&pipeline->decoder_runtime_fallbacks);
+ stats.decoder_backpressure_events = atomic_load(&pipeline->decoder_backpressure_events);
+ stats.decoder_corrupt_frames = atomic_load(&pipeline->decoder_corrupt_frames);
+ stats.decoder_info_changes = atomic_load(&pipeline->decoder_info_changes);
stats.keyframe_requests = atomic_load(&pipeline->keyframe_requests);
stats.dropped_packets = atomic_load(&pipeline->dropped_packets);
pthread_mutex_lock(&pipeline->packet_lock);
@@ -614,16 +712,9 @@ int go_video_pipeline_destroy(GoVideoPipeline* pipeline) {
sws_freeContext(pipeline->scaler);
free(pipeline->rgb_buffer);
destroy_texture(pipeline);
- if (pipeline->cedar)
- go_cedar_library_close(pipeline->cedar);
- if (pipeline->cedar_frame)
- av_frame_free(&pipeline->cedar_frame);
- if (pipeline->decoder_frame)
- av_frame_free(&pipeline->decoder_frame);
- if (pipeline->decoder_packet)
- av_packet_free(&pipeline->decoder_packet);
- if (pipeline->decoder)
- avcodec_free_context(&pipeline->decoder);
+ go_video_decoder_selection_destroy(&pipeline->decoders);
+ if (pipeline->decoded_frame)
+ av_frame_free(&pipeline->decoded_frame);
if (pipeline->depacketizer)
go_h264_depacketizer_destroy(pipeline->depacketizer);
if (pipeline->frame_lock_initialized)
diff --git a/src/media/video/video_pipeline.h b/src/media/video/video_pipeline.h
index 27c6ff8..578f822 100644
--- a/src/media/video/video_pipeline.h
+++ b/src/media/video/video_pipeline.h
@@ -5,6 +5,8 @@
#include
#include
+#include "video_decoder.h"
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -13,6 +15,14 @@ extern "C" {
typedef struct GoVideoPipeline GoVideoPipeline;
+typedef struct {
+ SDL_Renderer* renderer;
+ const char* bootstrap_path;
+ int max_width;
+ int max_height;
+ GoVideoDecoderPreference decoder_preference;
+} GoVideoPipelineConfig;
+
typedef struct {
int rtp_packets;
int payload_packets;
@@ -33,12 +43,18 @@ typedef struct {
int decoder_send_errors;
int decoder_receive_errors;
int last_decoder_error;
+ GoVideoDecoderBackend decoder_backend;
+ int decoder_init_failures;
+ int decoder_runtime_fallbacks;
+ int decoder_backpressure_events;
+ int decoder_corrupt_frames;
+ int decoder_info_changes;
int keyframe_requests;
int pending_packets;
int dropped_packets;
} GoVideoStats;
-GoVideoPipeline* go_video_pipeline_create(SDL_Renderer* renderer, const char* bootstrap_path);
+GoVideoPipeline* go_video_pipeline_create(const GoVideoPipelineConfig* config);
int go_video_pipeline_start(GoVideoPipeline* pipeline);
void go_video_pipeline_stop(GoVideoPipeline* pipeline);
void go_video_pipeline_set_crop_aspect(GoVideoPipeline* pipeline, unsigned int width,
@@ -47,6 +63,7 @@ void go_video_pipeline_push_rtp(GoVideoPipeline* pipeline, const uint8_t* packet
void go_video_pipeline_render(GoVideoPipeline* pipeline);
int go_video_pipeline_needs_keyframe(const GoVideoPipeline* pipeline);
int go_video_pipeline_has_media(const GoVideoPipeline* pipeline);
+int go_video_pipeline_failed(const GoVideoPipeline* pipeline);
void go_video_pipeline_note_keyframe_request(GoVideoPipeline* pipeline);
GoVideoStats go_video_pipeline_stats(GoVideoPipeline* pipeline);
int go_video_pipeline_destroy(GoVideoPipeline* pipeline);
diff --git a/tests/cedar_fake_valid.c b/tests/cedar_fake_valid.c
new file mode 100644
index 0000000..2d5a610
--- /dev/null
+++ b/tests/cedar_fake_valid.c
@@ -0,0 +1,53 @@
+#include "cedar_decoder.h"
+
+#include
+#include
+
+struct GoCedarDecoder {
+ uint8_t frame[48];
+ char error[64];
+};
+
+GoCedarDecoder* go_cedar_v1_create(int width, int height) {
+ if (width == 13 || width <= 0 || height <= 0)
+ return NULL;
+ GoCedarDecoder* decoder = calloc(1, sizeof(*decoder));
+ if (decoder)
+ memset(decoder->frame, 0x80, sizeof(decoder->frame));
+ return decoder;
+}
+
+int go_cedar_v1_feed(GoCedarDecoder* decoder, const uint8_t* data, size_t length,
+ GoCedarFrame* frame) {
+ if (!decoder || !data || length == 0 || !frame)
+ return -1;
+ memset(frame, 0, sizeof(*frame));
+ if (data[0] == 0xfe)
+ return 0;
+ if (data[0] == 0xff) {
+ strcpy(decoder->error, "fake Cedar runtime failure");
+ return -1;
+ }
+ frame->y = decoder->frame;
+ frame->uv = decoder->frame + 32;
+ frame->width = 4;
+ frame->height = 4;
+ frame->y_stride = 8;
+ frame->uv_stride = 8;
+ return 1;
+}
+
+int go_cedar_v1_flush(GoCedarDecoder* decoder, GoCedarFrame* frame) {
+ if (!decoder || !frame)
+ return -1;
+ memset(frame, 0, sizeof(*frame));
+ return 0;
+}
+
+const char* go_cedar_v1_last_error(const GoCedarDecoder* decoder) {
+ return decoder && decoder->error[0] ? decoder->error : "fake Cedar failure";
+}
+
+void go_cedar_v1_destroy(GoCedarDecoder* decoder) {
+ free(decoder);
+}
diff --git a/tests/device/mpp_bridge_test.c b/tests/device/mpp_bridge_test.c
new file mode 100644
index 0000000..8a15414
--- /dev/null
+++ b/tests/device/mpp_bridge_test.c
@@ -0,0 +1,265 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpp_loader.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum { MPP_DRAIN_TIMEOUT_MS = 2000 };
+
+typedef struct {
+ int expected_width;
+ int expected_height;
+ int checksum_frame_limit;
+ int frames;
+ uint64_t checksum;
+} TestState;
+
+static int monotonic_milliseconds(int64_t* milliseconds) {
+ struct timespec now;
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
+ return -1;
+ *milliseconds = (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+ return 0;
+}
+
+static int wait_one_millisecond(void) {
+ struct timespec remaining = {.tv_sec = 0, .tv_nsec = 1000000};
+ while (nanosleep(&remaining, &remaining) != 0) {
+ if (errno != EINTR)
+ return -1;
+ }
+ return 0;
+}
+
+static uint64_t checksum_bytes(uint64_t value, const uint8_t* data, size_t length) {
+ for (size_t index = 0; index < length; ++index) {
+ value ^= data[index];
+ value *= UINT64_C(1099511628211);
+ }
+ return value;
+}
+
+static int drain_frames(GoMppLibrary* library, TestState* state) {
+ for (;;) {
+ GoMppFrame frame;
+ int result = go_mpp_library_receive(library, &frame);
+ if (result == 0)
+ return 0;
+ if (result < 0) {
+ fprintf(stderr, "MPP frame receive failed: %s\n", go_mpp_library_error(library));
+ return -1;
+ }
+ if (!frame.y || !frame.uv || frame.width != state->expected_width ||
+ frame.height != state->expected_height || frame.y_stride < frame.width ||
+ frame.uv_stride < frame.width) {
+ fprintf(stderr, "invalid MPP frame: %dx%d stride=%d/%d\n", frame.width, frame.height,
+ frame.y_stride, frame.uv_stride);
+ go_mpp_library_release_frame(library, &frame);
+ return -1;
+ }
+ if (state->frames < state->checksum_frame_limit) {
+ for (int row = 0; row < frame.height; ++row) {
+ state->checksum =
+ checksum_bytes(state->checksum, frame.y + (size_t)row * (size_t)frame.y_stride,
+ (size_t)frame.width);
+ }
+ for (int row = 0; row < (frame.height + 1) / 2; ++row) {
+ state->checksum = checksum_bytes(state->checksum,
+ frame.uv + (size_t)row * (size_t)frame.uv_stride,
+ (size_t)frame.width);
+ }
+ }
+ state->frames++;
+ go_mpp_library_release_frame(library, &frame);
+ }
+}
+
+static int submit_access_unit(GoMppLibrary* library, const uint8_t* data, size_t length,
+ TestState* state) {
+ for (int attempt = 0; attempt < 256; ++attempt) {
+ int result = go_mpp_library_submit(library, data, length);
+ if (result > 0)
+ return drain_frames(library, state);
+ if (result < 0) {
+ fprintf(stderr, "MPP access-unit submission failed: %s\n",
+ go_mpp_library_error(library));
+ return -1;
+ }
+ if (drain_frames(library, state) != 0)
+ return -1;
+ if (wait_one_millisecond() != 0)
+ return -1;
+ }
+ fprintf(stderr, "MPP input remained backpressured\n");
+ return -1;
+}
+
+static int drain_until_minimum(GoMppLibrary* library, TestState* state, int minimum_frames) {
+ int64_t now;
+ if (monotonic_milliseconds(&now) != 0)
+ return -1;
+ int64_t deadline = now + MPP_DRAIN_TIMEOUT_MS;
+ while (state->frames < minimum_frames) {
+ if (drain_frames(library, state) != 0)
+ return -1;
+ if (state->frames >= minimum_frames)
+ break;
+ if (monotonic_milliseconds(&now) != 0)
+ return -1;
+ if (now >= deadline)
+ break;
+ if (wait_one_millisecond() != 0)
+ return -1;
+ }
+ return 0;
+}
+
+static size_t start_code_length(const uint8_t* data, size_t length, size_t offset) {
+ if (offset + 3 <= length && data[offset] == 0 && data[offset + 1] == 0 && data[offset + 2] == 1)
+ return 3;
+ if (offset + 4 <= length && data[offset] == 0 && data[offset + 1] == 0 &&
+ data[offset + 2] == 0 && data[offset + 3] == 1)
+ return 4;
+ return 0;
+}
+
+static size_t find_next_aud(const uint8_t* data, size_t length, size_t offset) {
+ while (offset + 4 <= length) {
+ size_t prefix = start_code_length(data, length, offset);
+ if (prefix > 0 && offset + prefix < length && (data[offset + prefix] & 0x1f) == 9)
+ return offset;
+ offset++;
+ }
+ return length;
+}
+
+static uint8_t* read_file(const char* path, size_t* length) {
+ FILE* file = fopen(path, "rb");
+ if (!file) {
+ fprintf(stderr, "%s: %s\n", path, strerror(errno));
+ return NULL;
+ }
+ if (fseek(file, 0, SEEK_END) != 0) {
+ fclose(file);
+ return NULL;
+ }
+ long size = ftell(file);
+ if (size <= 0 || fseek(file, 0, SEEK_SET) != 0) {
+ fclose(file);
+ return NULL;
+ }
+ uint8_t* data = malloc((size_t)size);
+ if (!data) {
+ fclose(file);
+ return NULL;
+ }
+ size_t read = fread(data, 1, (size_t)size, file);
+ int failed = read != (size_t)size || ferror(file);
+ fclose(file);
+ if (failed) {
+ free(data);
+ return NULL;
+ }
+ *length = read;
+ return data;
+}
+
+static int parse_positive_int(const char* value) {
+ char* end = NULL;
+ long parsed = strtol(value, &end, 10);
+ return end && *end == 0 && parsed > 0 && parsed <= INT32_MAX ? (int)parsed : -1;
+}
+
+static int parse_checksum(const char* value, uint64_t* output) {
+ char* end = NULL;
+ errno = 0;
+ unsigned long long parsed = strtoull(value, &end, 16);
+ if (errno != 0 || !end || *end != 0)
+ return -1;
+ *output = (uint64_t)parsed;
+ return 0;
+}
+
+int main(int argc, char** argv) {
+ if (argc != 5 && argc != 6) {
+ fprintf(stderr,
+ "usage: %s "
+ "[expected-checksum]\n",
+ argv[0]);
+ return 2;
+ }
+ TestState state = {
+ .expected_width = parse_positive_int(argv[2]),
+ .expected_height = parse_positive_int(argv[3]),
+ .checksum = UINT64_C(1469598103934665603),
+ };
+ int minimum_frames = parse_positive_int(argv[4]);
+ state.checksum_frame_limit = minimum_frames;
+ if (state.expected_width < 0 || state.expected_height < 0 || minimum_frames < 0)
+ return 2;
+ uint64_t expected_checksum = 0;
+ if (argc == 6 && parse_checksum(argv[5], &expected_checksum) != 0)
+ return 2;
+
+ size_t length = 0;
+ uint8_t* data = read_file(argv[1], &length);
+ if (!data) {
+ fprintf(stderr, "failed to read H.264 fixture\n");
+ return 1;
+ }
+
+ GoMppLibrary* library = go_mpp_library_open(state.expected_width, state.expected_height);
+ if (!go_mpp_library_ready(library)) {
+ fprintf(stderr, "MPP H.264 initialization failed: %s\n", go_mpp_library_error(library));
+ go_mpp_library_close(library);
+ free(data);
+ return 1;
+ }
+
+ size_t first_aud = find_next_aud(data, length, 0);
+ if (first_aud == length) {
+ fprintf(stderr, "fixture contains no H.264 access-unit delimiters\n");
+ go_mpp_library_close(library);
+ free(data);
+ return 1;
+ }
+ size_t access_unit_start = 0;
+ size_t next_aud = find_next_aud(data, length, first_aud + 4);
+ int failed = 0;
+ while (access_unit_start < length) {
+ size_t access_unit_end = next_aud;
+ if (access_unit_end <= access_unit_start ||
+ submit_access_unit(library, data + access_unit_start,
+ access_unit_end - access_unit_start, &state) != 0) {
+ failed = 1;
+ break;
+ }
+ if (access_unit_end == length)
+ break;
+ access_unit_start = access_unit_end;
+ next_aud = find_next_aud(data, length, access_unit_start + 4);
+ }
+ if (!failed && drain_until_minimum(library, &state, minimum_frames) != 0)
+ failed = 1;
+
+ go_mpp_library_close(library);
+ free(data);
+ if (failed || state.frames < minimum_frames ||
+ (argc == 6 && state.checksum != expected_checksum)) {
+ fprintf(stderr, "decoded %d frames; expected at least %d\n", state.frames, minimum_frames);
+ if (argc == 6)
+ fprintf(stderr, "visible NV12 checksum: %016" PRIx64 "; expected %016" PRIx64 "\n",
+ state.checksum, expected_checksum);
+ return 1;
+ }
+ printf("decoded frames: %d\n", state.frames);
+ printf("visible frame: %dx%d\n", state.expected_width, state.expected_height);
+ printf("first %d frames NV12 checksum: %016" PRIx64 "\n", minimum_frames, state.checksum);
+ return 0;
+}
diff --git a/tests/device/mpp_probe.c b/tests/device/mpp_probe.c
new file mode 100644
index 0000000..bc343fa
--- /dev/null
+++ b/tests/device/mpp_probe.c
@@ -0,0 +1,16 @@
+#include "mpp_loader.h"
+
+#include
+
+int main(void) {
+ GoMppLibrary* library = go_mpp_library_open(1280, 720);
+ printf("MPP plugin: %s\n", go_mpp_library_path(library));
+ if (!go_mpp_library_ready(library)) {
+ fprintf(stderr, "MPP H.264 initialization failed: %s\n", go_mpp_library_error(library));
+ go_mpp_library_close(library);
+ return 1;
+ }
+ printf("MPP H.264 initialization: success\n");
+ go_mpp_library_close(library);
+ return 0;
+}
diff --git a/tests/mpp_fake_missing_symbol.c b/tests/mpp_fake_missing_symbol.c
new file mode 100644
index 0000000..6a46a16
--- /dev/null
+++ b/tests/mpp_fake_missing_symbol.c
@@ -0,0 +1,37 @@
+#include "mpp_decoder.h"
+
+#include
+
+struct GoMppDecoder {
+ int unused;
+};
+
+uint32_t go_mpp_decoder_abi_version(void) {
+ return GO_MPP_DECODER_ABI_VERSION;
+}
+
+GoMppDecoder* go_mpp_decoder_create(int max_width, int max_height) {
+ return max_width > 0 && max_height > 0 ? calloc(1, sizeof(GoMppDecoder)) : NULL;
+}
+
+int go_mpp_decoder_submit(GoMppDecoder* decoder, const uint8_t* data, size_t length) {
+ return decoder && data && length > 0;
+}
+
+int go_mpp_decoder_receive(GoMppDecoder* decoder, GoMppFrame* frame) {
+ return decoder && frame ? 0 : -1;
+}
+
+void go_mpp_decoder_release_frame(GoMppDecoder* decoder, GoMppFrame* frame) {
+ (void)decoder;
+ (void)frame;
+}
+
+const char* go_mpp_decoder_last_error(GoMppDecoder* decoder) {
+ (void)decoder;
+ return "fake error";
+}
+
+void go_mpp_decoder_destroy(GoMppDecoder* decoder) {
+ free(decoder);
+}
diff --git a/tests/mpp_fake_valid.c b/tests/mpp_fake_valid.c
new file mode 100644
index 0000000..fe2ef22
--- /dev/null
+++ b/tests/mpp_fake_valid.c
@@ -0,0 +1,71 @@
+#include "mpp_decoder.h"
+
+#include
+#include
+
+struct GoMppDecoder {
+ int frame_ready;
+ const char* error;
+};
+
+uint32_t go_mpp_decoder_abi_version(void) {
+ return GO_MPP_DECODER_ABI_VERSION;
+}
+
+GoMppDecoder* go_mpp_decoder_create(int max_width, int max_height) {
+ if (max_width == 13 || max_width <= 0 || max_height <= 0)
+ return NULL;
+ return calloc(1, sizeof(GoMppDecoder));
+}
+
+int go_mpp_decoder_submit(GoMppDecoder* decoder, const uint8_t* data, size_t length) {
+ if (!decoder || !data || length == 0)
+ return -1;
+ if (data[0] == 0xfe)
+ return 0;
+ if (data[0] == 0xff) {
+ decoder->error = "fake runtime failure";
+ return -1;
+ }
+ decoder->frame_ready = 1;
+ return 1;
+}
+
+int go_mpp_decoder_receive(GoMppDecoder* decoder, GoMppFrame* frame) {
+ static uint8_t y[16];
+ static uint8_t uv[8];
+ if (!decoder || !frame)
+ return -1;
+ if (!decoder->frame_ready)
+ return 0;
+ memset(frame, 0, sizeof(*frame));
+ frame->y = y;
+ frame->uv = uv;
+ frame->width = 4;
+ frame->height = 4;
+ frame->y_stride = 4;
+ frame->uv_stride = 4;
+ frame->owner = decoder;
+ decoder->frame_ready = 0;
+ return 1;
+}
+
+void go_mpp_decoder_release_frame(GoMppDecoder* decoder, GoMppFrame* frame) {
+ if (decoder && frame && frame->owner == decoder)
+ memset(frame, 0, sizeof(*frame));
+}
+
+int go_mpp_decoder_reset(GoMppDecoder* decoder) {
+ if (!decoder)
+ return -1;
+ decoder->frame_ready = 0;
+ return 0;
+}
+
+const char* go_mpp_decoder_last_error(GoMppDecoder* decoder) {
+ return decoder && decoder->error ? decoder->error : "fake initialization failed";
+}
+
+void go_mpp_decoder_destroy(GoMppDecoder* decoder) {
+ free(decoder);
+}
diff --git a/tests/mpp_fake_wrong_abi.c b/tests/mpp_fake_wrong_abi.c
new file mode 100644
index 0000000..a824c21
--- /dev/null
+++ b/tests/mpp_fake_wrong_abi.c
@@ -0,0 +1,5 @@
+#include "mpp_decoder.h"
+
+uint32_t go_mpp_decoder_abi_version(void) {
+ return GO_MPP_DECODER_ABI_VERSION + 1;
+}
diff --git a/tests/mpp_loader_test.c b/tests/mpp_loader_test.c
new file mode 100644
index 0000000..d8a427c
--- /dev/null
+++ b/tests/mpp_loader_test.c
@@ -0,0 +1,56 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "mpp_loader.h"
+
+#include
+#include
+#include
+
+static GoMppLibrary* open_plugin(const char* path, int width) {
+ assert(setenv("GREENOVERCAST_MPP_LIBRARY", path, 1) == 0);
+ return go_mpp_library_open(width, 720);
+}
+
+int main(int argc, char** argv) {
+ assert(argc == 4);
+
+ GoMppLibrary* valid = open_plugin(argv[1], 1280);
+ assert(go_mpp_library_ready(valid));
+ assert(strcmp(go_mpp_library_path(valid), argv[1]) == 0);
+ const uint8_t access_unit[] = {0, 0, 0, 1, 0x65};
+ assert(go_mpp_library_submit(valid, access_unit, sizeof(access_unit)) == 1);
+ GoMppFrame frame;
+ assert(go_mpp_library_receive(valid, &frame) == 1);
+ assert(frame.width == 4);
+ assert(frame.height == 4);
+ go_mpp_library_release_frame(valid, &frame);
+ assert(frame.owner == NULL);
+ assert(go_mpp_library_receive(valid, &frame) == 0);
+ assert(go_mpp_library_reset(valid) == 0);
+ const uint8_t failure[] = {0xff};
+ assert(go_mpp_library_submit(valid, failure, sizeof(failure)) == -1);
+ assert(strstr(go_mpp_library_error(valid), "fake runtime failure") != NULL);
+ go_mpp_library_close(valid);
+
+ GoMppLibrary* create_failure = open_plugin(argv[1], 13);
+ assert(!go_mpp_library_ready(create_failure));
+ assert(strstr(go_mpp_library_error(create_failure), "initialization failed") != NULL);
+ go_mpp_library_close(create_failure);
+
+ GoMppLibrary* wrong_abi = open_plugin(argv[2], 1280);
+ assert(!go_mpp_library_ready(wrong_abi));
+ assert(strstr(go_mpp_library_error(wrong_abi), "incompatible") != NULL);
+ go_mpp_library_close(wrong_abi);
+
+ GoMppLibrary* missing_symbol = open_plugin(argv[3], 1280);
+ assert(!go_mpp_library_ready(missing_symbol));
+ assert(strstr(go_mpp_library_error(missing_symbol), "go_mpp_decoder_reset") != NULL);
+ go_mpp_library_close(missing_symbol);
+
+ GoMppLibrary* missing_library = open_plugin("/not/a/real/mpp-plugin.so", 1280);
+ assert(!go_mpp_library_ready(missing_library));
+ assert(go_mpp_library_error(missing_library)[0] != 0);
+ go_mpp_library_close(missing_library);
+ unsetenv("GREENOVERCAST_MPP_LIBRARY");
+ return 0;
+}
diff --git a/tests/video_decoder_cedar_test.c b/tests/video_decoder_cedar_test.c
new file mode 100644
index 0000000..d2b42fa
--- /dev/null
+++ b/tests/video_decoder_cedar_test.c
@@ -0,0 +1,56 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "video_decoder.h"
+
+#include
+#include
+#include
+
+int main(int argc, char** argv) {
+ assert(argc == 2);
+ assert(setenv("GREENOVERCAST_CEDAR_LIBRARY", argv[1], 1) == 0);
+
+ char error[256] = {0};
+ GoVideoDecoder* decoder = go_video_decoder_cedar_create(1280, 720, error, sizeof(error));
+ assert(decoder != NULL);
+ assert(go_video_decoder_backend(decoder) == GO_VIDEO_DECODER_BACKEND_CEDAR);
+
+ const uint8_t no_frame[] = {0xfe};
+ assert(go_video_decoder_submit_access_unit(decoder, no_frame, sizeof(no_frame)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+ GoDecodedVideoFrame frame;
+ assert(go_video_decoder_receive_frame(decoder, &frame) == GO_VIDEO_DECODER_RESULT_AGAIN);
+
+ const uint8_t access_unit[] = {0, 0, 0, 1, 0x65};
+ assert(go_video_decoder_submit_access_unit(decoder, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+ assert(go_video_decoder_receive_frame(decoder, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ assert(frame.format == GO_VIDEO_PIXEL_FORMAT_NV12);
+ assert(frame.width == 4 && frame.height == 4);
+ assert(frame.strides[0] == 8 && frame.strides[1] == 8);
+ assert(frame.info_changed == 1);
+ assert(go_video_decoder_submit_access_unit(decoder, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_AGAIN);
+ assert(go_video_decoder_reset(decoder) == -1);
+ assert(strstr(go_video_decoder_last_error(decoder), "frame outstanding") != NULL);
+ go_video_decoder_release_frame(decoder, &frame);
+
+ assert(go_video_decoder_reset(decoder) == 0);
+ assert(go_video_decoder_submit_access_unit(decoder, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+ assert(go_video_decoder_receive_frame(decoder, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ assert(frame.info_changed == 1);
+ go_video_decoder_release_frame(decoder, &frame);
+
+ const uint8_t failure[] = {0xff};
+ assert(go_video_decoder_submit_access_unit(decoder, failure, sizeof(failure)) ==
+ GO_VIDEO_DECODER_RESULT_FATAL);
+ assert(strstr(go_video_decoder_last_error(decoder), "fake Cedar runtime failure") != NULL);
+ go_video_decoder_destroy(decoder);
+
+ decoder = go_video_decoder_cedar_create(13, 720, error, sizeof(error));
+ assert(decoder == NULL);
+ assert(strstr(error, "initialization failed") != NULL);
+ unsetenv("GREENOVERCAST_CEDAR_LIBRARY");
+ return 0;
+}
diff --git a/tests/video_decoder_mpp_test.c b/tests/video_decoder_mpp_test.c
new file mode 100644
index 0000000..5b15959
--- /dev/null
+++ b/tests/video_decoder_mpp_test.c
@@ -0,0 +1,63 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "video_decoder.h"
+
+#include
+#include
+#include
+
+int main(int argc, char** argv) {
+ assert(argc == 2);
+ assert(setenv("GREENOVERCAST_MPP_LIBRARY", argv[1], 1) == 0);
+
+ char error[256] = {0};
+ GoVideoDecoder* decoder = go_video_decoder_mpp_create(1280, 720, error, sizeof(error));
+ assert(decoder != NULL);
+ assert(go_video_decoder_backend(decoder) == GO_VIDEO_DECODER_BACKEND_MPP);
+
+ const uint8_t backpressure[] = {0xfe};
+ assert(go_video_decoder_submit_access_unit(decoder, backpressure, sizeof(backpressure)) ==
+ GO_VIDEO_DECODER_RESULT_AGAIN);
+
+ const uint8_t access_unit[] = {0, 0, 0, 1, 0x65};
+ assert(go_video_decoder_submit_access_unit(decoder, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+ GoDecodedVideoFrame frame;
+ assert(go_video_decoder_receive_frame(decoder, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ assert(frame.format == GO_VIDEO_PIXEL_FORMAT_NV12);
+ assert(frame.width == 4 && frame.height == 4);
+ assert(frame.strides[0] == 4 && frame.strides[1] == 4);
+ assert(frame.info_changed == 1);
+ assert(frame.backend_frame != NULL);
+ assert(go_video_decoder_reset(decoder) == -1);
+ assert(strstr(go_video_decoder_last_error(decoder), "frame outstanding") != NULL);
+ go_video_decoder_release_frame(decoder, &frame);
+ assert(frame.backend_frame == NULL);
+
+ assert(go_video_decoder_submit_access_unit(decoder, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+ assert(go_video_decoder_receive_frame(decoder, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ assert(frame.info_changed == 0);
+ go_video_decoder_release_frame(decoder, &frame);
+
+ assert(go_video_decoder_reset(decoder) == 0);
+ assert(go_video_decoder_submit_access_unit(decoder, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+ assert(go_video_decoder_receive_frame(decoder, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ assert(frame.info_changed == 1);
+ go_video_decoder_release_frame(decoder, &frame);
+
+ const uint8_t failure[] = {0xff};
+ assert(go_video_decoder_submit_access_unit(decoder, failure, sizeof(failure)) ==
+ GO_VIDEO_DECODER_RESULT_FATAL);
+ assert(strstr(go_video_decoder_last_error(decoder), "fake runtime failure") != NULL);
+
+ go_video_decoder_destroy(decoder);
+
+ memset(error, 0, sizeof(error));
+ decoder = go_video_decoder_mpp_create(13, 720, error, sizeof(error));
+ assert(decoder == NULL);
+ assert(strstr(error, "initialization failed") != NULL);
+ unsetenv("GREENOVERCAST_MPP_LIBRARY");
+ return 0;
+}
diff --git a/tests/video_decoder_selection_test.c b/tests/video_decoder_selection_test.c
new file mode 100644
index 0000000..0117fba
--- /dev/null
+++ b/tests/video_decoder_selection_test.c
@@ -0,0 +1,166 @@
+#include "video_decoder.h"
+
+#include
+#include
+#include
+
+typedef struct {
+ GoVideoDecoder base;
+ GoVideoDecoderBackend backend;
+ int resets;
+ int destroys;
+} FakeDecoder;
+
+static FakeDecoder software;
+static FakeDecoder cedar;
+static FakeDecoder mpp;
+static int cedar_available = 1;
+static int mpp_available = 1;
+
+static GoVideoDecoderResult fake_submit(GoVideoDecoder* decoder, const uint8_t* data,
+ size_t length) {
+ (void)decoder;
+ (void)data;
+ (void)length;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static GoVideoDecoderResult fake_receive(GoVideoDecoder* decoder, GoDecodedVideoFrame* frame) {
+ (void)decoder;
+ (void)frame;
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+}
+
+static void fake_release(GoVideoDecoder* decoder, GoDecodedVideoFrame* frame) {
+ (void)decoder;
+ (void)frame;
+}
+
+static int fake_reset(GoVideoDecoder* decoder) {
+ ((FakeDecoder*)decoder)->resets++;
+ return 0;
+}
+
+static const char* fake_error(const GoVideoDecoder* decoder) {
+ (void)decoder;
+ return "fake decoder failure";
+}
+
+static void fake_destroy(GoVideoDecoder* decoder) {
+ ((FakeDecoder*)decoder)->destroys++;
+}
+
+static const GoVideoDecoderOps software_ops = {
+ .name = "fake-software",
+ .backend = GO_VIDEO_DECODER_BACKEND_SOFTWARE,
+ .submit_access_unit = fake_submit,
+ .receive_frame = fake_receive,
+ .release_frame = fake_release,
+ .reset = fake_reset,
+ .last_error = fake_error,
+ .destroy = fake_destroy,
+};
+
+static const GoVideoDecoderOps cedar_ops = {
+ .name = "fake-cedar",
+ .backend = GO_VIDEO_DECODER_BACKEND_CEDAR,
+ .submit_access_unit = fake_submit,
+ .receive_frame = fake_receive,
+ .release_frame = fake_release,
+ .reset = fake_reset,
+ .last_error = fake_error,
+ .destroy = fake_destroy,
+};
+
+static const GoVideoDecoderOps mpp_ops = {
+ .name = "fake-mpp",
+ .backend = GO_VIDEO_DECODER_BACKEND_MPP,
+ .submit_access_unit = fake_submit,
+ .receive_frame = fake_receive,
+ .release_frame = fake_release,
+ .reset = fake_reset,
+ .last_error = fake_error,
+ .destroy = fake_destroy,
+};
+
+static GoVideoDecoder* initialize(FakeDecoder* decoder, const GoVideoDecoderOps* ops) {
+ decoder->base.ops = ops;
+ return &decoder->base;
+}
+
+GoVideoDecoder* go_video_decoder_ffmpeg_create(char* error, size_t error_capacity) {
+ (void)error;
+ (void)error_capacity;
+ return initialize(&software, &software_ops);
+}
+
+GoVideoDecoder* go_video_decoder_cedar_create(int max_width, int max_height, char* error,
+ size_t error_capacity) {
+ (void)max_width;
+ (void)max_height;
+ if (!cedar_available) {
+ if (error && error_capacity > 0)
+ snprintf(error, error_capacity, "fake Cedar unavailable");
+ return NULL;
+ }
+ return initialize(&cedar, &cedar_ops);
+}
+
+GoVideoDecoder* go_video_decoder_mpp_create(int max_width, int max_height, char* error,
+ size_t error_capacity) {
+ (void)max_width;
+ (void)max_height;
+ if (mpp_available)
+ return initialize(&mpp, &mpp_ops);
+ if (error && error_capacity > 0)
+ snprintf(error, error_capacity, "fake MPP unavailable");
+ return NULL;
+}
+
+int main(void) {
+ GoVideoDecoderSelection selection;
+ char error[128];
+ GoVideoDecoderSelectionConfig config = {
+ .max_width = 1280,
+ .max_height = 720,
+ .preference = GO_VIDEO_DECODER_PREFERENCE_MPP,
+ };
+
+ assert(go_video_decoder_selection_create(&config, &selection, error, sizeof(error)) == 0);
+ assert(selection.active == &mpp.base);
+ assert(selection.software == NULL);
+ assert(selection.allow_runtime_fallback == 0);
+ go_video_decoder_selection_destroy(&selection);
+ assert(mpp.destroys == 1);
+
+ mpp_available = 0;
+ cedar_available = 0;
+ config.preference = GO_VIDEO_DECODER_PREFERENCE_AUTO;
+ assert(go_video_decoder_selection_create(&config, &selection, error, sizeof(error)) == 0);
+ assert(selection.active == &software.base);
+ assert(selection.software == &software.base);
+ go_video_decoder_selection_destroy(&selection);
+ assert(software.destroys == 1);
+
+ selection = (GoVideoDecoderSelection){
+ .active = initialize(&mpp, &mpp_ops),
+ .software = initialize(&software, &software_ops),
+ .allow_runtime_fallback = 1,
+ };
+ assert(go_video_decoder_selection_fallback(&selection, error, sizeof(error)) == 0);
+ assert(selection.active == &software.base);
+ assert(selection.allow_runtime_fallback == 0);
+ assert(mpp.destroys == 2);
+ assert(software.resets == 1);
+ go_video_decoder_selection_destroy(&selection);
+ assert(software.destroys == 2);
+
+ config.preference = GO_VIDEO_DECODER_PREFERENCE_MPP;
+ assert(go_video_decoder_selection_create(&config, &selection, error, sizeof(error)) == -1);
+ assert(strstr(error, "fake MPP unavailable") != NULL);
+
+ config.max_width = 0;
+ assert(go_video_decoder_selection_create(&config, &selection, error, sizeof(error)) == -1);
+ assert(strstr(error, "invalid video decoder configuration") != NULL);
+ return 0;
+}
diff --git a/tests/video_decoder_test.c b/tests/video_decoder_test.c
new file mode 100644
index 0000000..4689772
--- /dev/null
+++ b/tests/video_decoder_test.c
@@ -0,0 +1,150 @@
+#include "video_decoder.h"
+
+#include
+#include
+
+typedef struct {
+ GoVideoDecoder base;
+ GoVideoDecoderResult submit_result;
+ int frames_remaining;
+ int submitted;
+ int received;
+ int released;
+ int reset;
+ int destroyed;
+} FakeDecoder;
+
+static GoVideoDecoderResult fake_submit(GoVideoDecoder* base, const uint8_t* data, size_t length) {
+ FakeDecoder* decoder = (FakeDecoder*)base;
+ assert(data != NULL);
+ assert(length > 0);
+ decoder->submitted++;
+ return decoder->submit_result;
+}
+
+static GoVideoDecoderResult fake_receive(GoVideoDecoder* base, GoDecodedVideoFrame* frame) {
+ FakeDecoder* decoder = (FakeDecoder*)base;
+ if (decoder->frames_remaining == 0)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ decoder->frames_remaining--;
+ decoder->received++;
+ frame->format = GO_VIDEO_PIXEL_FORMAT_NV12;
+ frame->width = 1280;
+ frame->height = 720;
+ frame->backend_frame = decoder;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static void fake_release(GoVideoDecoder* base, GoDecodedVideoFrame* frame) {
+ FakeDecoder* decoder = (FakeDecoder*)base;
+ assert(frame->backend_frame == decoder);
+ decoder->released++;
+}
+
+static int fake_reset(GoVideoDecoder* base) {
+ FakeDecoder* decoder = (FakeDecoder*)base;
+ decoder->reset++;
+ return 0;
+}
+
+static const char* fake_last_error(const GoVideoDecoder* base) {
+ (void)base;
+ return "fake failure";
+}
+
+static void fake_destroy(GoVideoDecoder* base) {
+ FakeDecoder* decoder = (FakeDecoder*)base;
+ decoder->destroyed++;
+}
+
+static const GoVideoDecoderOps fake_ops = {
+ .name = "fake",
+ .backend = GO_VIDEO_DECODER_BACKEND_MPP,
+ .submit_access_unit = fake_submit,
+ .receive_frame = fake_receive,
+ .release_frame = fake_release,
+ .reset = fake_reset,
+ .last_error = fake_last_error,
+ .destroy = fake_destroy,
+};
+
+int main(void) {
+ GoVideoDecoderPreference preference;
+ assert(go_video_decoder_preference_parse(NULL, &preference) == 0);
+ assert(preference == GO_VIDEO_DECODER_PREFERENCE_AUTO);
+ assert(go_video_decoder_preference_parse("mpp", &preference) == 0);
+ assert(preference == GO_VIDEO_DECODER_PREFERENCE_MPP);
+ assert(go_video_decoder_preference_parse("invalid", &preference) == -1);
+
+ const uint8_t rockchip[] = "rockchip,rk3566\0rockchip,rk3568";
+ const uint8_t allwinner[] = "allwinner,sun50i-h700\0allwinner,sunxi-unknown";
+ assert(go_video_decoder_platform(rockchip, sizeof(rockchip), 1) == GO_VIDEO_PLATFORM_ROCKCHIP);
+ assert(go_video_decoder_platform(allwinner, sizeof(allwinner), 1) ==
+ GO_VIDEO_PLATFORM_ALLWINNER);
+ assert(go_video_decoder_platform(NULL, 0, 1) == GO_VIDEO_PLATFORM_OTHER_ARM);
+ assert(go_video_decoder_platform(rockchip, sizeof(rockchip), 0) == GO_VIDEO_PLATFORM_NON_ARM);
+
+ GoVideoDecoderBackend candidates[3];
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_AUTO,
+ GO_VIDEO_PLATFORM_ROCKCHIP, candidates, 3) == 2);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_MPP);
+ assert(candidates[1] == GO_VIDEO_DECODER_BACKEND_SOFTWARE);
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_AUTO,
+ GO_VIDEO_PLATFORM_ALLWINNER, candidates, 3) == 2);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_CEDAR);
+ assert(candidates[1] == GO_VIDEO_DECODER_BACKEND_SOFTWARE);
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_AUTO,
+ GO_VIDEO_PLATFORM_OTHER_ARM, candidates, 3) == 3);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_MPP);
+ assert(candidates[1] == GO_VIDEO_DECODER_BACKEND_CEDAR);
+ assert(candidates[2] == GO_VIDEO_DECODER_BACKEND_SOFTWARE);
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_AUTO,
+ GO_VIDEO_PLATFORM_NON_ARM, candidates, 3) == 1);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_SOFTWARE);
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_MPP,
+ GO_VIDEO_PLATFORM_NON_ARM, candidates, 3) == 1);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_MPP);
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_CEDAR,
+ GO_VIDEO_PLATFORM_ROCKCHIP, candidates, 3) == 1);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_CEDAR);
+ assert(go_video_decoder_candidate_order(GO_VIDEO_DECODER_PREFERENCE_SOFTWARE,
+ GO_VIDEO_PLATFORM_ROCKCHIP, candidates, 3) == 1);
+ assert(candidates[0] == GO_VIDEO_DECODER_BACKEND_SOFTWARE);
+
+ FakeDecoder decoder = {
+ .submit_result = GO_VIDEO_DECODER_RESULT_OK,
+ .frames_remaining = 2,
+ };
+ go_video_decoder_initialize(&decoder.base, &fake_ops);
+
+ assert(strcmp(go_video_decoder_name(&decoder.base), "fake") == 0);
+ assert(go_video_decoder_backend(&decoder.base) == GO_VIDEO_DECODER_BACKEND_MPP);
+ const uint8_t access_unit[] = {0, 0, 0, 1, 0x65};
+ assert(go_video_decoder_submit_access_unit(&decoder.base, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_OK);
+
+ GoDecodedVideoFrame frame;
+ assert(go_video_decoder_receive_frame(&decoder.base, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ assert(frame.backend_frame == &decoder);
+ go_video_decoder_release_frame(&decoder.base, &frame);
+ assert(frame.backend_frame == NULL);
+ assert(go_video_decoder_receive_frame(&decoder.base, &frame) == GO_VIDEO_DECODER_RESULT_OK);
+ go_video_decoder_release_frame(&decoder.base, &frame);
+ assert(go_video_decoder_receive_frame(&decoder.base, &frame) == GO_VIDEO_DECODER_RESULT_AGAIN);
+ assert(decoder.submitted == 1);
+ assert(decoder.received == 2);
+ assert(decoder.released == 2);
+
+ decoder.submit_result = GO_VIDEO_DECODER_RESULT_AGAIN;
+ assert(go_video_decoder_submit_access_unit(&decoder.base, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_AGAIN);
+ decoder.submit_result = GO_VIDEO_DECODER_RESULT_FATAL;
+ assert(go_video_decoder_submit_access_unit(&decoder.base, access_unit, sizeof(access_unit)) ==
+ GO_VIDEO_DECODER_RESULT_FATAL);
+ assert(strcmp(go_video_decoder_last_error(&decoder.base), "fake failure") == 0);
+ assert(go_video_decoder_reset(&decoder.base) == 0);
+ go_video_decoder_destroy(&decoder.base);
+ assert(decoder.reset == 1);
+ assert(decoder.destroyed == 1);
+ return 0;
+}
diff --git a/tests/video_frame_copy_test.c b/tests/video_frame_copy_test.c
new file mode 100644
index 0000000..f5e686d
--- /dev/null
+++ b/tests/video_frame_copy_test.c
@@ -0,0 +1,92 @@
+#include "video_frame_copy.h"
+
+#include
+#include
+#include
+
+static void test_nv12(int width, int height, int source_padding, int destination_padding) {
+ int chroma_height = (height + 1) / 2;
+ int chroma_width = width + width % 2;
+ int source_stride = width + source_padding;
+ int destination_stride = width + destination_padding;
+ size_t source_size = (size_t)source_stride * (size_t)(height + chroma_height);
+ size_t destination_size = (size_t)destination_stride * (size_t)(height + chroma_height);
+ uint8_t* source = malloc(source_size);
+ uint8_t* destination = malloc(destination_size);
+ assert(source && destination);
+ memset(source, 0xa5, source_size);
+ memset(destination, 0, destination_size);
+
+ GoDecodedVideoFrame frame = {
+ .format = GO_VIDEO_PIXEL_FORMAT_NV12,
+ .width = width,
+ .height = height,
+ .planes = {source, source + (size_t)source_stride * (size_t)height, NULL},
+ .strides = {source_stride, source_stride, 0},
+ };
+ uint8_t* planes[3] = {
+ destination,
+ destination + (size_t)destination_stride * (size_t)height,
+ NULL,
+ };
+ int strides[3] = {destination_stride, destination_stride, 0};
+ assert(go_video_frame_copy(&frame, width, height, planes, strides) == 0);
+ for (int row = 0; row < height; ++row)
+ assert(memcmp(planes[0] + (size_t)row * (size_t)destination_stride,
+ frame.planes[0] + (size_t)row * (size_t)source_stride, (size_t)width) == 0);
+ for (int row = 0; row < chroma_height; ++row)
+ assert(memcmp(planes[1] + (size_t)row * (size_t)destination_stride,
+ frame.planes[1] + (size_t)row * (size_t)source_stride,
+ (size_t)chroma_width) == 0);
+ free(destination);
+ free(source);
+}
+
+static void test_yuv420p(void) {
+ uint8_t y[8 * 5];
+ uint8_t u[4 * 3];
+ uint8_t v[4 * 3];
+ uint8_t output_y[10 * 5];
+ uint8_t output_u[6 * 3];
+ uint8_t output_v[6 * 3];
+ memset(y, 1, sizeof(y));
+ memset(u, 2, sizeof(u));
+ memset(v, 3, sizeof(v));
+ GoDecodedVideoFrame frame = {
+ .format = GO_VIDEO_PIXEL_FORMAT_YUV420P,
+ .width = 8,
+ .height = 5,
+ .planes = {y, u, v},
+ .strides = {8, 4, 4},
+ };
+ uint8_t* planes[3] = {output_y, output_u, output_v};
+ int strides[3] = {10, 6, 6};
+ assert(go_video_frame_copy(&frame, 8, 5, planes, strides) == 0);
+ assert(output_y[0] == 1 && output_y[40] == 1);
+ assert(output_u[0] == 2 && output_u[12] == 2);
+ assert(output_v[0] == 3 && output_v[12] == 3);
+}
+
+int main(void) {
+ test_nv12(640, 360, 32, 16);
+ test_nv12(1280, 720, 64, 32);
+ test_nv12(1920, 1080, 128, 64);
+ test_nv12(8, 5, 4, 2);
+ test_nv12(7, 5, 3, 2);
+ test_yuv420p();
+
+ uint8_t plane[16] = {0};
+ GoDecodedVideoFrame invalid = {
+ .format = GO_VIDEO_PIXEL_FORMAT_NV12,
+ .width = 8,
+ .height = 4,
+ .planes = {plane, plane, NULL},
+ .strides = {7, 8, 0},
+ };
+ assert(go_video_frame_validate(&invalid, 8, 4) == -1);
+ invalid.strides[0] = 8;
+ assert(go_video_frame_validate(&invalid, 7, 4) == -1);
+ invalid.format = (GoVideoPixelFormat)99;
+ assert(go_video_frame_validate(&invalid, 8, 4) == -1);
+ return 0;
+}
diff --git a/tools/build-dependencies.sh b/tools/build-dependencies.sh
index 55aac74..4064eb0 100755
--- a/tools/build-dependencies.sh
+++ b/tools/build-dependencies.sh
@@ -1,6 +1,11 @@
#!/bin/sh
set -eu
+if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ "$1" != "--toolchain-only" ]; }; then
+ echo "usage: $0 [--toolchain-only]" >&2
+ exit 1
+fi
+
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
TOOLS="$ROOT/.tools"
DOWNLOADS="$TOOLS/downloads"
@@ -67,6 +72,10 @@ chmod +x "$AR_WRAPPER" "$RANLIB_WRAPPER"
printf '%s\n' 'set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)'
} >"$TOOLCHAIN"
+if [ "${1:-}" = "--toolchain-only" ]; then
+ exit 0
+fi
+
hash_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | cut -d' ' -f1
diff --git a/tools/build-mpp.sh b/tools/build-mpp.sh
new file mode 100755
index 0000000..b351ee3
--- /dev/null
+++ b/tools/build-mpp.sh
@@ -0,0 +1,82 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
+OUTPUT_DIR=${1:-$ROOT/zig-out/rockchip}
+MPP_SOURCE="$ROOT/vendor/mpp"
+EXPECTED_COMMIT=c08762ebfadeb4e986d2fed993bc7a54862d3ebe
+BUILD_DIR="$ROOT/.tools/build/mpp-aarch64-release"
+TOOLCHAIN="$ROOT/.tools/cross/aarch64-linux-gnu/toolchain.cmake"
+CROSS_CXX="$ROOT/.tools/cross/aarch64-linux-gnu/c++"
+ZIG="$ROOT/.tools/zig-0.14.1/zig"
+ZIG_RUN="$ROOT/tools/zig.sh"
+
+[ -x "$ZIG" ] || {
+ echo "missing Zig 0.14.1; run tools/bootstrap.sh" >&2
+ exit 1
+}
+[ -f "$MPP_SOURCE/CMakeLists.txt" ] || {
+ echo "missing Rockchip MPP submodule; run tools/bootstrap.sh" >&2
+ exit 1
+}
+
+actual_commit=$(git -C "$MPP_SOURCE" rev-parse HEAD)
+[ "$actual_commit" = "$EXPECTED_COMMIT" ] || {
+ echo "Rockchip MPP must be at $EXPECTED_COMMIT (found $actual_commit)" >&2
+ exit 1
+}
+
+mkdir -p "$OUTPUT_DIR" "$ROOT/.tools/cache/zig"
+export ZIG_GLOBAL_CACHE_DIR="$ROOT/.tools/cache/zig"
+
+if [ -n "${MPP_LINK_LIBRARY:-}" ]; then
+ [ -f "$MPP_LINK_LIBRARY" ] || {
+ echo "MPP_LINK_LIBRARY does not exist: $MPP_LINK_LIBRARY" >&2
+ exit 1
+ }
+ mpp_headers=${MPP_INCLUDE_DIR:-$MPP_SOURCE/inc}
+ [ -f "$mpp_headers/rk_mpi.h" ] || {
+ echo "MPP headers not found in $mpp_headers" >&2
+ exit 1
+ }
+ mpp_link=$MPP_LINK_LIBRARY
+ mpp_runtime=$MPP_LINK_LIBRARY
+else
+ "$ROOT/tools/build-dependencies.sh" --toolchain-only
+ # MPP invokes CMAKE_LINKER directly to merge target object files with `-r`.
+ cmake -S "$MPP_SOURCE" -B "$BUILD_DIR" \
+ -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \
+ -DCMAKE_LINKER="$CROSS_CXX" \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBUILD_TEST=OFF \
+ -DBUILD_SHARED_LIBS=ON
+ cmake --build "$BUILD_DIR" --target rockchip_mpp --parallel
+ mpp_headers="$MPP_SOURCE/inc"
+ mpp_link="$BUILD_DIR/mpp/librockchip_mpp.so"
+ mpp_runtime="$BUILD_DIR/mpp/librockchip_mpp.so.0"
+fi
+
+"$ZIG_RUN" objcopy --strip-all "$mpp_runtime" "$OUTPUT_DIR/librockchip_mpp.so.1"
+
+"$ZIG_RUN" cc -target aarch64-linux-gnu.2.38 -O2 -fPIC -fvisibility=hidden -shared -s \
+ -ffile-prefix-map="$ROOT"=. -Werror -Wall -Wextra \
+ -I"$ROOT/src/media/video" -I"$mpp_headers" \
+ "$ROOT/src/media/video/mpp_bridge.c" "$mpp_link" \
+ -Wl,-z,defs -Wl,-soname,libgreenovercast-mpp.so -Wl,-rpath,'$ORIGIN' \
+ -o "$OUTPUT_DIR/libgreenovercast-mpp.so"
+
+"$ZIG_RUN" cc -target aarch64-linux-gnu.2.38 -O2 -s -Werror -Wall -Wextra \
+ -I"$ROOT/src/media/video" \
+ "$ROOT/src/media/video/mpp_loader.c" "$ROOT/tests/device/mpp_probe.c" \
+ -ldl -Wl,-rpath,'$ORIGIN' -o "$OUTPUT_DIR/greenovercast-mpp-probe.aarch64"
+
+"$ZIG_RUN" cc -target aarch64-linux-gnu.2.38 -O2 -s -Werror -Wall -Wextra \
+ -I"$ROOT/src/media/video" \
+ "$ROOT/src/media/video/mpp_loader.c" "$ROOT/tests/device/mpp_bridge_test.c" \
+ -ldl -Wl,-rpath,'$ORIGIN' -o "$OUTPUT_DIR/greenovercast-mpp-bridge-test.aarch64"
+
+file "$OUTPUT_DIR/libgreenovercast-mpp.so"
+file "$OUTPUT_DIR/librockchip_mpp.so.1"
+printf 'built: %s\n' "$OUTPUT_DIR/libgreenovercast-mpp.so"
+printf 'built: %s\n' "$OUTPUT_DIR/greenovercast-mpp-probe.aarch64"
+printf 'built: %s\n' "$OUTPUT_DIR/greenovercast-mpp-bridge-test.aarch64"
diff --git a/tools/build-release.sh b/tools/build-release.sh
index adad242..d9ee7f2 100755
--- a/tools/build-release.sh
+++ b/tools/build-release.sh
@@ -47,6 +47,7 @@ trap restore_libdatachannel EXIT HUP INT TERM
mkdir -p "$BUILD_DIR" "$OBJECT_DIR" "$ZIG_CACHE_DIR" "$(dirname -- "$OUTPUT")"
"$ROOT/tools/build-cedarx.sh" "$(dirname -- "$OUTPUT")/libgreenovercast-cedar.so"
+"$ROOT/tools/build-mpp.sh"
export ZIG_GLOBAL_CACHE_DIR="$ZIG_CACHE_DIR"
cmake_fresh=
@@ -92,9 +93,23 @@ compile_adapter() {
-c "$source" -o "$object"
}
+compile_zig_video_core() {
+ source=$1
+ object=$2
+ "$ZIG" build-obj "$source" -target aarch64-linux-gnu.2.38 -O ReleaseSafe -lc \
+ -I"$ROOT/src/media/video" -femit-bin="$object"
+}
+
compile_adapter "$ROOT/src/media/audio/opus_adapter.c" "$OBJECT_DIR/opus_adapter.o"
compile_adapter "$ROOT/src/media/audio/audio_pipeline.c" "$OBJECT_DIR/audio_pipeline.o"
compile_adapter "$ROOT/src/media/video/cedar_loader.c" "$OBJECT_DIR/cedar_loader.o"
+compile_zig_video_core "$ROOT/src/media/video/video_decoder.zig" "$OBJECT_DIR/video_decoder.o"
+compile_zig_video_core "$ROOT/src/media/video/video_decoder_selection.zig" "$OBJECT_DIR/video_decoder_selection.o"
+compile_adapter "$ROOT/src/media/video/video_decoder_ffmpeg.c" "$OBJECT_DIR/video_decoder_ffmpeg.o"
+compile_adapter "$ROOT/src/media/video/video_decoder_cedar.c" "$OBJECT_DIR/video_decoder_cedar.o"
+compile_adapter "$ROOT/src/media/video/mpp_loader.c" "$OBJECT_DIR/mpp_loader.o"
+compile_adapter "$ROOT/src/media/video/video_decoder_mpp.c" "$OBJECT_DIR/video_decoder_mpp.o"
+compile_zig_video_core "$ROOT/src/media/video/video_frame_copy.zig" "$OBJECT_DIR/video_frame_copy.o"
compile_adapter "$ROOT/src/media/video/video_pipeline.c" "$OBJECT_DIR/video_pipeline.o"
compile_adapter "$ROOT/src/auth/token_store_adapter.c" "$OBJECT_DIR/token_store_adapter.o"
compile_adapter "$ROOT/src/session/webrtc_session.c" "$OBJECT_DIR/webrtc_session.o"
@@ -142,7 +157,12 @@ compile_adapter "$ROOT/src/platform/sdl_platform.c" "$OBJECT_DIR/sdl_platform.o"
"$ZIG" build-exe "$ROOT/src/main.zig" \
"$OBJECT_DIR/opus_adapter.o" "$OBJECT_DIR/audio_pipeline.o" \
- "$OBJECT_DIR/cedar_loader.o" "$OBJECT_DIR/video_pipeline.o" \
+ "$OBJECT_DIR/cedar_loader.o" "$OBJECT_DIR/video_decoder.o" \
+ "$OBJECT_DIR/video_decoder_selection.o" \
+ "$OBJECT_DIR/video_decoder_ffmpeg.o" "$OBJECT_DIR/video_decoder_cedar.o" \
+ "$OBJECT_DIR/mpp_loader.o" "$OBJECT_DIR/video_decoder_mpp.o" \
+ "$OBJECT_DIR/video_frame_copy.o" \
+ "$OBJECT_DIR/video_pipeline.o" \
"$OBJECT_DIR/token_store_adapter.o" "$OBJECT_DIR/xbox_auth.o" \
"$OBJECT_DIR/controller.o" \
"$OBJECT_DIR/cloud_session.o" "$OBJECT_DIR/webrtc_session.o" \
diff --git a/tools/package-portmaster.sh b/tools/package-portmaster.sh
index 064a2f6..c5a9916 100755
--- a/tools/package-portmaster.sh
+++ b/tools/package-portmaster.sh
@@ -8,6 +8,9 @@ PORTMASTER_NEW=${PORTMASTER_NEW:-${2:-}}
SOURCE="$ROOT/packaging/portmaster/greenovercast"
BINARY="$ROOT/zig-out/bin/webrtc_stream"
CEDAR="$ROOT/zig-out/bin/libgreenovercast-cedar.so"
+MPP_PLUGIN="$ROOT/zig-out/rockchip/libgreenovercast-mpp.so"
+MPP_RUNTIME="$ROOT/zig-out/rockchip/librockchip_mpp.so.1"
+MPP_PROBE="$ROOT/zig-out/rockchip/greenovercast-mpp-probe.aarch64"
AVCODEC="$ROOT/zig-out/bin/libavcodec.so.58"
AVUTIL="$ROOT/zig-out/bin/libavutil.so.56"
SWSCALE="$ROOT/zig-out/bin/libswscale.so.5"
@@ -25,6 +28,12 @@ esac
echo "missing Cedar library: $CEDAR" >&2
exit 1
}
+for mpp_file in "$MPP_PLUGIN" "$MPP_RUNTIME" "$MPP_PROBE"; do
+ [ -f "$mpp_file" ] || {
+ echo "missing Rockchip MPP file: $mpp_file" >&2
+ exit 1
+ }
+done
for private_library in "$AVCODEC" "$AVUTIL" "$SWSCALE"; do
[ -f "$private_library" ] || {
echo "missing private library: $private_library" >&2
@@ -54,11 +63,22 @@ cp "$PORTMASTER_NEW/SOURCE_SETUP.txt" "$checker/SOURCE_SETUP.txt"
cp -R "$SOURCE" "$port"
cp "$BINARY" "$port/greenovercast/webrtc_stream.aarch64"
cp "$CEDAR" "$port/greenovercast/libgreenovercast-cedar.so"
+mkdir -p "$port/greenovercast/rockchip"
+cp "$MPP_PLUGIN" "$port/greenovercast/libgreenovercast-mpp.so"
+cp "$MPP_RUNTIME" "$port/greenovercast/rockchip/librockchip_mpp.so.1"
+cp "$MPP_PROBE" "$port/greenovercast/rockchip/greenovercast-mpp-probe.aarch64"
+cp "$ROOT/vendor/mpp/LICENSES/Apache-2.0" \
+ "$port/greenovercast/licenses/LICENSE.Rockchip-MPP-Apache-2.0.txt"
+cp "$ROOT/vendor/mpp/LICENSES/MIT" \
+ "$port/greenovercast/licenses/LICENSE.Rockchip-MPP-MIT.txt"
cp "$AVCODEC" "$port/greenovercast/libavcodec.so.58"
cp "$AVUTIL" "$port/greenovercast/libavutil.so.56"
cp "$SWSCALE" "$port/greenovercast/libswscale.so.5"
chmod 644 "$port/GreenOvercast.sh" "$port/greenovercast/webrtc_stream.aarch64" \
"$port/greenovercast/libgreenovercast-cedar.so" \
+ "$port/greenovercast/libgreenovercast-mpp.so" \
+ "$port/greenovercast/rockchip/librockchip_mpp.so.1" \
+ "$port/greenovercast/rockchip/greenovercast-mpp-probe.aarch64" \
"$port/greenovercast/libavcodec.so.58" \
"$port/greenovercast/libavutil.so.56" \
"$port/greenovercast/libswscale.so.5"
@@ -92,6 +112,11 @@ required = {
"greenovercast/screenshot.png",
"greenovercast/webrtc_stream.aarch64",
"greenovercast/libgreenovercast-cedar.so",
+ "greenovercast/libgreenovercast-mpp.so",
+ "greenovercast/rockchip/librockchip_mpp.so.1",
+ "greenovercast/rockchip/greenovercast-mpp-probe.aarch64",
+ "greenovercast/licenses/LICENSE.Rockchip-MPP-Apache-2.0.txt",
+ "greenovercast/licenses/LICENSE.Rockchip-MPP-MIT.txt",
"greenovercast/libavcodec.so.58",
"greenovercast/libavutil.so.56",
"greenovercast/libswscale.so.5",
diff --git a/tools/video-diagnostics.sh b/tools/video-diagnostics.sh
new file mode 100755
index 0000000..b90d20b
--- /dev/null
+++ b/tools/video-diagnostics.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+set -u
+
+app_dir=${1:-.}
+mpp_plugin=${GREENOVERCAST_MPP_LIBRARY:-$app_dir/libgreenovercast-mpp.so}
+if [ -f "$app_dir/rockchip/greenovercast-mpp-probe.aarch64" ]; then
+ mpp_probe="$app_dir/rockchip/greenovercast-mpp-probe.aarch64"
+ private_mpp_dir="$app_dir/rockchip"
+else
+ mpp_probe="$app_dir/greenovercast-mpp-probe.aarch64"
+ private_mpp_dir="$app_dir"
+fi
+
+printf 'Architecture: %s\n' "$(uname -m)"
+printf 'Userspace bits: %s\n' "$(getconf LONG_BIT 2>/dev/null || printf unknown)"
+printf 'Kernel: %s\n' "$(uname -r)"
+printf 'Libc: %s\n' "$(getconf GNU_LIBC_VERSION 2>/dev/null || printf unknown)"
+printf 'Decoder preference: %s\n' "${GREENOVERCAST_VIDEO_DECODER:-auto}"
+
+printf 'Device-tree compatible:\n'
+compatible_found=0
+for compatible_path in /proc/device-tree/compatible /sys/firmware/devicetree/base/compatible; do
+ if [ -r "$compatible_path" ]; then
+ tr '\0' '\n' <"$compatible_path"
+ compatible_found=1
+ break
+ fi
+done
+[ "$compatible_found" -eq 1 ] || printf 'unavailable\n'
+
+printf 'MPP plugin: %s\n' "$mpp_plugin"
+if [ ! -f "$mpp_plugin" ]; then
+ printf 'MPP plugin status: missing\n'
+ exit 1
+fi
+file "$mpp_plugin" 2>/dev/null || true
+if command -v ldd >/dev/null 2>&1; then
+ printf 'MPP dependencies using firmware libraries:\n'
+ LD_LIBRARY_PATH="$app_dir:${LD_LIBRARY_PATH:-}" ldd "$mpp_plugin" 2>&1
+fi
+if [ -f "$mpp_probe" ] && [ ! -x "$mpp_probe" ]; then
+ if ! chmod +x "$mpp_probe" 2>/dev/null; then
+ printf 'MPP probe: unable to make executable\n'
+ exit 1
+ fi
+fi
+if [ -x "$mpp_probe" ]; then
+ printf 'MPP firmware probe:\n'
+ if GREENOVERCAST_MPP_LIBRARY="$mpp_plugin" \
+ LD_LIBRARY_PATH="$app_dir:${LD_LIBRARY_PATH:-}" \
+ "$mpp_probe"; then
+ exit 0
+ fi
+ if [ -f "$private_mpp_dir/librockchip_mpp.so.1" ]; then
+ printf 'MPP private-runtime probe:\n'
+ GREENOVERCAST_MPP_LIBRARY="$mpp_plugin" \
+ LD_LIBRARY_PATH="$private_mpp_dir:$app_dir:${LD_LIBRARY_PATH:-}" \
+ "$mpp_probe"
+ exit $?
+ fi
+else
+ printf 'MPP probe: missing\n'
+ exit 1
+fi
diff --git a/vendor/manifest.lock b/vendor/manifest.lock
index fe6f743..1dcccd1 100644
--- a/vendor/manifest.lock
+++ b/vendor/manifest.lock
@@ -20,6 +20,15 @@ linkage = "dynamic (private .so in app lib dir; replaceable per LGPLv2.1 ยง6)"
build = "source-built H.264 decoder plus project-owned H616 /dev/cedar_dev and ION adapter; tools/build-cedarx.sh"
status = "1280x720 H.264 hardware decode live-cloud proven through Ports on muOS and Knulli"
+[dep.rockchip_mpp]
+version = "1.1.0"
+source = "https://github.com/rockchip-linux/mpp"
+commit = "c08762ebfadeb4e986d2fed993bc7a54862d3ebe"
+license = "Apache-2.0 and MIT"
+linkage = "dynamic optional decoder plugin; firmware MPP preferred when compatible"
+build = "git submodule headers and optional private userspace library; tools/build-mpp.sh"
+status = "live cloud playback proven on ROCKNIX; RK3326 dArkOS 4.4 offline 720p decode and frontend startup proven"
+
[dep.libdatachannel]
version = "v0.24.3"
commit = "c6696d157b5612df2a741d9a03b192b47ab6cefb"
diff --git a/vendor/mpp b/vendor/mpp
new file mode 160000
index 0000000..c08762e
--- /dev/null
+++ b/vendor/mpp
@@ -0,0 +1 @@
+Subproject commit c08762ebfadeb4e986d2fed993bc7a54862d3ebe