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 @@ screenshot 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