diff --git a/README.md b/README.md
index fe7ad6c..5d1504c 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,8 @@
GreenOvercast is a native Xbox Cloud Gaming client for small ARM64 Linux
-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).
+handhelds. It requests a stream matching the device display and uses hardware
+video decoding on supported H700 and Rockchip firmware.
This independent project is not affiliated with or endorsed by Microsoft.
@@ -23,41 +21,43 @@ GreenOvercast from Ports.
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.
+the code to sign in.
GreenOvercast is experimental.
## Controls
-| Button | Action |
-| ------------------ | ------------------------------------------- |
-| D-pad / Left Stick | Navigate |
-| A | Select |
-| B | Back |
-| X | Open search / delete a letter |
-| Y | Change aspect ratio / clear search |
-| L1 / R1 | Xbox LB / RB |
-| L2 / R2 | Xbox LT / RT |
-| Start | Apply search / Xbox Menu |
-| Select | Xbox View |
-| L3 + R3 | Xbox Guide |
-| Select + Start | Exit GreenOvercast after holding one second |
-
-In-game controls use the standard Xbox controller layout.
-
-The aspect-ratio setting applies to the next game you start. Most games only
-support 16:9.
+| Button | Library / menus | In game |
+| ------------------ | --------------------------------- | --------------------------------- |
+| D-pad / Left Stick | Navigate; hold to scroll faster | Movement |
+| A | Select, play, or type | Xbox A |
+| B | Back or cancel | Xbox B |
+| X | Search or delete a letter | Xbox X |
+| Y | Favorite a game or clear search | Xbox Y |
+| L1 / R1 | Switch All / Favorites | Xbox LB / RB |
+| L2 / R2 | Jump by first letter | Xbox LT / RT |
+| Start | Settings or apply search | Xbox Menu |
+| Select | — | Xbox View |
+| L3 + R3 | — | Xbox Guide |
+| Select + Start | Exit after holding for one second | Exit after holding for one second |
+
+Settings include Xbox/Nintendo face-button layouts, game artwork, and Sign
+out. Games that return a 16:9 stream remain letterboxed on 4:3 displays.
## Supported devices
-| Device | OS | Status |
-| -------- | -------------------- | ------ |
-| RG35XX-H | muOS 2508.4 | Tested |
-| RG40XX-H | Knulli (Batocera 42) | Tested |
+| Device | OS | Status |
+| ---------- | ------------------------------- | ------ |
+| RG35XX-H | muOS 2508.4 | Tested |
+| RG40XX-H | Knulli (Batocera 42) | Tested |
+| RG40XX-H | ROCKNIX 20260801 | Tested |
+| Miyoo Flip | SpruceOS 4.2.0 | Tested |
+| R36S | AmberELEC prerelease-20250515 | Tested |
-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.
+Hardware decoding is verified on the H700 systems above. muOS and Knulli use
+CedarX; ROCKNIX 20260801 uses the bundled Cedrus modules. Tested Rockchip builds
+use Rockchip MPP on RK3566 with SpruceOS and RK3326 with AmberELEC. 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.
@@ -68,26 +68,23 @@ You need a Linux or macOS host with `cmake`, `curl`, `git`, `make`, `patch`,
`perl`, and `python3`.
```sh
-tools/bootstrap.sh # fetch Zig 0.14.1
-tools/zig.sh build smoke # cross-build the aarch64 smoke binary
-tools/zig.sh build product-check # compile the Zig product modules
-tools/zig.sh build test # host unit tests
-tools/zig.sh build fmt-check # format check
+tools/bootstrap.sh
+tools/zig.sh build
+tools/zig.sh build test
+tools/zig.sh build fmt-check
```
-Build the release and PortMaster package with:
+Build the PortMaster package with:
```sh
-tools/build-release.sh
-PORTMASTER_NEW=/path/to/PortMaster-New tools/package-portmaster.sh
+PORTMASTER_NEW=/path/to/PortMaster-New tools/zig.sh build package
```
-I would recommend using a sparse checkout of [PortMaster-New](https://github.com/PortsMaster/PortMaster-New) because
-she's a big one. [Here is a great guide made by JeodC to help you with that](https://gist.github.com/JeodC/7a51211ad94ad6084d14042d80a62549).
+PortMaster-New is large. [JeodC's sparse-checkout guide](https://gist.github.com/JeodC/7a51211ad94ad6084d14042d80a62549)
+shows how to fetch only the files needed for packaging.
-`package-portmaster.sh` runs the current PortMaster-New checks and archive
-builder. To queue the finished archive for PortMaster's supported autoinstall
-flow:
+The package build runs the current PortMaster checks and archive builder. To
+queue the finished archive for PortMaster's supported autoinstall flow:
```sh
tools/deploy.sh zig-out/greenovercast.zip
diff --git a/THIRDPARTY.md b/THIRDPARTY.md
index c7f2112..e4ba393 100644
--- a/THIRDPARTY.md
+++ b/THIRDPARTY.md
@@ -1,6 +1,6 @@
# Third-party licenses
-GreenOvercast is MPL-2.0. The libraries below are linked or bundled into the
+GreenOvercast is MPL-2.0. The components below are linked or bundled into the
release. Full license texts for the bundled libraries ship alongside the port in
`packaging/portmaster/greenovercast/greenovercast/licenses/`.
@@ -13,8 +13,10 @@ 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) |
+| [Linux Cedrus](https://www.kernel.org/) (7.0.11) | GPL-2.0-only | ROCKNIX H700 V4L2 Request decoder modules |
| [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) |
+| [FFmpeg](https://ffmpeg.org/) (libavcodec, libavutil, libswscale) | LGPLv2.1-or-later | H.264 decode, JPEG artwork, and color conversion |
+| [libudev-zero](https://github.com/illiliti/libudev-zero) | ISC | V4L2 media-device discovery (static) |
| [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) |
| [libcurl](https://curl.se/) | curl (MIT-style) | HTTP for Xbox services (static) |
diff --git a/build.zig b/build.zig
index a4ac410..cd3db8c 100644
--- a/build.zig
+++ b/build.zig
@@ -10,6 +10,348 @@ const aarch64_linux_query: std.Target.Query = .{
const c_test_flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror" };
+const project_include_paths = [_][]const u8{
+ ".tools/deps/aarch64-linux-gnu/include",
+ "vendor/libdatachannel/include",
+ "src/media/audio",
+ "src/media/video",
+ "src/media/rtp",
+ "src/auth",
+ "src/catalog",
+ "src/input",
+ "src/net",
+ "src/platform",
+ "src/session",
+ "src/ui",
+};
+
+const ZigImport = struct { name: []const u8, path: []const u8 };
+
+const release_zig_roots = [_]struct {
+ name: []const u8,
+ path: []const u8,
+ imports: []const ZigImport = &.{},
+}{
+ .{
+ .name = "greenovercast-auth",
+ .path = "src/auth/xbox_auth.zig",
+ .imports = &.{
+ .{ .name = "form_writer", .path = "src/net/form_writer.zig" },
+ .{ .name = "json_reader", .path = "src/net/json_reader.zig" },
+ },
+ },
+ .{
+ .name = "greenovercast-cloud",
+ .path = "src/session/cloud_session.zig",
+ .imports = &.{.{ .name = "json_reader", .path = "src/net/json_reader.zig" }},
+ },
+ .{
+ .name = "greenovercast-webrtc",
+ .path = "src/session/webrtc_session.zig",
+ .imports = &.{
+ .{ .name = "json_reader", .path = "src/net/json_reader.zig" },
+ .{ .name = "json_writer", .path = "src/net/json_writer.zig" },
+ },
+ },
+ .{ .name = "greenovercast-controller", .path = "src/input/controller.zig" },
+ .{
+ .name = "greenovercast-ui",
+ .path = "src/ui/handheld_ui.zig",
+ .imports = &.{.{ .name = "catalog_search", .path = "src/catalog/catalog_search.zig" }},
+ },
+ .{ .name = "greenovercast-sdl-platform", .path = "src/platform/sdl_platform.zig" },
+ .{
+ .name = "greenovercast-audio-pipeline",
+ .path = "src/media/audio/audio_pipeline.zig",
+ .imports = &.{.{ .name = "rtp_packet", .path = "src/media/rtp/packet.zig" }},
+ },
+ .{ .name = "greenovercast-mpp-loader", .path = "src/media/video/mpp_loader.zig" },
+ .{ .name = "greenovercast-video-decoder-mpp", .path = "src/media/video/video_decoder_mpp.zig" },
+ .{ .name = "greenovercast-cedar-loader", .path = "src/media/video/cedar_loader.zig" },
+ .{ .name = "greenovercast-video-decoder-cedar", .path = "src/media/video/video_decoder_cedar.zig" },
+ .{ .name = "greenovercast-video-pipeline", .path = "src/media/video/video_pipeline.zig" },
+ .{ .name = "greenovercast-h264-depacketizer", .path = "src/media/rtp/h264_depacketizer.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 release_c_sources = [_][]const u8{
+ "src/media/video/video_decoder_ffmpeg.c",
+ "src/media/video/video_decoder_v4l2_request.c",
+ "src/auth/token_store_adapter.c",
+ "src/net/http_client.c",
+ "src/ui/artwork_decoder.c",
+};
+
+const cedar_sources = [_][]const u8{
+ "vendor/cedarx/base/AwPool.c",
+ "vendor/cedarx/base/CdxList.c",
+ "vendor/cedarx/base/CdxQueue.c",
+ "vendor/cedarx/base/CdxUtils.c",
+ "vendor/cedarx/vdecoder/adapter.c",
+ "vendor/cedarx/vdecoder/fbm.c",
+ "vendor/cedarx/vdecoder/sbm.c",
+ "vendor/cedarx/vdecoder/vdecoder.c",
+ "vendor/cedarx/vdecoder/videoengine.c",
+ "vendor/cedarx/plugin/vdecoder/h264/h264.c",
+ "vendor/cedarx/plugin/vdecoder/h264/h264_dec.c",
+ "vendor/cedarx/plugin/vdecoder/h264/h264_hal.c",
+ "vendor/cedarx/plugin/vdecoder/h264/h264_mmco.c",
+ "vendor/cedarx/plugin/vdecoder/h264/h264_nalu.c",
+ "src/media/video/cedar_h616_runtime.c",
+ "src/media/video/cedar_bridge.c",
+};
+
+fn addProjectIncludes(b: *std.Build, module: *std.Build.Module) void {
+ for (project_include_paths) |path| module.addIncludePath(b.path(path));
+}
+
+fn addReleaseZigObject(
+ b: *std.Build,
+ target: std.Build.ResolvedTarget,
+ optimize: std.builtin.OptimizeMode,
+ name: []const u8,
+ source: []const u8,
+ imports: []const ZigImport,
+) *std.Build.Step.Compile {
+ const module = b.createModule(.{
+ .root_source_file = b.path(source),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ });
+ addProjectIncludes(b, module);
+ for (imports) |item|
+ addZigImport(b, module, target, optimize, item.name, item.path);
+ return b.addObject(.{ .name = name, .root_module = module });
+}
+
+fn addZigImport(
+ b: *std.Build,
+ module: *std.Build.Module,
+ target: std.Build.ResolvedTarget,
+ optimize: std.builtin.OptimizeMode,
+ name: []const u8,
+ source: []const u8,
+) void {
+ module.addImport(name, b.createModule(.{
+ .root_source_file = b.path(source),
+ .target = target,
+ .optimize = optimize,
+ }));
+}
+
+fn addStaticArchive(module: *std.Build.Module, b: *std.Build, path: []const u8) void {
+ module.addObjectFile(b.path(path));
+}
+
+fn addReleaseArtifacts(
+ b: *std.Build,
+ target: std.Build.ResolvedTarget,
+ optimize: std.builtin.OptimizeMode,
+) *std.Build.Step {
+ const native_dependencies = b.addSystemCommand(&.{
+ "sh",
+ b.pathFromRoot("tools/build-dependencies.sh"),
+ });
+ native_dependencies.setCwd(b.path("."));
+
+ const main_module = b.createModule(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .link_libcpp = true,
+ .strip = true,
+ });
+ addProjectIncludes(b, main_module);
+ main_module.addLibraryPath(b.path(".tools/deps/aarch64-linux-gnu/lib"));
+ main_module.addRPathSpecial("$ORIGIN");
+ main_module.addCSourceFiles(.{
+ .files = &release_c_sources,
+ .flags = &.{ "-std=gnu11", "-D_GNU_SOURCE", "-Wall", "-Wextra", "-Werror" },
+ });
+
+ for (release_zig_roots) |root| {
+ const object = addReleaseZigObject(b, target, optimize, root.name, root.path, root.imports);
+ object.step.dependOn(&native_dependencies.step);
+ main_module.addObject(object);
+ }
+
+ const libdatachannel = ".tools/build/libdatachannel-aarch64-release";
+ addStaticArchive(main_module, b, libdatachannel ++ "/libdatachannel.a");
+ addStaticArchive(main_module, b, libdatachannel ++ "/deps/libjuice/libjuice.a");
+ addStaticArchive(main_module, b, libdatachannel ++ "/deps/usrsctp/usrsctplib/libusrsctp.a");
+ addStaticArchive(main_module, b, libdatachannel ++ "/deps/libsrtp/libsrtp2.a");
+ addStaticArchive(main_module, b, ".tools/deps/aarch64-linux-gnu/lib/libcurl.a");
+ addStaticArchive(main_module, b, ".tools/deps/aarch64-linux-gnu/lib/libssl.a");
+ addStaticArchive(main_module, b, ".tools/deps/aarch64-linux-gnu/lib/libcrypto.a");
+ addStaticArchive(main_module, b, ".tools/deps/aarch64-linux-gnu/lib/libopus.a");
+ for ([_][]const u8{ "pthread", "dl", "SDL2", "avcodec", "avutil", "swscale" }) |library| {
+ main_module.linkSystemLibrary(library, .{ .use_pkg_config = .no });
+ }
+
+ const executable = b.addExecutable(.{
+ .name = "webrtc_stream",
+ .root_module = main_module,
+ });
+ executable.step.dependOn(&native_dependencies.step);
+ const install_executable = b.addInstallArtifact(executable, .{});
+
+ const cedar_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .pic = true,
+ .strip = true,
+ });
+ for ([_][]const u8{
+ "vendor/cedarx/base/include",
+ "vendor/cedarx/common/include",
+ "vendor/cedarx/vdecoder/include",
+ "vendor/cedarx/plugin/vdecoder/h264",
+ "src/media/video",
+ }) |path| cedar_module.addIncludePath(b.path(path));
+ cedar_module.addCSourceFiles(.{
+ .files = &cedar_sources,
+ .flags = &.{
+ "-std=gnu11",
+ "-D_GNU_SOURCE",
+ "-includestdint.h",
+ "-fvisibility=hidden",
+ "-Wno-int-to-pointer-cast",
+ "-Wno-pointer-to-int-cast",
+ "-Wno-format",
+ "-Wno-unused-variable",
+ "-Wno-unused-parameter",
+ },
+ });
+ cedar_module.linkSystemLibrary("pthread", .{ .use_pkg_config = .no });
+ cedar_module.linkSystemLibrary("dl", .{ .use_pkg_config = .no });
+ const cedar = b.addSharedLibrary(.{
+ .name = "greenovercast-cedar",
+ .root_module = cedar_module,
+ });
+ const install_cedar = b.addInstallArtifact(cedar, .{});
+
+ const cedar_test_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .strip = true,
+ });
+ cedar_test_module.addIncludePath(b.path("src/media/video"));
+ cedar_test_module.addCSourceFile(.{
+ .file = b.path("tests/device/cedar_bridge_test.c"),
+ .flags = c_test_flags,
+ });
+ cedar_test_module.linkLibrary(cedar);
+ cedar_test_module.addRPathSpecial("$ORIGIN");
+ const cedar_test = b.addExecutable(.{
+ .name = "cedar_bridge_test",
+ .root_module = cedar_test_module,
+ });
+ const install_cedar_test = b.addInstallArtifact(cedar_test, .{});
+
+ const mpp_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .pic = true,
+ .strip = true,
+ });
+ mpp_module.addIncludePath(b.path("src/media/video"));
+ mpp_module.addIncludePath(b.path("vendor/mpp/inc"));
+ mpp_module.addLibraryPath(b.path(".tools/build/mpp-aarch64-release/mpp"));
+ mpp_module.addRPathSpecial("$ORIGIN");
+ mpp_module.addCSourceFile(.{
+ .file = b.path("src/media/video/mpp_bridge.c"),
+ .flags = c_test_flags,
+ });
+ mpp_module.linkSystemLibrary("rockchip_mpp", .{
+ .use_pkg_config = .no,
+ .preferred_link_mode = .dynamic,
+ .search_strategy = .paths_first,
+ });
+ const mpp = b.addSharedLibrary(.{
+ .name = "greenovercast-mpp",
+ .root_module = mpp_module,
+ });
+ mpp.step.dependOn(&native_dependencies.step);
+ const install_mpp = b.addInstallFile(
+ mpp.getEmittedBin(),
+ "rockchip/libgreenovercast-mpp.so",
+ );
+
+ const mpp_runtime_copy = b.addObjCopy(
+ b.path(".tools/build/mpp-aarch64-release/mpp/librockchip_mpp.so.0"),
+ .{ .basename = "librockchip_mpp.so.1", .strip = .debug_and_symbols },
+ );
+ mpp_runtime_copy.step.dependOn(&native_dependencies.step);
+ const install_mpp_runtime = b.addInstallFile(
+ mpp_runtime_copy.getOutput(),
+ "rockchip/librockchip_mpp.so.1",
+ );
+
+ const mpp_device_sources = [_]struct { name: []const u8, source: []const u8 }{
+ .{ .name = "greenovercast-mpp-probe.aarch64", .source = "tests/device/mpp_probe.c" },
+ .{ .name = "greenovercast-mpp-bridge-test.aarch64", .source = "tests/device/mpp_bridge_test.c" },
+ };
+ const mpp_loader_device_object = addReleaseZigObject(
+ b,
+ target,
+ optimize,
+ "greenovercast-mpp-loader-device",
+ "src/media/video/mpp_loader.zig",
+ &.{},
+ );
+ var mpp_device_installs: [mpp_device_sources.len]*std.Build.Step.InstallFile = undefined;
+ for (mpp_device_sources, 0..) |item, index| {
+ const module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .strip = true,
+ });
+ module.addIncludePath(b.path("src/media/video"));
+ module.addRPathSpecial("$ORIGIN");
+ module.addCSourceFile(.{ .file = b.path(item.source), .flags = c_test_flags });
+ module.addObject(mpp_loader_device_object);
+ module.linkSystemLibrary("dl", .{ .use_pkg_config = .no });
+ const tool = b.addExecutable(.{ .name = item.name, .root_module = module });
+ mpp_device_installs[index] = b.addInstallFile(
+ tool.getEmittedBin(),
+ b.fmt("rockchip/{s}", .{item.name}),
+ );
+ }
+
+ const private_libraries = [_]struct { source: []const u8, name: []const u8 }{
+ .{ .source = ".tools/deps/aarch64-linux-gnu/lib/libavcodec.so.63", .name = "libavcodec.so.63" },
+ .{ .source = ".tools/deps/aarch64-linux-gnu/lib/libavutil.so.61", .name = "libavutil.so.61" },
+ .{ .source = ".tools/deps/aarch64-linux-gnu/lib/libswscale.so.10", .name = "libswscale.so.10" },
+ };
+ var private_library_installs: [private_libraries.len]*std.Build.Step.InstallFile = undefined;
+ for (private_libraries, 0..) |library, index| {
+ const copy = b.addObjCopy(b.path(library.source), .{
+ .basename = library.name,
+ .strip = .debug_and_symbols,
+ });
+ copy.step.dependOn(&native_dependencies.step);
+ private_library_installs[index] = b.addInstallBinFile(copy.getOutput(), library.name);
+ }
+
+ const release = b.step("release", "Build the aarch64 application and decoder plugins");
+ release.dependOn(&install_executable.step);
+ release.dependOn(&install_cedar.step);
+ release.dependOn(&install_cedar_test.step);
+ release.dependOn(&install_mpp.step);
+ release.dependOn(&install_mpp_runtime.step);
+ for (mpp_device_installs) |install| release.dependOn(&install.step);
+ for (private_library_installs) |install| release.dependOn(&install.step);
+ return release;
+}
+
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),
@@ -62,6 +404,20 @@ fn addHostCFakeLibrary(
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe });
const aarch64_linux = b.resolveTargetQuery(aarch64_linux_query);
+ const release = addReleaseArtifacts(b, aarch64_linux, .ReleaseSafe);
+ b.default_step.dependOn(release);
+
+ const product_check = b.step("product-check", "Build the complete aarch64 product");
+ product_check.dependOn(release);
+
+ const package_command = b.addSystemCommand(&.{
+ "sh",
+ b.pathFromRoot("tools/package-portmaster.sh"),
+ });
+ package_command.setCwd(b.path("."));
+ package_command.step.dependOn(release);
+ const package = b.step("package", "Build the PortMaster archive");
+ package.dependOn(&package_command.step);
const stat_compat = b.createModule(.{
.root_source_file = b.path("src/platform/linux/stat_compat.zig"),
@@ -86,43 +442,6 @@ pub fn build(b: *std.Build) void {
const smoke_install = b.addInstallArtifact(smoke, .{});
const smoke_step = b.step("smoke", "Cross-build the aarch64 ABI smoke binary");
smoke_step.dependOn(&smoke_install.step);
- b.default_step.dependOn(&smoke_install.step);
-
- const product_check = b.step("product-check", "Compile the aarch64 Zig product modules");
- const product_roots = [_]struct { name: []const u8, path: []const u8 }{
- .{ .name = "greenovercast-main", .path = "src/main.zig" },
- .{ .name = "greenovercast-auth", .path = "src/auth/xbox_auth.zig" },
- .{ .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",
- "src/media/audio",
- "src/media/video",
- "src/media/rtp",
- "src/auth",
- "src/catalog",
- "src/input",
- "src/net",
- "src/platform",
- "src/session",
- "src/ui",
- };
- for (product_roots) |root| {
- const module = b.createModule(.{
- .root_source_file = b.path(root.path),
- .target = aarch64_linux,
- .optimize = .ReleaseSafe,
- .link_libc = true,
- });
- for (product_include_paths) |include_path| module.addIncludePath(b.path(include_path));
- const object = b.addObject(.{ .name = root.name, .root_module = module });
- product_check.dependOn(&object.step);
- }
const fmt_check = b.step("fmt-check", "Check zig fmt on project Zig sources");
fmt_check.dependOn(&b.addFmt(.{
@@ -131,11 +450,24 @@ pub fn build(b: *std.Build) void {
}).step);
const test_step = b.step("test", "Run host unit tests");
+ const rocknix_build_regression_test = b.addSystemCommand(&.{
+ "sh",
+ b.pathFromRoot("tests/rocknix_build_regression_test.sh"),
+ });
+ rocknix_build_regression_test.setCwd(b.path("."));
+ test_step.dependOn(&rocknix_build_regression_test.step);
+
const test_roots = [_][]const u8{
"src/app/state.zig",
"src/catalog/catalog_parser.zig",
"src/catalog/catalog_search.zig",
"src/input/wire_encoder.zig",
+ "src/input/guide_chord.zig",
+ "src/session/message_protocol.zig",
+ "src/ui/keyboard.zig",
+ "src/ui/navigation_repeat.zig",
+ "src/ui/persistent_settings.zig",
+ "src/ui/stream_dimensions.zig",
"src/media/rtp/h264_depacketizer.zig",
"src/net/json_reader.zig",
"src/net/json_writer.zig",
@@ -159,6 +491,20 @@ pub fn build(b: *std.Build) void {
"video-decoder-selection",
"src/media/video/video_decoder_selection.zig",
);
+ const cedar_loader_object =
+ addHostZigObject(b, "cedar-loader", "src/media/video/cedar_loader.zig");
+ const cedar_decoder_object = addHostZigObject(
+ b,
+ "video-decoder-cedar",
+ "src/media/video/video_decoder_cedar.zig",
+ );
+ const mpp_loader_object =
+ addHostZigObject(b, "mpp-loader", "src/media/video/mpp_loader.zig");
+ const mpp_decoder_object = addHostZigObject(
+ b,
+ "video-decoder-mpp",
+ "src/media/video/video_decoder_mpp.zig",
+ );
const decoder_contract_test = addHostCExecutable(
b,
@@ -192,12 +538,8 @@ pub fn build(b: *std.Build) void {
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},
+ &.{"tests/video_decoder_cedar_test.c"},
+ &.{ video_decoder_object, cedar_loader_object, cedar_decoder_object },
true,
);
const run_cedar_decoder_test = b.addRunArtifact(cedar_decoder_test);
@@ -213,11 +555,8 @@ pub fn build(b: *std.Build) void {
const mpp_loader_test = addHostCExecutable(
b,
"mpp-loader-test",
- &.{
- "src/media/video/mpp_loader.c",
- "tests/mpp_loader_test.c",
- },
- &.{},
+ &.{"tests/mpp_loader_test.c"},
+ &.{mpp_loader_object},
true,
);
const run_mpp_loader_test = b.addRunArtifact(mpp_loader_test);
@@ -229,12 +568,8 @@ pub fn build(b: *std.Build) void {
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},
+ &.{"tests/video_decoder_mpp_test.c"},
+ &.{ video_decoder_object, mpp_loader_object, mpp_decoder_object },
true,
);
const run_mpp_decoder_test = b.addRunArtifact(mpp_decoder_test);
diff --git a/packaging/portmaster/greenovercast/GreenOvercast.sh b/packaging/portmaster/greenovercast/GreenOvercast.sh
index a61335f..f88823d 100644
--- a/packaging/portmaster/greenovercast/GreenOvercast.sh
+++ b/packaging/portmaster/greenovercast/GreenOvercast.sh
@@ -19,29 +19,47 @@ source "$controlfolder/control.txt"
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
get_controls
+# AmberELEC's shared GO-Super mapping exposes R36S R3 as Guide.
+case "$sdl_controllerconfig" in
+190000004b4800000011000000010000,GO-Super\ Gamepad,*guide:b15,*)
+ sdl_controllerconfig=${sdl_controllerconfig/,guide:b15,/,rightstick:b15,guide:b16,}
+ ;;
+esac
+
GAMEDIR="/$directory/ports/greenovercast"
cd "$GAMEDIR" || exit 1
+rocknix_h700_modules="$GAMEDIR/rocknix/h700/cedrus-modules"
+
+finish() {
+ status=$?
+ trap - EXIT HUP INT TERM
+ if [ "$CFW_NAME" = "ROCKNIX" ] && [ -x "$rocknix_h700_modules" ]; then
+ $ESUDO "$rocknix_h700_modules" unload ||
+ echo "Unable to unload the ROCKNIX H700 video decoder." >&2
+ fi
+ pm_finish
+ exit "$status"
+}
+trap finish EXIT HUP INT TERM
+
fail() {
pm_show_error "$1"
- pm_finish
exit 1
}
config_dir="$XDG_CONFIG_HOME/greenovercast"
-if [ "$CFW_NAME" = "knulli" ]; then
- credential_dir="${XDG_RUNTIME_DIR:-/var/run}/greenovercast"
-else
- credential_dir="$config_dir"
-fi
+artwork_cache_dir="$config_dir/artwork"
+credential_dir="$config_dir"
-mkdir -p "$config_dir" "$credential_dir" || fail "Unable to create GreenOvercast's storage."
+mkdir -p "$config_dir" "$credential_dir" "$artwork_cache_dir" || fail "Unable to create GreenOvercast's storage."
chmod 700 "$credential_dir" || fail "Unable to protect GreenOvercast's private storage."
credential_file="$credential_dir/tokens.bin"
credential_key_file="$credential_dir/tokens.key"
video_bootstrap_file="$credential_dir/h264-parameter-sets.bin"
catalog_file="$credential_dir/catalog.tsv"
+settings_file="$config_dir/settings.tsv"
log_file="$credential_dir/greenovercast.log"
: >"$log_file"
@@ -52,10 +70,18 @@ for private_file in "$credential_file" "$credential_key_file" "$catalog_file"; d
[ ! -e "$private_file" ] || chmod 600 "$private_file"
done
+if [ "$CFW_NAME" = "ROCKNIX" ] && [ -f "$rocknix_h700_modules" ]; then
+ $ESUDO chmod +x "$rocknix_h700_modules"
+ $ESUDO "$rocknix_h700_modules" load ||
+ fail "Unable to start the ROCKNIX H700 video decoder."
+fi
+
export GREENOVERCAST_TOKEN_FILE="$credential_file"
export GREENOVERCAST_TOKEN_KEY_FILE="$credential_key_file"
export GREENOVERCAST_H264_BOOTSTRAP_FILE="$video_bootstrap_file"
export GREENOVERCAST_CATALOG_FILE="$catalog_file"
+export GREENOVERCAST_SETTINGS_FILE="$settings_file"
+export GREENOVERCAST_ARTWORK_CACHE_DIR="$artwork_cache_dir"
export GREENOVERCAST_CEDAR_LIBRARY="$GAMEDIR/libgreenovercast-cedar.so"
export GREENOVERCAST_MPP_LIBRARY="$GAMEDIR/libgreenovercast-mpp.so"
export LD_LIBRARY_PATH="$GAMEDIR:${LD_LIBRARY_PATH:-}"
@@ -88,6 +114,4 @@ $ESUDO chmod +x "$GAMEDIR/webrtc_stream.aarch64"
pm_platform_helper "$GAMEDIR/webrtc_stream.aarch64"
"$GAMEDIR/webrtc_stream.aarch64" "$1"
status=$?
-
-pm_finish
exit "$status"
diff --git a/packaging/portmaster/greenovercast/README.md b/packaging/portmaster/greenovercast/README.md
index 7550b84..f5aaba4 100644
--- a/packaging/portmaster/greenovercast/README.md
+++ b/packaging/portmaster/greenovercast/README.md
@@ -6,23 +6,27 @@ access.
The first launch shows a Microsoft device code. Open
`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.
+in.
-Tested on the RG35XX-H with muOS and the RG40XX-H with Knulli.
+Tested on the RG35XX-H with muOS, the RG40XX-H with Knulli and ROCKNIX
+20260801, the Miyoo Flip with SpruceOS, and the R36S with AmberELEC.
The release requires glibc 2.38 or newer. ArkOS is not supported.
## Controls
-| Button | Action |
-| ------------------ | ------------------------------------------------- |
-| D-Pad / Left Stick | Navigate the library and keyboard / Xbox movement |
-| A | Select or type / Xbox A |
-| B | Back or cancel / Xbox B |
-| X | Open search or delete a letter / Xbox X |
-| Y | Toggle 16:9/4:3 or clear search / Xbox Y |
-| L1 / R1 | Xbox LB / RB |
-| L2 / R2 | Xbox LT / RT |
-| Start | Apply a search / Xbox Menu |
-| Select | Xbox View |
-| L3 + R3 | Xbox Guide |
-| Select + Start | Exit GreenOvercast |
+| Button | Library / menus | In game |
+| ------------------ | --------------------------------- | --------------------------------- |
+| D-Pad / Left Stick | Navigate; hold to scroll faster | Movement |
+| A | Select, play, or type | Xbox A |
+| B | Back or cancel | Xbox B |
+| X | Search or delete a letter | Xbox X |
+| Y | Favorite a game or clear search | Xbox Y |
+| L1 / R1 | Switch All / Favorites | Xbox LB / RB |
+| L2 / R2 | Jump by first letter | Xbox LT / RT |
+| Start | Settings or apply search | Xbox Menu |
+| Select | — | Xbox View |
+| L3 + R3 | — | Xbox Guide |
+| Select + Start | Exit after holding for one second | Exit after holding for one second |
+
+Settings include Xbox/Nintendo face-button layouts, game artwork, and Sign
+out. Games that return a 16:9 stream remain letterboxed on 4:3 displays.
diff --git a/packaging/portmaster/greenovercast/greenovercast/CEDAR-SOURCE.md b/packaging/portmaster/greenovercast/greenovercast/CEDAR-SOURCE.md
index 4744ad3..05a069a 100644
--- a/packaging/portmaster/greenovercast/greenovercast/CEDAR-SOURCE.md
+++ b/packaging/portmaster/greenovercast/greenovercast/CEDAR-SOURCE.md
@@ -5,9 +5,8 @@ Allwinner's `media-codec` repository at commit
`a912bbe300d522e199001bd903bab22e54eff37b`. The upstream source is
.
-GreenOvercast builds these files into the replaceable
-`libgreenovercast-cedar.so`; they are never linked into the MPL executable.
-`tools/build-cedarx.sh` is the complete build recipe.
+GreenOvercast builds these files into `libgreenovercast-cedar.so`. The build
+recipe is in `build.zig`.
The maintained changes are limited to the aarch64/H616 port:
diff --git a/packaging/portmaster/greenovercast/greenovercast/FFMPEG-SOURCE.md b/packaging/portmaster/greenovercast/greenovercast/FFMPEG-SOURCE.md
index 3d4d93b..cc98442 100644
--- a/packaging/portmaster/greenovercast/greenovercast/FFMPEG-SOURCE.md
+++ b/packaging/portmaster/greenovercast/greenovercast/FFMPEG-SOURCE.md
@@ -1,12 +1,18 @@
# FFmpeg
GreenOvercast ships `libavcodec`, `libavutil`, and `libswscale` from FFmpeg
-4.4.8 under LGPLv2.1-or-later.
+9.0 under LGPLv2.1-or-later.
-Source:
+Source:
-SHA-256: `c73848c4ae283d9eaee7be3b276affbc3543380483555500d0dd2c9b7e1c39c3`
+SHA-256: `7f607a00dd0d28a729d5a4811205812eef01cf6ef6155025febb6f36a9062d52`
+
+H.264 V4L2 Request support comes from the ROCKNIX patch at:
+
+
+
+Patch SHA-256: `afd04c202c27081c355d8d34b58a52c4141de26433007e27ed6e0d2093d10d3c`
`tools/build-dependencies.sh` contains the complete build configuration. The
-libraries are dynamically linked and may be replaced with ABI-compatible
-builds. The license text is in `licenses/LICENSE.FFmpeg.txt`.
+libraries are dynamically linked. The license text is in
+`licenses/LICENSE.FFmpeg.txt`.
diff --git a/packaging/portmaster/greenovercast/greenovercast/ROCKNIX-H700-SOURCE.md b/packaging/portmaster/greenovercast/greenovercast/ROCKNIX-H700-SOURCE.md
new file mode 100644
index 0000000..a93f96e
--- /dev/null
+++ b/packaging/portmaster/greenovercast/greenovercast/ROCKNIX-H700-SOURCE.md
@@ -0,0 +1,19 @@
+# ROCKNIX H700 kernel modules
+
+ROCKNIX 20260801 does not expose the H700 video engine. This package includes
+two modules built for its stock 7.0.11 kernel: the Linux Cedrus driver and a
+device-tree overlay that adds the video-engine node.
+
+The corresponding source is in `vendor/rocknix-h700-cedrus/` in the
+GreenOvercast source release. `tools/build-rocknix-h700-cedrus.sh` rebuilds the
+modules from an exact prepared ROCKNIX kernel tree.
+
+Source versions:
+
+- ROCKNIX `20260801`, commit `3b9cb1f6bf48ee9ca0cf01edd9af52ca2a6b73fb`
+- Linux `7.0.11`
+- config SHA-256 `ea1abaf7109d6132e0ecd3cee51d8e08cde5e2143813f017ed35399950482081`
+- `Module.symvers` SHA-256 `b95c5a532ae10737d39bac79823e1977db7ec603410b4ab2b77edabc8dd41674`
+
+The modules are GPL-2.0-only. The H616 support is based on the upstream Cedrus
+work linked from `vendor/manifest.lock`.
diff --git a/packaging/portmaster/greenovercast/greenovercast/licenses/LICENSE.Linux.txt b/packaging/portmaster/greenovercast/greenovercast/licenses/LICENSE.Linux.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/packaging/portmaster/greenovercast/greenovercast/licenses/LICENSE.Linux.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/packaging/portmaster/greenovercast/greenovercast/licenses/LICENSE.libudev-zero.txt b/packaging/portmaster/greenovercast/greenovercast/licenses/LICENSE.libudev-zero.txt
new file mode 100644
index 0000000..448d38a
--- /dev/null
+++ b/packaging/portmaster/greenovercast/greenovercast/licenses/LICENSE.libudev-zero.txt
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2020-2021 illiliti
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/cedrus-modules b/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/cedrus-modules
new file mode 100644
index 0000000..8486b06
--- /dev/null
+++ b/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/cedrus-modules
@@ -0,0 +1,102 @@
+#!/bin/sh
+set -u
+
+action=${1:?usage: cedrus-modules load|unload}
+state_file=/run/greenovercast-rocknix-h700-modules.loaded
+module_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
+overlay_module="$module_dir/greenovercast_h700_overlay.ko"
+cedrus_module="$module_dir/sunxi-cedrus.ko"
+
+module_loaded() {
+ grep -q "^$1 " /proc/modules 2>/dev/null
+}
+
+cedrus_available() {
+ for name_file in /sys/class/video4linux/video*/name; do
+ [ -r "$name_file" ] && [ "$(cat "$name_file")" = "cedrus" ] && return 0
+ done
+ return 1
+}
+
+release_value() {
+ sed -n "s/^$1=\"\(.*\)\"$/\1/p" /etc/os-release 2>/dev/null
+}
+
+supported_release() {
+ [ "$(uname -m)" = "aarch64" ] &&
+ [ "$(uname -r)" = "7.0.11" ] &&
+ [ "$(release_value OS_NAME)" = "ROCKNIX" ] &&
+ [ "$(release_value OS_VERSION)" = "20260801" ] &&
+ [ "$(release_value OS_BUILD)" = "official" ] &&
+ [ "$(release_value BUILD_ID)" = "3b9cb1f6bf48ee9ca0cf01edd9af52ca2a6b73fb" ] &&
+ [ "$(release_value HW_DEVICE)" = "H700" ] &&
+ h700_compatible
+}
+
+h700_compatible() {
+ for compatible_file in /proc/device-tree/compatible /sys/firmware/devicetree/base/compatible; do
+ if [ -r "$compatible_file" ] &&
+ tr '\0' '\n' <"$compatible_file" | grep -qx 'allwinner,sun50i-h700'; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+unload_owned() {
+ [ -f "$state_file" ] || return 0
+ if module_loaded sunxi_cedrus && ! rmmod sunxi_cedrus; then
+ return 1
+ fi
+ if module_loaded greenovercast_h700_overlay && ! rmmod greenovercast_h700_overlay; then
+ return 1
+ fi
+ rm -f "$state_file"
+}
+
+case "$action" in
+load)
+ if cedrus_available; then
+ exit 0
+ fi
+ if ! supported_release; then
+ echo "ROCKNIX H700 Cedrus modules do not match this firmware; using decoder fallback."
+ exit 0
+ fi
+
+ if [ -f "$state_file" ]; then
+ unload_owned || exit 1
+ elif module_loaded greenovercast_h700_overlay || module_loaded sunxi_cedrus; then
+ echo "Existing Cedrus modules are unavailable; using decoder fallback."
+ exit 0
+ fi
+
+ [ -r "$overlay_module" ] && [ -r "$cedrus_module" ] || {
+ echo "ROCKNIX H700 Cedrus modules are missing." >&2
+ exit 1
+ }
+ insmod "$overlay_module" || exit 1
+ if ! insmod "$cedrus_module"; then
+ rmmod greenovercast_h700_overlay 2>/dev/null || true
+ exit 1
+ fi
+ if ! cedrus_available; then
+ rmmod sunxi_cedrus 2>/dev/null || true
+ rmmod greenovercast_h700_overlay 2>/dev/null || true
+ echo "ROCKNIX H700 Cedrus did not register a video device." >&2
+ exit 1
+ fi
+ if ! printf '%s\n' "$(release_value BUILD_ID)" >"$state_file"; then
+ rmmod sunxi_cedrus 2>/dev/null || true
+ rmmod greenovercast_h700_overlay 2>/dev/null || true
+ exit 1
+ fi
+ ;;
+unload)
+ unload_owned
+ ;;
+*)
+ echo "usage: cedrus-modules load|unload" >&2
+ exit 2
+ ;;
+esac
diff --git a/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/greenovercast_h700_overlay.ko b/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/greenovercast_h700_overlay.ko
new file mode 100644
index 0000000..67d813e
Binary files /dev/null and b/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/greenovercast_h700_overlay.ko differ
diff --git a/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/sunxi-cedrus.ko b/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/sunxi-cedrus.ko
new file mode 100644
index 0000000..d80ae2d
Binary files /dev/null and b/packaging/portmaster/greenovercast/greenovercast/rocknix/h700/sunxi-cedrus.ko differ
diff --git a/packaging/portmaster/greenovercast/screenshot.png b/packaging/portmaster/greenovercast/screenshot.png
index b5a577f..a80b362 100644
Binary files a/packaging/portmaster/greenovercast/screenshot.png and b/packaging/portmaster/greenovercast/screenshot.png differ
diff --git a/src/app/release.zig b/src/app/release.zig
index 2f2f866..06d8021 100644
--- a/src/app/release.zig
+++ b/src/app/release.zig
@@ -20,6 +20,7 @@ pub const Result = enum {
session_ended,
missing_credentials,
reauth_required,
+ signed_out,
failed,
};
@@ -246,15 +247,31 @@ pub const Release = struct {
pub fn pickTitle(self: *Release) Result {
if (self.catalog == null) return .failed;
const requested = std.mem.sliceTo(&self.requested_title, 0);
- const title_id = self.catalog.?.pick(requested) catch return .failed;
+ const selection = self.catalog.?.pick(requested) catch return .failed;
@memset(&self.requested_title, 0);
- const selected = title_id orelse return .cancelled;
+ const selected = switch (selection) {
+ .title_id => |value| value,
+ .cancelled => return .cancelled,
+ .sign_out => return .signed_out,
+ };
if (selected.len >= self.title_id.len) return .failed;
@memset(&self.title_id, 0);
@memcpy(self.title_id[0..selected.len], selected);
return .ok;
}
+ pub fn signOut(self: *Release) Result {
+ self.drawLoading("SIGNING OUT", "REMOVING XBOX CREDENTIALS", null);
+ if (c.go_xbox_auth_sign_out(self.auth) != 0) {
+ std.debug.print("Xbox credentials could not be removed\n", .{});
+ return .failed;
+ }
+ if (self.catalog) |catalog| catalog.destroy();
+ self.catalog = null;
+ @memset(&self.title_id, 0);
+ return .ok;
+ }
+
pub fn createSession(self: *Release) Result {
if (self.title_id[0] == 0) return .failed;
self.drawLoading("STARTING GAME", "ALLOCATING CLOUD SESSION", "B CANCEL");
@@ -300,10 +317,6 @@ pub const Release = struct {
debug("Setting up WebRTC\n", .{});
const stream_width = c.go_handheld_ui_stream_width(self.ui());
const stream_height = c.go_handheld_ui_stream_height(self.ui());
- if (stream_width * 3 == stream_height * 4)
- c.go_video_pipeline_set_crop_aspect(self.video, stream_width, stream_height)
- else
- c.go_video_pipeline_set_crop_aspect(self.video, 0, 0);
self.webrtc = c.go_webrtc_session_create(
self.cloud,
self.video,
@@ -430,7 +443,7 @@ pub const Release = struct {
const audio = c.go_audio_pipeline_stats(self.audio);
const cloud = c.go_cloud_session_stats(self.cloud);
std.debug.print(
- "[{d}s] video_rtp={d} payload={d} rejected={d}/pt{d} aus={d} frames={d}/{d} " ++
+ "[{d}s] video_rtp={d} payload={d} rejected={d}/pt{d} aus={d} frames={d}/{d} source={d}x{d} " ++
"nals={d}/{d}/{d}/{d} ts={d} synced={d} gaps={d} missing={d} late_rtp={d} " ++
"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",
@@ -443,6 +456,8 @@ pub const Release = struct {
video.access_units,
video.decoded_frames,
video.rendered_frames,
+ video.source_width,
+ video.source_height,
video.frame_nals,
video.idr_nals,
video.parameter_nals,
diff --git a/src/app/state.zig b/src/app/state.zig
index eb260c5..96ea55f 100644
--- a/src/app/state.zig
+++ b/src/app/state.zig
@@ -24,6 +24,7 @@ pub const Event = enum {
auth_begin_sign_in,
auth_success,
auth_rejected,
+ auth_sign_out,
catalog_loaded,
user_select_title,
session_ready,
@@ -63,6 +64,7 @@ pub fn transition(current: State, event: Event) State {
},
.catalog => switch (event) {
.user_select_title => .provisioning,
+ .auth_sign_out => .signed_out,
else => .catalog,
},
.provisioning => switch (event) {
@@ -115,6 +117,13 @@ test "rejected credentials return to controller-first sign-in" {
try std.testing.expectEqual(State.device_code_pending, state);
}
+test "sign out returns the catalog to authentication" {
+ var state = transition(.catalog, .auth_sign_out);
+ try std.testing.expectEqual(State.signed_out, state);
+ state = transition(state, .auth_begin_sign_in);
+ try std.testing.expectEqual(State.device_code_pending, state);
+}
+
test "catalog selection reaches streaming" {
var state = transition(.catalog, .user_select_title);
try std.testing.expectEqual(State.provisioning, state);
diff --git a/src/auth/token_store_adapter.c b/src/auth/token_store_adapter.c
index c509d78..2085e34 100644
--- a/src/auth/token_store_adapter.c
+++ b/src/auth/token_store_adapter.c
@@ -240,3 +240,14 @@ int go_token_store_save(const char* credential_path, const char* key_path,
free(file_data);
return result;
}
+
+int go_token_store_delete(const char* credential_path, const char* key_path) {
+ if (!credential_path || !key_path)
+ return -1;
+ int failed = 0;
+ if (unlink(credential_path) != 0 && errno != ENOENT)
+ failed = 1;
+ if (unlink(key_path) != 0 && errno != ENOENT)
+ failed = 1;
+ return failed ? -1 : 0;
+}
diff --git a/src/auth/token_store_adapter.h b/src/auth/token_store_adapter.h
index ee72daf..c683e4a 100644
--- a/src/auth/token_store_adapter.h
+++ b/src/auth/token_store_adapter.h
@@ -7,5 +7,6 @@ int go_token_store_load(const char* credential_path, const char* key_path, char*
size_t refresh_token_capacity);
int go_token_store_save(const char* credential_path, const char* key_path,
const char* refresh_token);
+int go_token_store_delete(const char* credential_path, const char* key_path);
#endif
diff --git a/src/auth/xbox_auth.h b/src/auth/xbox_auth.h
index 665e968..9cd4464 100644
--- a/src/auth/xbox_auth.h
+++ b/src/auth/xbox_auth.h
@@ -19,6 +19,7 @@ GoXboxAuth* go_xbox_auth_create(void);
int go_xbox_auth_load_credentials(GoXboxAuth* auth);
int go_xbox_auth_device_sign_in(GoXboxAuth* auth, GoHandheldUi* ui);
GoXboxAuthResult go_xbox_auth_refresh(GoXboxAuth* auth);
+int go_xbox_auth_sign_out(GoXboxAuth* auth);
const char* go_xbox_auth_gssv_token(const GoXboxAuth* auth);
const char* go_xbox_auth_passport_token(const GoXboxAuth* auth);
void go_xbox_auth_destroy(GoXboxAuth* auth);
diff --git a/src/auth/xbox_auth.zig b/src/auth/xbox_auth.zig
index f78ce44..c6c9666 100644
--- a/src/auth/xbox_auth.zig
+++ b/src/auth/xbox_auth.zig
@@ -1,11 +1,11 @@
const std = @import("std");
+const form = @import("form_writer");
+const json = @import("json_reader");
const c = @cImport({
@cInclude("SDL2/SDL.h");
- @cInclude("form_writer.h");
@cInclude("handheld_ui.h");
@cInclude("http_client.h");
- @cInclude("json_reader.h");
@cInclude("token_store_adapter.h");
});
@@ -91,14 +91,7 @@ fn buildForm(output: []u8, fields: []const FormField) ![:0]u8 {
.{ if (index == 0) "" else "&", field.key },
);
used += prefix.len;
- const encoded = c.go_form_urlencode(
- field.value.ptr,
- field.value.len,
- output[used..].ptr,
- output.len - used,
- );
- if (encoded < 0) return error.NoSpaceLeft;
- used += @intCast(encoded);
+ used += form.encode(field.value, output[used..]) catch return error.NoSpaceLeft;
}
if (used >= output.len) return error.NoSpaceLeft;
output[used] = 0;
@@ -110,15 +103,13 @@ fn responseData(response: [*c]c.GoHttpResponse) ?[]const u8 {
return response.*.data[0..response.*.len];
}
-fn jsonString(data: []const u8, key: [*:0]const u8, output: []u8) ![]const u8 {
- const length = c.go_json_copy_string(data.ptr, data.len, key, output.ptr, output.len);
- if (length < 0) return error.MissingField;
- return output[0..@intCast(length)];
+fn jsonString(data: []const u8, key: []const u8, output: []u8) ![]const u8 {
+ const length = json.parseString(data, key, output) catch return error.MissingField;
+ return output[0..length];
}
-fn jsonUnsigned(data: []const u8, key: [*:0]const u8, fallback: c_uint) c_uint {
- var value: c_uint = 0;
- return if (c.go_json_unsigned(data.ptr, data.len, key, &value) == 0) value else fallback;
+fn jsonUnsigned(data: []const u8, key: []const u8, fallback: c_uint) c_uint {
+ return json.parseUnsigned(data, key) catch fallback;
}
fn requiresSignIn(response: [*c]c.GoHttpResponse) bool {
@@ -490,6 +481,18 @@ pub export fn go_xbox_auth_refresh(auth: ?*Auth) c_int {
return refresh(auth orelse return auth_failed) catch auth_failed;
}
+pub export fn go_xbox_auth_sign_out(auth: ?*Auth) c_int {
+ const handle = auth orelse return -1;
+ std.crypto.secureZero(u8, &handle.gssv_token);
+ std.crypto.secureZero(u8, &handle.user_token);
+ std.crypto.secureZero(u8, &handle.refresh_token);
+ std.crypto.secureZero(u8, &handle.passport_token);
+ return c.go_token_store_delete(
+ @ptrCast(&handle.token_path),
+ @ptrCast(&handle.token_key_path),
+ );
+}
+
pub export fn go_xbox_auth_gssv_token(auth: ?*const Auth) [*c]const u8 {
const handle = auth orelse return null;
return @ptrCast(&handle.gssv_token);
diff --git a/src/catalog/catalog_parser.h b/src/catalog/catalog_parser.h
index 264102e..f22d6de 100644
--- a/src/catalog/catalog_parser.h
+++ b/src/catalog/catalog_parser.h
@@ -11,12 +11,14 @@ enum {
GO_CATALOG_TITLE_ID_CAPACITY = 128,
GO_CATALOG_PRODUCT_ID_CAPACITY = 64,
GO_CATALOG_NAME_CAPACITY = 192,
+ GO_CATALOG_ARTWORK_URL_CAPACITY = 768,
};
typedef struct {
char title_id[GO_CATALOG_TITLE_ID_CAPACITY];
char product_id[GO_CATALOG_PRODUCT_ID_CAPACITY];
char name[GO_CATALOG_NAME_CAPACITY];
+ char artwork_url[GO_CATALOG_ARTWORK_URL_CAPACITY];
} GoCatalogTitle;
// Display name for a title, falling back to its id when no name is set.
diff --git a/src/catalog/catalog_parser.zig b/src/catalog/catalog_parser.zig
index 55108d4..5f94994 100644
--- a/src/catalog/catalog_parser.zig
+++ b/src/catalog/catalog_parser.zig
@@ -3,11 +3,13 @@ const std = @import("std");
pub const title_id_capacity = 128;
pub const product_id_capacity = 64;
pub const name_capacity = 192;
+pub const artwork_url_capacity = 768;
pub const Title = extern struct {
title_id: [title_id_capacity]u8,
product_id: [product_id_capacity]u8,
name: [name_capacity]u8,
+ artwork_url: [artwork_url_capacity]u8,
};
fn objectString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
@@ -150,6 +152,44 @@ fn findString(value: std.json.Value, key: []const u8) ?[]const u8 {
return null;
}
+fn findImage(value: std.json.Value, purpose: []const u8) ?[]const u8 {
+ switch (value) {
+ .object => |object| {
+ const image_purpose = objectString(object, "ImagePurpose");
+ const uri = objectString(object, "Uri");
+ if (image_purpose != null and uri != null and
+ std.mem.eql(u8, image_purpose.?, purpose)) return uri.?;
+ var iterator = object.iterator();
+ while (iterator.next()) |entry| {
+ if (findImage(entry.value_ptr.*, purpose)) |candidate| return candidate;
+ }
+ },
+ .array => |array| {
+ for (array.items) |item| {
+ if (findImage(item, purpose)) |candidate| return candidate;
+ }
+ },
+ else => {},
+ }
+ return null;
+}
+
+fn writeArtworkUrl(destination: []u8, uri: []const u8) bool {
+ const prefix = if (std.mem.startsWith(u8, uri, "//")) "https:" else "";
+ if (prefix.len == 0 and !std.mem.startsWith(u8, uri, "https://")) return false;
+ const suffix = if (std.mem.indexOfScalar(u8, uri, '?') == null)
+ "?w=320&h=480&format=jpg"
+ else
+ "&w=320&h=480&format=jpg";
+ const length = prefix.len + uri.len + suffix.len;
+ if (length >= destination.len) return false;
+ @memset(destination, 0);
+ @memcpy(destination[0..prefix.len], prefix);
+ @memcpy(destination[prefix.len..][0..uri.len], uri);
+ @memcpy(destination[prefix.len + uri.len ..][0..suffix.len], suffix);
+ return true;
+}
+
pub fn cString(bytes: []const u8) []const u8 {
return bytes[0 .. std.mem.indexOfScalar(u8, bytes, 0) orelse bytes.len];
}
@@ -158,14 +198,17 @@ fn applyMetadata(value: std.json.Value, titles: []Title, applied: *usize) void {
switch (value) {
.object => |object| {
if (objectString(object, "ProductId")) |product_id| {
- if (findString(value, "ProductTitle")) |name| {
- for (titles) |*title| {
- if (std.mem.eql(u8, cString(&title.product_id), product_id) and
- writeDisplayCString(&title.name, name))
- {
- applied.* += 1;
- break;
- }
+ for (titles) |*title| {
+ if (!std.mem.eql(u8, cString(&title.product_id), product_id)) continue;
+ var changed = false;
+ if (findString(value, "ProductTitle")) |name|
+ changed = writeDisplayCString(&title.name, name) or changed;
+ const artwork = findImage(value, "Poster") orelse findImage(value, "BoxArt");
+ if (artwork) |uri|
+ changed = writeArtworkUrl(&title.artwork_url, uri) or changed;
+ if (changed) {
+ applied.* += 1;
+ break;
}
}
}
@@ -245,11 +288,31 @@ test "metadata parsing follows nested localized properties" {
try std.testing.expect(writeCString(&titles[1].product_id, "P2"));
const fixture =
\\{"Products":[
- \\ {"ProductId":"P1","LocalizedProperties":[{"ProductTitle":"Hollow Knight"}]},
+ \\ {"ProductId":"P1","LocalizedProperties":[{"ProductTitle":"Hollow Knight","Images":[
+ \\ {"ImagePurpose":"BoxArt","Uri":"//images.example/box"},
+ \\ {"ImagePurpose":"Poster","Uri":"//images.example/poster"}
+ \\ ]}]},
\\ {"ProductId":"P2","LocalizedProperties":[{"ProductTitle":"Café\nRacer"}]}
\\]}
;
try std.testing.expectEqual(@as(usize, 2), try parseMetadata(fixture, &titles));
try std.testing.expectEqualStrings("Hollow Knight", cString(&titles[0].name));
try std.testing.expectEqualStrings("Caf? Racer", cString(&titles[1].name));
+ try std.testing.expectEqualStrings(
+ "https://images.example/poster?w=320&h=480&format=jpg",
+ cString(&titles[0].artwork_url),
+ );
+}
+
+test "invalid artwork does not replace a valid URL" {
+ var destination = [_]u8{0} ** artwork_url_capacity;
+ try std.testing.expect(writeArtworkUrl(&destination, "https://images.example/poster"));
+ try std.testing.expect(!writeArtworkUrl(&destination, "http://images.example/invalid"));
+ var too_long = [_]u8{'a'} ** artwork_url_capacity;
+ @memcpy(too_long[0..8], "https://");
+ try std.testing.expect(!writeArtworkUrl(&destination, &too_long));
+ try std.testing.expectEqualStrings(
+ "https://images.example/poster?w=320&h=480&format=jpg",
+ cString(&destination),
+ );
}
diff --git a/src/catalog/catalog_search.h b/src/catalog/catalog_search.h
deleted file mode 100644
index 9b6782c..0000000
--- a/src/catalog/catalog_search.h
+++ /dev/null
@@ -1,14 +0,0 @@
-#ifndef GREENOVERCAST_CATALOG_SEARCH_H
-#define GREENOVERCAST_CATALOG_SEARCH_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-int go_catalog_search_matches(const char* title, const char* query);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/src/catalog/catalog_search.zig b/src/catalog/catalog_search.zig
index 0b0384a..625ec6a 100644
--- a/src/catalog/catalog_search.zig
+++ b/src/catalog/catalog_search.zig
@@ -13,7 +13,7 @@ fn normalize(input: []const u8, output: []u8) ?[]const u8 {
return output[0..length];
}
-fn matches(title: []const u8, query: []const u8) bool {
+pub fn matches(title: []const u8, query: []const u8) bool {
var normalized_title_storage: [normalized_capacity]u8 = undefined;
var normalized_query_storage: [normalized_capacity]u8 = undefined;
const normalized_title = normalize(title, &normalized_title_storage) orelse return false;
@@ -22,12 +22,6 @@ fn matches(title: []const u8, query: []const u8) bool {
std.mem.indexOf(u8, normalized_title, normalized_query) != null;
}
-export fn go_catalog_search_matches(title: ?[*:0]const u8, query: ?[*:0]const u8) callconv(.c) c_int {
- const title_pointer = title orelse return 0;
- const query_pointer = query orelse return 0;
- return @intFromBool(matches(std.mem.span(title_pointer), std.mem.span(query_pointer)));
-}
-
test "search is case insensitive and ignores title separators" {
try std.testing.expect(matches("Hollow Knight: Voidheart Edition", "hollowk"));
try std.testing.expect(matches("Forza Horizon 5", "HORIZON5"));
diff --git a/src/catalog/service.zig b/src/catalog/service.zig
index 87d0538..6d02807 100644
--- a/src/catalog/service.zig
+++ b/src/catalog/service.zig
@@ -14,6 +14,12 @@ pub const LoadResult = enum {
cancelled,
};
+pub const PickResult = union(enum) {
+ title_id: []const u8,
+ cancelled,
+ sign_out,
+};
+
pub const Service = struct {
allocator: std.mem.Allocator,
titles: ?[]parser.Title = null,
@@ -77,7 +83,7 @@ pub const Service = struct {
if (self.count == 0) return error.EmptyCatalog;
self.applyCache();
- try self.fetchMissingNames();
+ try self.fetchMissingMetadata();
if (c.go_handheld_ui_cancelled(self.uiHandle()) != 0) return .cancelled;
for (titles[0..self.count]) |*title| {
@@ -96,7 +102,7 @@ pub const Service = struct {
return self.count;
}
- pub fn pick(self: *Service, requested: []const u8) !?[]const u8 {
+ pub fn pick(self: *Service, requested: []const u8) !PickResult {
const titles = self.titles orelse return error.NotLoaded;
if (self.count == 0) return error.EmptyCatalog;
@@ -109,12 +115,14 @@ pub const Service = struct {
@intCast(self.count),
@ptrCast(&requested_buffer),
);
- if (selected < 0) return null;
+ if (selected == c.GO_HANDHELD_UI_PICK_SIGN_OUT) return .sign_out;
+ if (selected == c.GO_HANDHELD_UI_PICK_CANCELLED) return .cancelled;
+ if (selected < 0) return error.InvalidSelection;
if (selected >= self.count) return error.InvalidSelection;
const title = &titles[@intCast(selected)];
const title_id = parser.cString(&title.title_id);
std.debug.print("Selected title: {s} ({s})\n", .{ titleName(title), title_id });
- return title_id;
+ return .{ .title_id = title_id };
}
pub fn destroy(self: *Service) void {
@@ -145,11 +153,15 @@ pub const Service = struct {
const line = std.mem.trimRight(u8, raw_line, "\r");
const separator = std.mem.indexOfScalar(u8, line, '\t') orelse continue;
const product_id = line[0..separator];
- const name = line[separator + 1 ..];
+ const remainder = line[separator + 1 ..];
+ const artwork_separator = std.mem.indexOfScalar(u8, remainder, '\t');
+ const name = if (artwork_separator) |index| remainder[0..index] else remainder;
+ const artwork = if (artwork_separator) |index| remainder[index + 1 ..] else "";
if (product_id.len == 0 or name.len == 0) continue;
for (titles[0..self.count]) |*title| {
if (std.mem.eql(u8, parser.cString(&title.product_id), product_id)) {
_ = parser.writeCString(&title.name, name);
+ if (artwork.len > 0) _ = parser.writeCString(&title.artwork_url, artwork);
break;
}
}
@@ -172,8 +184,9 @@ pub const Service = struct {
for (titles[0..self.count]) |*title| {
const product_id = parser.cString(&title.product_id);
const name = parser.cString(&title.name);
+ const artwork = parser.cString(&title.artwork_url);
if (product_id.len > 0 and name.len > 0)
- try writer.print("{s}\t{s}\n", .{ product_id, name });
+ try writer.print("{s}\t{s}\t{s}\n", .{ product_id, name, artwork });
}
try file.sync();
file.close();
@@ -181,7 +194,7 @@ pub const Service = struct {
try cwd.rename(temporary_path, path);
}
- fn fetchMissingNames(self: *Service) !void {
+ fn fetchMissingMetadata(self: *Service) !void {
const titles = self.titles orelse return;
var next: usize = 0;
var resolved: usize = 0;
@@ -190,7 +203,8 @@ pub const Service = struct {
var batch_count: usize = 0;
while (next < self.count and batch_count < metadata_batch) : (next += 1) {
if (parser.cString(&titles[next].product_id).len > 0 and
- parser.cString(&titles[next].name).len == 0)
+ (parser.cString(&titles[next].name).len == 0 or
+ parser.cString(&titles[next].artwork_url).len == 0))
{
indexes[batch_count] = next;
batch_count += 1;
@@ -234,7 +248,7 @@ pub const Service = struct {
var progress_buffer: [64]u8 = undefined;
const progress = try std.fmt.bufPrintZ(
&progress_buffer,
- "{d} OF {d} NAMES",
+ "{d} OF {d} GAMES",
.{ resolved, self.count },
);
c.go_handheld_ui_draw_loading(
diff --git a/src/input/controller.h b/src/input/controller.h
index f9d9b72..a43b2d4 100644
--- a/src/input/controller.h
+++ b/src/input/controller.h
@@ -11,9 +11,22 @@ extern "C" {
typedef struct GoControllerInput GoControllerInput;
+typedef enum {
+ GO_FACE_BUTTON_MODE_SYSTEM = 0,
+ GO_FACE_BUTTON_MODE_SWAPPED = 1,
+} GoFaceButtonMode;
+
GoControllerInput* go_controller_input_create(void);
void go_controller_input_destroy(GoControllerInput* input);
void go_controller_input_handle_event(GoControllerInput* input, const SDL_Event* event);
+int go_controller_input_event_is_active(const GoControllerInput* input,
+ const SDL_Event* event);
+void go_controller_input_set_face_button_mode(GoControllerInput* input, GoFaceButtonMode mode);
+SDL_GameControllerButton go_controller_input_map_button(const GoControllerInput* input,
+ Uint8 physical_button);
+int go_controller_input_button_pressed(const GoControllerInput* input,
+ SDL_GameControllerButton semantic_button);
+Sint16 go_controller_input_axis(const GoControllerInput* input, SDL_GameControllerAxis axis);
size_t go_controller_input_encode_metadata(GoControllerInput* input, uint8_t* output,
size_t capacity);
size_t go_controller_input_encode(GoControllerInput* input, uint8_t* output, size_t capacity);
diff --git a/src/input/controller.zig b/src/input/controller.zig
index f49c017..b75896f 100644
--- a/src/input/controller.zig
+++ b/src/input/controller.zig
@@ -1,14 +1,19 @@
const std = @import("std");
+const guide_chord = @import("guide_chord.zig");
+const wire = @import("wire_encoder.zig");
const c = @cImport({
@cInclude("SDL2/SDL.h");
- @cInclude("wire_encoder.h");
+ @cInclude("controller.h");
});
const Input = struct {
controller: ?*c.SDL_GameController = null,
sequence: u32 = 0,
exit_held_since: c.Uint32 = 0,
+ face_button_mode: c.GoFaceButtonMode = c.GO_FACE_BUTTON_MODE_SYSTEM,
+ guide_chord: guide_chord.State = .{},
+ pressed_buttons: u32 = 0,
};
fn debug(comptime format: []const u8, args: anytype) void {
@@ -20,6 +25,15 @@ fn activeControllerId(input: *const Input) c.SDL_JoystickID {
return c.SDL_JoystickInstanceID(c.SDL_GameControllerGetJoystick(controller));
}
+fn controllerDeviceIndex(instance_id: c.SDL_JoystickID) ?c_int {
+ var index: c_int = 0;
+ while (index < c.SDL_NumJoysticks()) : (index += 1) {
+ if (c.SDL_IsGameController(index) != 0 and
+ c.SDL_JoystickGetDeviceInstanceID(index) == instance_id) return index;
+ }
+ return null;
+}
+
fn openController(input: *Input, device_index: c_int) void {
if (c.SDL_IsGameController(device_index) == 0) return;
const next = c.SDL_GameControllerOpen(device_index) orelse return;
@@ -32,6 +46,8 @@ fn openController(input: *Input, device_index: c_int) void {
if (input.controller) |controller| c.SDL_GameControllerClose(controller);
input.controller = next;
input.exit_held_since = 0;
+ input.guide_chord.reset();
+ input.pressed_buttons = 0;
const name = c.SDL_GameControllerName(next);
debug("Controller active: {s} ({d} btn, {d} axes)\n", .{
if (name != null) std.mem.span(@as([*:0]const u8, @ptrCast(name))) else "unknown",
@@ -42,7 +58,9 @@ fn openController(input: *Input, device_index: c_int) void {
fn button(input: *const Input, value: c.SDL_GameControllerButton) bool {
const controller = input.controller orelse return false;
- return c.SDL_GameControllerGetButton(controller, value) != 0;
+ const index: u5 = @intCast(value);
+ return input.pressed_buttons & (@as(u32, 1) << index) != 0 or
+ c.SDL_GameControllerGetButton(controller, value) != 0;
}
fn axis(input: *const Input, value: c.SDL_GameControllerAxis) i16 {
@@ -50,6 +68,21 @@ fn axis(input: *const Input, value: c.SDL_GameControllerAxis) i16 {
return c.SDL_GameControllerGetAxis(controller, value);
}
+fn mappedButton(mode: c.GoFaceButtonMode, value: c.SDL_GameControllerButton) c.SDL_GameControllerButton {
+ if (mode != c.GO_FACE_BUTTON_MODE_SWAPPED) return value;
+ return switch (value) {
+ c.SDL_CONTROLLER_BUTTON_A => c.SDL_CONTROLLER_BUTTON_B,
+ c.SDL_CONTROLLER_BUTTON_B => c.SDL_CONTROLLER_BUTTON_A,
+ c.SDL_CONTROLLER_BUTTON_X => c.SDL_CONTROLLER_BUTTON_Y,
+ c.SDL_CONTROLLER_BUTTON_Y => c.SDL_CONTROLLER_BUTTON_X,
+ else => value,
+ };
+}
+
+fn semanticButtonPressed(input: *const Input, semantic: c.SDL_GameControllerButton) bool {
+ return button(input, mappedButton(input.face_button_mode, semantic));
+}
+
fn trigger(input: *const Input, value: c.SDL_GameControllerAxis) u16 {
const position = axis(input, value);
if (position <= 0) return 0;
@@ -88,6 +121,23 @@ pub export fn go_controller_input_destroy(input: ?*Input) void {
pub export fn go_controller_input_handle_event(input: ?*Input, event: ?*const c.SDL_Event) void {
const handle = input orelse return;
const current_event = event orelse return;
+ if (current_event.type == c.SDL_CONTROLLERBUTTONDOWN and
+ current_event.cbutton.which != activeControllerId(handle))
+ {
+ if (controllerDeviceIndex(current_event.cbutton.which)) |index| openController(handle, index);
+ }
+ if (current_event.type == c.SDL_CONTROLLERBUTTONDOWN or
+ current_event.type == c.SDL_CONTROLLERBUTTONUP)
+ {
+ if (current_event.cbutton.which != activeControllerId(handle)) return;
+ const index: u5 = @intCast(current_event.cbutton.button);
+ const mask = @as(u32, 1) << index;
+ if (current_event.type == c.SDL_CONTROLLERBUTTONDOWN)
+ handle.pressed_buttons |= mask
+ else
+ handle.pressed_buttons &= ~mask;
+ return;
+ }
if (current_event.type == c.SDL_CONTROLLERDEVICEADDED) {
openController(handle, current_event.cdevice.which);
return;
@@ -98,6 +148,8 @@ pub export fn go_controller_input_handle_event(input: ?*Input, event: ?*const c.
if (handle.controller) |controller| c.SDL_GameControllerClose(controller);
handle.controller = null;
handle.exit_held_since = 0;
+ handle.guide_chord.reset();
+ handle.pressed_buttons = 0;
var index: c_int = 0;
while (index < c.SDL_NumJoysticks()) : (index += 1) {
if (c.SDL_IsGameController(index) != 0) {
@@ -107,6 +159,70 @@ pub export fn go_controller_input_handle_event(input: ?*Input, event: ?*const c.
}
}
+pub export fn go_controller_input_event_is_active(
+ input: ?*const Input,
+ event: ?*const c.SDL_Event,
+) c_int {
+ const handle = input orelse return 0;
+ const current_event = event orelse return 0;
+ const instance_id = switch (current_event.type) {
+ c.SDL_CONTROLLERBUTTONDOWN, c.SDL_CONTROLLERBUTTONUP => current_event.cbutton.which,
+ c.SDL_CONTROLLERAXISMOTION => current_event.caxis.which,
+ else => return 1,
+ };
+ return @intFromBool(instance_id == activeControllerId(handle));
+}
+
+pub export fn go_controller_input_set_face_button_mode(
+ input: ?*Input,
+ mode: c.GoFaceButtonMode,
+) void {
+ const handle = input orelse return;
+ if (mode == c.GO_FACE_BUTTON_MODE_SYSTEM or mode == c.GO_FACE_BUTTON_MODE_SWAPPED)
+ handle.face_button_mode = mode;
+}
+
+pub export fn go_controller_input_map_button(
+ input: ?*const Input,
+ physical_button: c.Uint8,
+) c.SDL_GameControllerButton {
+ const handle = input orelse return @intCast(physical_button);
+ return mappedButton(handle.face_button_mode, @intCast(physical_button));
+}
+
+test "face button override preserves or swaps SDL semantics" {
+ try std.testing.expectEqual(
+ c.SDL_CONTROLLER_BUTTON_A,
+ mappedButton(c.GO_FACE_BUTTON_MODE_SYSTEM, c.SDL_CONTROLLER_BUTTON_A),
+ );
+ try std.testing.expectEqual(
+ c.SDL_CONTROLLER_BUTTON_B,
+ mappedButton(c.GO_FACE_BUTTON_MODE_SWAPPED, c.SDL_CONTROLLER_BUTTON_A),
+ );
+ try std.testing.expectEqual(
+ c.SDL_CONTROLLER_BUTTON_X,
+ mappedButton(c.GO_FACE_BUTTON_MODE_SWAPPED, c.SDL_CONTROLLER_BUTTON_Y),
+ );
+ try std.testing.expectEqual(
+ c.SDL_CONTROLLER_BUTTON_START,
+ mappedButton(c.GO_FACE_BUTTON_MODE_SWAPPED, c.SDL_CONTROLLER_BUTTON_START),
+ );
+}
+
+pub export fn go_controller_input_button_pressed(
+ input: ?*const Input,
+ semantic_button: c.SDL_GameControllerButton,
+) c_int {
+ return @intFromBool(semanticButtonPressed(input orelse return 0, semantic_button));
+}
+
+pub export fn go_controller_input_axis(
+ input: ?*const Input,
+ controller_axis: c.SDL_GameControllerAxis,
+) c.Sint16 {
+ return axis(input orelse return 0, controller_axis);
+}
+
pub export fn go_controller_input_encode_metadata(
input: ?*Input,
output: ?[*]u8,
@@ -134,30 +250,33 @@ pub export fn go_controller_input_encode(
if (capacity < 38) return 0;
var source_buttons: u32 = 0;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_A)) source_buttons |= c.GO_CONTROLLER_A;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_B)) source_buttons |= c.GO_CONTROLLER_B;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_X)) source_buttons |= c.GO_CONTROLLER_X;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_Y)) source_buttons |= c.GO_CONTROLLER_Y;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) source_buttons |= c.GO_CONTROLLER_LEFT_SHOULDER;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) source_buttons |= c.GO_CONTROLLER_RIGHT_SHOULDER;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_BACK)) source_buttons |= c.GO_CONTROLLER_BACK;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_START)) source_buttons |= c.GO_CONTROLLER_START;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_UP)) source_buttons |= c.GO_CONTROLLER_DPAD_UP;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_DOWN)) source_buttons |= c.GO_CONTROLLER_DPAD_DOWN;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_LEFT)) source_buttons |= c.GO_CONTROLLER_DPAD_LEFT;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) source_buttons |= c.GO_CONTROLLER_DPAD_RIGHT;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_LEFTSTICK)) source_buttons |= c.GO_CONTROLLER_LEFT_STICK;
- if (button(handle, c.SDL_CONTROLLER_BUTTON_RIGHTSTICK)) source_buttons |= c.GO_CONTROLLER_RIGHT_STICK;
+ if (semanticButtonPressed(handle, c.SDL_CONTROLLER_BUTTON_A)) source_buttons |= wire.SourceButton.a;
+ if (semanticButtonPressed(handle, c.SDL_CONTROLLER_BUTTON_B)) source_buttons |= wire.SourceButton.b;
+ if (semanticButtonPressed(handle, c.SDL_CONTROLLER_BUTTON_X)) source_buttons |= wire.SourceButton.x;
+ if (semanticButtonPressed(handle, c.SDL_CONTROLLER_BUTTON_Y)) source_buttons |= wire.SourceButton.y;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) source_buttons |= wire.SourceButton.left_shoulder;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) source_buttons |= wire.SourceButton.right_shoulder;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_BACK)) source_buttons |= wire.SourceButton.back;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_START)) source_buttons |= wire.SourceButton.start;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_UP)) source_buttons |= wire.SourceButton.dpad_up;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_DOWN)) source_buttons |= wire.SourceButton.dpad_down;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_LEFT)) source_buttons |= wire.SourceButton.dpad_left;
+ if (button(handle, c.SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) source_buttons |= wire.SourceButton.dpad_right;
+ const left_stick = button(handle, c.SDL_CONTROLLER_BUTTON_LEFTSTICK);
+ const right_stick = button(handle, c.SDL_CONTROLLER_BUTTON_RIGHTSTICK);
+ const stick_buttons = handle.guide_chord.update(left_stick, right_stick);
+ if (stick_buttons.left) source_buttons |= wire.SourceButton.left_stick;
+ if (stick_buttons.right) source_buttons |= wire.SourceButton.right_stick;
const raw_left_y = axis(handle, c.SDL_CONTROLLER_AXIS_LEFTY);
const raw_right_y = axis(handle, c.SDL_CONTROLLER_AXIS_RIGHTY);
const left_y = if (raw_left_y == std.math.minInt(i16)) std.math.maxInt(i16) else -raw_left_y;
const right_y = if (raw_right_y == std.math.minInt(i16)) std.math.maxInt(i16) else -raw_right_y;
- c.go_xcloud_encode_gamepad(
- bytes,
+ wire.encodeGamepadRaw(
+ bytes[0..wire.PACKET_SIZE],
handle.sequence,
0.0,
- c.go_xcloud_button_mask(source_buttons),
+ wire.buttonMask(source_buttons),
axis(handle, c.SDL_CONTROLLER_AXIS_LEFTX),
left_y,
axis(handle, c.SDL_CONTROLLER_AXIS_RIGHTX),
@@ -166,7 +285,7 @@ pub export fn go_controller_input_encode(
trigger(handle, c.SDL_CONTROLLER_AXIS_TRIGGERRIGHT),
);
handle.sequence +%= 1;
- return 38;
+ return wire.PACKET_SIZE;
}
pub export fn go_controller_input_exit_held(
diff --git a/src/input/guide_chord.zig b/src/input/guide_chord.zig
new file mode 100644
index 0000000..cb7ec98
--- /dev/null
+++ b/src/input/guide_chord.zig
@@ -0,0 +1,55 @@
+const std = @import("std");
+
+pub const Buttons = struct {
+ left: bool = false,
+ right: bool = false,
+};
+
+pub const State = struct {
+ active: bool = false,
+ pulse_packets: u8 = 0,
+
+ pub fn reset(self: *State) void {
+ self.* = .{};
+ }
+
+ pub fn update(self: *State, left: bool, right: bool) Buttons {
+ if (self.active) {
+ if (self.pulse_packets > 0) {
+ self.pulse_packets -= 1;
+ return .{ .left = true, .right = true };
+ }
+ if (!left and !right) {
+ self.active = false;
+ return .{};
+ }
+ if (left != right) return .{ .left = left, .right = right };
+ return .{};
+ }
+ if (left and right) {
+ self.active = true;
+ self.pulse_packets = 7;
+ return .{ .left = true, .right = true };
+ }
+ return .{ .left = left, .right = right };
+ }
+};
+
+test "the guide chord pulses once and restores a held stick" {
+ var state = State{};
+ var buttons = state.update(true, true);
+ try std.testing.expect(buttons.left and buttons.right);
+ for (0..7) |_| {
+ buttons = state.update(true, true);
+ try std.testing.expect(buttons.left and buttons.right);
+ }
+ buttons = state.update(true, true);
+ try std.testing.expect(!buttons.left and !buttons.right);
+ buttons = state.update(true, false);
+ try std.testing.expect(buttons.left and !buttons.right);
+ buttons = state.update(true, true);
+ try std.testing.expect(!buttons.left and !buttons.right);
+ _ = state.update(false, false);
+ buttons = state.update(true, true);
+ try std.testing.expect(buttons.left and buttons.right);
+}
diff --git a/src/input/wire_encoder.h b/src/input/wire_encoder.h
deleted file mode 100644
index 5d648c0..0000000
--- a/src/input/wire_encoder.h
+++ /dev/null
@@ -1,28 +0,0 @@
-#ifndef GREENOVERCAST_WIRE_ENCODER_H
-#define GREENOVERCAST_WIRE_ENCODER_H
-
-#include
-
-enum GoControllerButton {
- GO_CONTROLLER_A = 1u << 0,
- GO_CONTROLLER_B = 1u << 1,
- GO_CONTROLLER_X = 1u << 2,
- GO_CONTROLLER_Y = 1u << 3,
- GO_CONTROLLER_LEFT_SHOULDER = 1u << 4,
- GO_CONTROLLER_RIGHT_SHOULDER = 1u << 5,
- GO_CONTROLLER_BACK = 1u << 6,
- GO_CONTROLLER_START = 1u << 7,
- GO_CONTROLLER_DPAD_UP = 1u << 8,
- GO_CONTROLLER_DPAD_DOWN = 1u << 9,
- GO_CONTROLLER_DPAD_LEFT = 1u << 10,
- GO_CONTROLLER_DPAD_RIGHT = 1u << 11,
- GO_CONTROLLER_LEFT_STICK = 1u << 12,
- GO_CONTROLLER_RIGHT_STICK = 1u << 13,
-};
-
-uint16_t go_xcloud_button_mask(uint32_t source_buttons);
-void go_xcloud_encode_gamepad(uint8_t* buffer, uint32_t sequence, double timestamp_ms,
- uint16_t buttons, int16_t left_x, int16_t left_y, int16_t right_x,
- int16_t right_y, uint16_t left_trigger, uint16_t right_trigger);
-
-#endif
diff --git a/src/input/wire_encoder.zig b/src/input/wire_encoder.zig
index 6c632c0..cd6de6e 100644
--- a/src/input/wire_encoder.zig
+++ b/src/input/wire_encoder.zig
@@ -79,10 +79,6 @@ pub fn buttonMask(source: u32) u16 {
return mask;
}
-pub export fn go_xcloud_button_mask(source: u32) u16 {
- return buttonMask(source);
-}
-
fn clampF32(v: f32, lo: f32, hi: f32) f32 {
return @max(lo, @min(hi, v));
}
@@ -103,7 +99,7 @@ pub fn encodeGamepad(buf: []u8, sequence: u32, timestamp_ms: f64, state: Gamepad
encodeGamepadRaw(buf, sequence, timestamp_ms, state.buttons, axisToI16(state.left_x), axisToI16(state.left_y), axisToI16(state.right_x), axisToI16(state.right_y), triggerToU16(state.left_trigger), triggerToU16(state.right_trigger));
}
-fn encodeGamepadRaw(buf: []u8, sequence: u32, timestamp_ms: f64, buttons: u16, left_x: i16, left_y: i16, right_x: i16, right_y: i16, left_trigger: u16, right_trigger: u16) void {
+pub fn encodeGamepadRaw(buf: []u8, sequence: u32, timestamp_ms: f64, buttons: u16, left_x: i16, left_y: i16, right_x: i16, right_y: i16, left_trigger: u16, right_trigger: u16) void {
std.debug.assert(buf.len >= PACKET_SIZE);
std.mem.writeInt(u16, buf[0..2], report_type_gamepad, .little);
@@ -123,10 +119,6 @@ fn encodeGamepadRaw(buf: []u8, sequence: u32, timestamp_ms: f64, buttons: u16, l
std.mem.writeInt(u32, buf[34..38], 1, .big);
}
-pub export fn go_xcloud_encode_gamepad(buf: [*]u8, sequence: u32, timestamp_ms: f64, buttons: u16, left_x: i16, left_y: i16, right_x: i16, right_y: i16, left_trigger: u16, right_trigger: u16) void {
- encodeGamepadRaw(buf[0..PACKET_SIZE], sequence, timestamp_ms, buttons, left_x, left_y, right_x, right_y, left_trigger, right_trigger);
-}
-
test "neutral state produces known bytes" {
var buf: [PACKET_SIZE]u8 = undefined;
encodeGamepad(&buf, 0, 0.0, .{});
diff --git a/src/main.zig b/src/main.zig
index 447f053..12fa0da 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -38,6 +38,48 @@ fn stage(result: release_mod.Result) enum { ok, cancelled, failed } {
};
}
+const CatalogFlow = enum { ok, cancelled, failed };
+
+fn openCatalog(
+ release: *release_mod.Release,
+ state: *state_mod.State,
+ sign_in_required: bool,
+) CatalogFlow {
+ var needs_sign_in = sign_in_required;
+ var reauth_attempted = sign_in_required;
+ while (true) {
+ if (needs_sign_in) {
+ reauth_attempted = true;
+ needs_sign_in = false;
+ if (!move(state, .auth_begin_sign_in, .device_code_pending)) return .failed;
+ switch (stage(release.deviceSignIn())) {
+ .ok => if (!move(state, .auth_success, .authenticating)) return .failed,
+ .cancelled => return .cancelled,
+ .failed => return .failed,
+ }
+ }
+
+ const auth_result = release.refreshAuth();
+ if (auth_result == .reauth_required and !reauth_attempted) {
+ if (!move(state, .auth_rejected, .signed_out)) return .failed;
+ needs_sign_in = true;
+ continue;
+ }
+ switch (stage(auth_result)) {
+ .ok => if (!move(state, .auth_success, .loading_catalog)) return .failed,
+ .cancelled => return .cancelled,
+ .failed => return .failed,
+ }
+ break;
+ }
+ switch (stage(release.loadCatalog())) {
+ .ok => if (!move(state, .catalog_loaded, .catalog)) return .failed,
+ .cancelled => return .cancelled,
+ .failed => return .failed,
+ }
+ return .ok;
+}
+
fn runSelectedSession(release: *release_mod.Release, state: *state_mod.State) release_mod.Result {
var result = release.createSession();
if (result != .ok) return result;
@@ -81,39 +123,23 @@ pub fn main() u8 {
return finish(&state, 1);
}
- var reauth_attempted = false;
+ switch (openCatalog(release, &state, needs_sign_in)) {
+ .ok => {},
+ .cancelled => return finish(&state, 0),
+ .failed => return finish(&state, 1),
+ }
while (true) {
- if (needs_sign_in) {
- reauth_attempted = true;
- needs_sign_in = false;
- if (!move(&state, .auth_begin_sign_in, .device_code_pending)) return 1;
- switch (stage(release.deviceSignIn())) {
- .ok => if (!move(&state, .auth_success, .authenticating)) return 1,
+ const selection = release.pickTitle();
+ if (selection == .signed_out) {
+ if (release.signOut() != .ok or !move(&state, .auth_sign_out, .signed_out))
+ return finish(&state, 1);
+ switch (openCatalog(release, &state, true)) {
+ .ok => continue,
.cancelled => return finish(&state, 0),
.failed => return finish(&state, 1),
}
}
-
- const auth_result = release.refreshAuth();
- if (auth_result == .reauth_required and !reauth_attempted) {
- if (!move(&state, .auth_rejected, .signed_out)) return 1;
- needs_sign_in = true;
- continue;
- }
- switch (stage(auth_result)) {
- .ok => if (!move(&state, .auth_success, .loading_catalog)) return 1,
- .cancelled => return finish(&state, 0),
- .failed => return finish(&state, 1),
- }
- break;
- }
- switch (stage(release.loadCatalog())) {
- .ok => if (!move(&state, .catalog_loaded, .catalog)) return 1,
- .cancelled => return finish(&state, 0),
- .failed => return finish(&state, 1),
- }
- while (true) {
- switch (stage(release.pickTitle())) {
+ switch (stage(selection)) {
.ok => if (!move(&state, .user_select_title, .provisioning)) return 1,
.cancelled => return finish(&state, 0),
.failed => return finish(&state, 1),
diff --git a/src/media/audio/audio_pipeline.c b/src/media/audio/audio_pipeline.c
deleted file mode 100644
index 2481506..0000000
--- a/src/media/audio/audio_pipeline.c
+++ /dev/null
@@ -1,207 +0,0 @@
-#include "audio_pipeline.h"
-
-#include
-#include
-#include
-#include
-
-#include "opus_adapter.h"
-#include "packet.h"
-
-#define AUDIO_PAYLOAD_TYPE 111
-#define AUDIO_QUEUE_CAPACITY 32
-#define AUDIO_TARGET_PENDING_PACKETS 2
-#define AUDIO_PACKET_MAX 2048
-#define AUDIO_MAX_SAMPLES (5760 * 2)
-#define AUDIO_TARGET_MAX_BYTES (48000 * 2 * 2 * 40 / 1000)
-#define AUDIO_HARD_RESET_BYTES (48000 * 2 * 2 * 120 / 1000)
-
-typedef struct {
- uint16_t length;
- uint8_t data[AUDIO_PACKET_MAX];
-} AudioPacket;
-
-struct GoAudioPipeline {
- SDL_AudioDeviceID device;
- GoOpus* decoder;
- AudioPacket queue[AUDIO_QUEUE_CAPACITY];
- int queue_head;
- int queue_tail;
- int queue_count;
- pthread_mutex_t lock;
- pthread_cond_t condition;
- pthread_t thread;
- int thread_started;
- atomic_int stop;
- atomic_int accepting_packets;
- atomic_int rtp_packets;
- atomic_int decoded_packets;
- atomic_int dropped_packets;
- atomic_int late_packets;
- atomic_int queue_resets;
-};
-
-static void* audio_worker(void* context) {
- GoAudioPipeline* pipeline = context;
- while (!atomic_load(&pipeline->stop)) {
- AudioPacket packet;
- pthread_mutex_lock(&pipeline->lock);
- while (pipeline->queue_count == 0 && !atomic_load(&pipeline->stop))
- pthread_cond_wait(&pipeline->condition, &pipeline->lock);
- if (atomic_load(&pipeline->stop)) {
- pthread_mutex_unlock(&pipeline->lock);
- break;
- }
- while (pipeline->queue_count > AUDIO_TARGET_PENDING_PACKETS) {
- pipeline->queue_head = (pipeline->queue_head + 1) % AUDIO_QUEUE_CAPACITY;
- pipeline->queue_count--;
- atomic_fetch_add(&pipeline->late_packets, 1);
- }
- packet = pipeline->queue[pipeline->queue_head];
- pipeline->queue_head = (pipeline->queue_head + 1) % AUDIO_QUEUE_CAPACITY;
- pipeline->queue_count--;
- pthread_mutex_unlock(&pipeline->lock);
-
- short pcm[AUDIO_MAX_SAMPLES];
- int samples =
- go_opus_decode(pipeline->decoder, packet.data, packet.length, pcm, AUDIO_MAX_SAMPLES);
- if (samples <= 0) {
- atomic_fetch_add(&pipeline->dropped_packets, 1);
- continue;
- }
- Uint32 output_bytes = (Uint32)samples * sizeof(short);
- Uint32 queued;
- while ((queued = SDL_GetQueuedAudioSize(pipeline->device)) + output_bytes >
- AUDIO_TARGET_MAX_BYTES &&
- !atomic_load(&pipeline->stop)) {
- if (queued > AUDIO_HARD_RESET_BYTES) {
- SDL_ClearQueuedAudio(pipeline->device);
- atomic_fetch_add(&pipeline->queue_resets, 1);
- break;
- }
- SDL_Delay(2);
- }
- if (atomic_load(&pipeline->stop))
- break;
- if (SDL_QueueAudio(pipeline->device, pcm, output_bytes) == 0)
- atomic_fetch_add(&pipeline->decoded_packets, 1);
- else
- atomic_fetch_add(&pipeline->dropped_packets, 1);
- }
- return NULL;
-}
-
-GoAudioPipeline* go_audio_pipeline_create(SDL_AudioDeviceID device) {
- if (!device)
- return NULL;
- GoAudioPipeline* pipeline = calloc(1, sizeof(*pipeline));
- if (!pipeline)
- return NULL;
- pipeline->device = device;
- pipeline->decoder = go_opus_create(48000, 2);
- if (!pipeline->decoder) {
- free(pipeline);
- return NULL;
- }
- if (pthread_mutex_init(&pipeline->lock, NULL) != 0) {
- go_opus_destroy(pipeline->decoder);
- free(pipeline);
- return NULL;
- }
- if (pthread_cond_init(&pipeline->condition, NULL) != 0) {
- pthread_mutex_destroy(&pipeline->lock);
- go_opus_destroy(pipeline->decoder);
- free(pipeline);
- return NULL;
- }
- return pipeline;
-}
-
-int go_audio_pipeline_start(GoAudioPipeline* pipeline) {
- if (!pipeline || pipeline->thread_started)
- return -1;
- SDL_ClearQueuedAudio(pipeline->device);
- atomic_store(&pipeline->stop, 0);
- atomic_store(&pipeline->accepting_packets, 1);
- if (pthread_create(&pipeline->thread, NULL, audio_worker, pipeline) != 0) {
- atomic_store(&pipeline->accepting_packets, 0);
- return -1;
- }
- pipeline->thread_started = 1;
- SDL_PauseAudioDevice(pipeline->device, 0);
- return 0;
-}
-
-void go_audio_pipeline_stop(GoAudioPipeline* pipeline) {
- if (!pipeline)
- return;
- atomic_store(&pipeline->accepting_packets, 0);
- if (pipeline->thread_started) {
- atomic_store(&pipeline->stop, 1);
- pthread_mutex_lock(&pipeline->lock);
- pthread_cond_broadcast(&pipeline->condition);
- pthread_mutex_unlock(&pipeline->lock);
- pthread_join(pipeline->thread, NULL);
- pipeline->thread_started = 0;
- }
- SDL_PauseAudioDevice(pipeline->device, 1);
- SDL_ClearQueuedAudio(pipeline->device);
- pthread_mutex_lock(&pipeline->lock);
- pipeline->queue_head = 0;
- pipeline->queue_tail = 0;
- pipeline->queue_count = 0;
- pthread_mutex_unlock(&pipeline->lock);
-}
-
-void go_audio_pipeline_push_rtp(GoAudioPipeline* pipeline, const uint8_t* packet, size_t length) {
- if (!pipeline || !packet || !atomic_load(&pipeline->accepting_packets))
- return;
- GoRtpPayload payload = go_rtp_parse_payload(packet, length, AUDIO_PAYLOAD_TYPE);
- if (!payload.accepted)
- return;
- atomic_fetch_add(&pipeline->rtp_packets, 1);
- if (payload.length > AUDIO_PACKET_MAX) {
- atomic_fetch_add(&pipeline->dropped_packets, 1);
- return;
- }
-
- pthread_mutex_lock(&pipeline->lock);
- if (pipeline->queue_count == AUDIO_QUEUE_CAPACITY) {
- atomic_fetch_add(&pipeline->dropped_packets, 1);
- } else {
- AudioPacket* target = &pipeline->queue[pipeline->queue_tail];
- target->length = (uint16_t)payload.length;
- memcpy(target->data, payload.data, payload.length);
- pipeline->queue_tail = (pipeline->queue_tail + 1) % AUDIO_QUEUE_CAPACITY;
- pipeline->queue_count++;
- pthread_cond_signal(&pipeline->condition);
- }
- pthread_mutex_unlock(&pipeline->lock);
-}
-
-GoAudioStats go_audio_pipeline_stats(GoAudioPipeline* pipeline) {
- GoAudioStats stats = {0};
- if (!pipeline)
- return stats;
- stats.rtp_packets = atomic_load(&pipeline->rtp_packets);
- stats.decoded_packets = atomic_load(&pipeline->decoded_packets);
- stats.dropped_packets = atomic_load(&pipeline->dropped_packets);
- stats.late_packets = atomic_load(&pipeline->late_packets);
- stats.queue_resets = atomic_load(&pipeline->queue_resets);
- pthread_mutex_lock(&pipeline->lock);
- stats.pending_packets = pipeline->queue_count;
- pthread_mutex_unlock(&pipeline->lock);
- stats.queued_milliseconds =
- SDL_GetQueuedAudioSize(pipeline->device) * 1000 / (48000 * 2 * sizeof(short));
- return stats;
-}
-
-void go_audio_pipeline_destroy(GoAudioPipeline* pipeline) {
- if (!pipeline)
- return;
- go_audio_pipeline_stop(pipeline);
- go_opus_destroy(pipeline->decoder);
- pthread_cond_destroy(&pipeline->condition);
- pthread_mutex_destroy(&pipeline->lock);
- free(pipeline);
-}
diff --git a/src/media/audio/audio_pipeline.zig b/src/media/audio/audio_pipeline.zig
new file mode 100644
index 0000000..f76fcaf
--- /dev/null
+++ b/src/media/audio/audio_pipeline.zig
@@ -0,0 +1,205 @@
+const std = @import("std");
+const rtp = @import("rtp_packet");
+
+const c = @cImport({
+ @cInclude("SDL2/SDL.h");
+ @cInclude("opus/opus.h");
+});
+
+const payload_type = 111;
+const queue_capacity = 32;
+const target_pending_packets = 2;
+const packet_capacity = 2048;
+const max_samples = 5760 * 2;
+const target_max_bytes = 48000 * 2 * 2 * 40 / 1000;
+const hard_reset_bytes = 48000 * 2 * 2 * 120 / 1000;
+
+const AudioPacket = struct {
+ length: u16 = 0,
+ data: [packet_capacity]u8 = undefined,
+};
+
+const Pipeline = struct {
+ device: c.SDL_AudioDeviceID,
+ decoder: *c.OpusDecoder,
+ queue: [queue_capacity]AudioPacket = undefined,
+ queue_head: usize = 0,
+ queue_tail: usize = 0,
+ queue_count: usize = 0,
+ mutex: std.Thread.Mutex = .{},
+ condition: std.Thread.Condition = .{},
+ thread: ?std.Thread = null,
+ stop: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ accepting_packets: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ rtp_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoded_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ dropped_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ late_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ queue_resets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+};
+
+pub const Stats = extern struct {
+ rtp_packets: c_int = 0,
+ decoded_packets: c_int = 0,
+ dropped_packets: c_int = 0,
+ late_packets: c_int = 0,
+ pending_packets: c_int = 0,
+ queue_resets: c_int = 0,
+ queued_milliseconds: c_uint = 0,
+};
+
+fn worker(pipeline: *Pipeline) void {
+ while (!pipeline.stop.load(.acquire)) {
+ pipeline.mutex.lock();
+ while (pipeline.queue_count == 0 and !pipeline.stop.load(.acquire))
+ pipeline.condition.wait(&pipeline.mutex);
+ if (pipeline.stop.load(.acquire)) {
+ pipeline.mutex.unlock();
+ break;
+ }
+ while (pipeline.queue_count > target_pending_packets) {
+ pipeline.queue_head = (pipeline.queue_head + 1) % queue_capacity;
+ pipeline.queue_count -= 1;
+ _ = pipeline.late_packets.fetchAdd(1, .monotonic);
+ }
+ const packet = pipeline.queue[pipeline.queue_head];
+ pipeline.queue_head = (pipeline.queue_head + 1) % queue_capacity;
+ pipeline.queue_count -= 1;
+ pipeline.mutex.unlock();
+
+ var pcm: [max_samples]c_short = undefined;
+ const samples_per_channel = c.opus_decode(
+ pipeline.decoder,
+ &packet.data,
+ packet.length,
+ &pcm,
+ max_samples / 2,
+ 0,
+ );
+ if (samples_per_channel <= 0) {
+ _ = pipeline.dropped_packets.fetchAdd(1, .monotonic);
+ continue;
+ }
+ const samples = samples_per_channel * 2;
+ const output_bytes: c.Uint32 = @intCast(@as(usize, @intCast(samples)) * @sizeOf(c_short));
+ while (!pipeline.stop.load(.acquire)) {
+ const queued = c.SDL_GetQueuedAudioSize(pipeline.device);
+ if (queued > hard_reset_bytes) {
+ c.SDL_ClearQueuedAudio(pipeline.device);
+ _ = pipeline.queue_resets.fetchAdd(1, .monotonic);
+ break;
+ }
+ const pending_bytes = @as(u64, queued) + @as(u64, output_bytes);
+ if (queued == 0 or pending_bytes <= target_max_bytes) break;
+ c.SDL_Delay(2);
+ }
+ if (pipeline.stop.load(.acquire)) break;
+ if (c.SDL_QueueAudio(pipeline.device, &pcm, output_bytes) == 0)
+ _ = pipeline.decoded_packets.fetchAdd(1, .monotonic)
+ else
+ _ = pipeline.dropped_packets.fetchAdd(1, .monotonic);
+ }
+}
+
+pub export fn go_audio_pipeline_create(device: c.SDL_AudioDeviceID) ?*Pipeline {
+ if (device == 0) return null;
+ var opus_error: c_int = 0;
+ const decoder = c.opus_decoder_create(48000, 2, &opus_error) orelse return null;
+ if (opus_error != c.OPUS_OK) {
+ c.opus_decoder_destroy(decoder);
+ return null;
+ }
+ errdefer c.opus_decoder_destroy(decoder);
+ const pipeline = std.heap.c_allocator.create(Pipeline) catch return null;
+ pipeline.* = .{ .device = device, .decoder = decoder };
+ return pipeline;
+}
+
+pub export fn go_audio_pipeline_start(pipeline_pointer: ?*Pipeline) c_int {
+ const pipeline = pipeline_pointer orelse return -1;
+ if (pipeline.thread != null) return -1;
+ c.SDL_ClearQueuedAudio(pipeline.device);
+ pipeline.stop.store(false, .release);
+ pipeline.accepting_packets.store(true, .release);
+ pipeline.thread = std.Thread.spawn(.{}, worker, .{pipeline}) catch {
+ pipeline.accepting_packets.store(false, .release);
+ return -1;
+ };
+ c.SDL_PauseAudioDevice(pipeline.device, 0);
+ return 0;
+}
+
+pub export fn go_audio_pipeline_stop(pipeline_pointer: ?*Pipeline) void {
+ const pipeline = pipeline_pointer orelse return;
+ pipeline.accepting_packets.store(false, .release);
+ if (pipeline.thread) |thread| {
+ pipeline.stop.store(true, .release);
+ pipeline.mutex.lock();
+ pipeline.condition.broadcast();
+ pipeline.mutex.unlock();
+ thread.join();
+ pipeline.thread = null;
+ }
+ c.SDL_PauseAudioDevice(pipeline.device, 1);
+ c.SDL_ClearQueuedAudio(pipeline.device);
+ pipeline.mutex.lock();
+ pipeline.queue_head = 0;
+ pipeline.queue_tail = 0;
+ pipeline.queue_count = 0;
+ pipeline.mutex.unlock();
+}
+
+pub export fn go_audio_pipeline_push_rtp(
+ pipeline_pointer: ?*Pipeline,
+ packet_pointer: ?[*]const u8,
+ length: usize,
+) void {
+ const pipeline = pipeline_pointer orelse return;
+ const packet = packet_pointer orelse return;
+ if (!pipeline.accepting_packets.load(.acquire)) return;
+ const parsed = rtp.parse(packet[0..length]) catch return;
+ if (parsed.header.payload_type != payload_type or parsed.payload.len == 0) return;
+ _ = pipeline.rtp_packets.fetchAdd(1, .monotonic);
+ if (parsed.payload.len > packet_capacity) {
+ _ = pipeline.dropped_packets.fetchAdd(1, .monotonic);
+ return;
+ }
+
+ pipeline.mutex.lock();
+ defer pipeline.mutex.unlock();
+ if (!pipeline.accepting_packets.load(.acquire)) return;
+ if (pipeline.queue_count == queue_capacity) {
+ _ = pipeline.dropped_packets.fetchAdd(1, .monotonic);
+ return;
+ }
+ const target = &pipeline.queue[pipeline.queue_tail];
+ target.length = @intCast(parsed.payload.len);
+ @memcpy(target.data[0..parsed.payload.len], parsed.payload);
+ pipeline.queue_tail = (pipeline.queue_tail + 1) % queue_capacity;
+ pipeline.queue_count += 1;
+ pipeline.condition.signal();
+}
+
+pub export fn go_audio_pipeline_stats(pipeline_pointer: ?*Pipeline) Stats {
+ const pipeline = pipeline_pointer orelse return .{};
+ var stats = Stats{
+ .rtp_packets = pipeline.rtp_packets.load(.monotonic),
+ .decoded_packets = pipeline.decoded_packets.load(.monotonic),
+ .dropped_packets = pipeline.dropped_packets.load(.monotonic),
+ .late_packets = pipeline.late_packets.load(.monotonic),
+ .queue_resets = pipeline.queue_resets.load(.monotonic),
+ };
+ pipeline.mutex.lock();
+ stats.pending_packets = @intCast(pipeline.queue_count);
+ pipeline.mutex.unlock();
+ stats.queued_milliseconds = c.SDL_GetQueuedAudioSize(pipeline.device) * 1000 /
+ (48000 * 2 * @sizeOf(c_short));
+ return stats;
+}
+
+pub export fn go_audio_pipeline_destroy(pipeline_pointer: ?*Pipeline) void {
+ const pipeline = pipeline_pointer orelse return;
+ go_audio_pipeline_stop(pipeline);
+ c.opus_decoder_destroy(pipeline.decoder);
+ std.heap.c_allocator.destroy(pipeline);
+}
diff --git a/src/media/audio/opus_adapter.c b/src/media/audio/opus_adapter.c
deleted file mode 100644
index 9692fe7..0000000
--- a/src/media/audio/opus_adapter.c
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Minimal declarations for the libopus ABI provided by the target firmware. */
-#include
-#include
-
-#include "opus_adapter.h"
-
-typedef int32_t opus_int32;
-typedef int16_t opus_int16;
-
-typedef struct OpusDecoder OpusDecoder;
-
-extern OpusDecoder* opus_decoder_create(opus_int32 Fs, int channels, int* error);
-extern int opus_decode(OpusDecoder* st, const unsigned char* data, opus_int32 len, opus_int16* pcm,
- int frame_size, int decode_fec);
-extern void opus_decoder_destroy(OpusDecoder* st);
-
-struct GoOpus {
- OpusDecoder* dec;
- int channels;
-};
-
-GoOpus* go_opus_create(int sample_rate, int channels) {
- int err = 0;
- OpusDecoder* d = opus_decoder_create(sample_rate, channels, &err);
- if (err != 0 || !d)
- return NULL;
- GoOpus* go = (GoOpus*)malloc(sizeof(GoOpus));
- if (!go) {
- opus_decoder_destroy(d);
- return NULL;
- }
- go->dec = d;
- go->channels = channels;
- return go;
-}
-
-/* Returns number of samples decoded (per channel * channels), or negative on error. */
-int go_opus_decode(GoOpus* go, const void* packet, int len, short* pcm, int max_samples) {
- int frame = opus_decode(go->dec, (const unsigned char*)packet, len, (opus_int16*)pcm,
- max_samples / go->channels, 0);
- if (frame < 0)
- return frame;
- return frame * go->channels;
-}
-
-void go_opus_destroy(GoOpus* go) {
- if (go) {
- opus_decoder_destroy(go->dec);
- free(go);
- }
-}
diff --git a/src/media/audio/opus_adapter.h b/src/media/audio/opus_adapter.h
deleted file mode 100644
index 1b0203f..0000000
--- a/src/media/audio/opus_adapter.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#ifndef GREENOVERCAST_OPUS_ADAPTER_H
-#define GREENOVERCAST_OPUS_ADAPTER_H
-
-typedef struct GoOpus GoOpus;
-
-GoOpus* go_opus_create(int sample_rate, int channels);
-int go_opus_decode(GoOpus* go, const void* packet, int len, short* pcm, int max_samples);
-void go_opus_destroy(GoOpus* go);
-
-#endif
diff --git a/src/media/rtp/packet.h b/src/media/rtp/packet.h
deleted file mode 100644
index 81991cc..0000000
--- a/src/media/rtp/packet.h
+++ /dev/null
@@ -1,28 +0,0 @@
-#ifndef GREENOVERCAST_RTP_PACKET_H
-#define GREENOVERCAST_RTP_PACKET_H
-
-#include
-#include
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct {
- uint32_t accepted;
- uint32_t marker;
- uint16_t sequence;
- uint16_t reserved;
- uint32_t timestamp;
- const uint8_t* data;
- size_t length;
-} GoRtpPayload;
-
-GoRtpPayload go_rtp_parse_payload(const uint8_t* packet, size_t length,
- uint8_t expected_payload_type);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/src/media/rtp/packet.zig b/src/media/rtp/packet.zig
index 55590a3..2863dfb 100644
--- a/src/media/rtp/packet.zig
+++ b/src/media/rtp/packet.zig
@@ -13,16 +13,6 @@ pub const Parsed = struct {
payload: []const u8,
};
-pub const Payload = extern struct {
- accepted: u32 = 0,
- marker: u32 = 0,
- sequence: u16 = 0,
- reserved: u16 = 0,
- timestamp: u32 = 0,
- data: ?[*]const u8 = null,
- length: usize = 0,
-};
-
pub const ParseError = error{
TooShort,
BadVersion,
@@ -68,25 +58,6 @@ pub fn parse(data: []const u8) ParseError!Parsed {
};
}
-pub export fn go_rtp_parse_payload(
- packet: ?[*]const u8,
- length: usize,
- expected_payload_type: u8,
-) Payload {
- if (expected_payload_type > 127) return .{};
- const bytes = packet orelse return .{};
- const parsed = parse(bytes[0..length]) catch return .{};
- if (parsed.header.payload_type != expected_payload_type or parsed.payload.len == 0) return .{};
- return .{
- .accepted = 1,
- .marker = @intFromBool(parsed.header.marker),
- .sequence = parsed.header.sequence,
- .timestamp = parsed.header.timestamp,
- .data = parsed.payload.ptr,
- .length = parsed.payload.len,
- };
-}
-
test "parse minimal header" {
const data = [_]u8{
0x80, 0xe6, 0x00, 0x01,
@@ -144,20 +115,3 @@ test "reject bad version and padding" {
zero_padding[0] = 0xa0;
try std.testing.expectError(error.BadPadding, parse(&zero_padding));
}
-
-test "C payload adapter validates payload type" {
- const data = [_]u8{
- 0x80, 0xe6, 0x00, 0x01,
- 0x00, 0x00, 0x0e, 0x10,
- 0x12, 0x34, 0x56, 0x78,
- 0xaa, 0xbb,
- };
-
- const accepted = go_rtp_parse_payload(&data, data.len, 102);
- const rejected = go_rtp_parse_payload(&data, data.len, 111);
-
- try std.testing.expectEqual(@as(u32, 1), accepted.accepted);
- try std.testing.expectEqual(@as(u32, 1), accepted.marker);
- try std.testing.expectEqualSlices(u8, &.{ 0xaa, 0xbb }, accepted.data.?[0..accepted.length]);
- try std.testing.expectEqual(@as(u32, 0), rejected.accepted);
-}
diff --git a/src/media/video/cedar_loader.c b/src/media/video/cedar_loader.c
deleted file mode 100644
index 4496982..0000000
--- a/src/media/video/cedar_loader.c
+++ /dev/null
@@ -1,88 +0,0 @@
-#include "cedar_loader.h"
-
-#include
-#include
-#include
-#include
-
-typedef GoCedarDecoder* (*CreateDecoder)(int width, int height);
-typedef int (*FeedDecoder)(GoCedarDecoder* decoder, const uint8_t* data, size_t length,
- GoCedarFrame* frame);
-typedef const char* (*DecoderError)(const GoCedarDecoder* decoder);
-typedef void (*DestroyDecoder)(GoCedarDecoder* decoder);
-
-struct GoCedarLibrary {
- void* handle;
- GoCedarDecoder* decoder;
- FeedDecoder feed;
- DecoderError decoder_error;
- DestroyDecoder destroy;
- char error[256];
-};
-
-static void set_loader_error(GoCedarLibrary* library, const char* message) {
- snprintf(library->error, sizeof(library->error), "%s", message ? message : "unknown error");
-}
-
-GoCedarLibrary* go_cedar_library_open(int width, int height) {
- GoCedarLibrary* library = calloc(1, sizeof(*library));
- if (!library)
- return NULL;
-
- const char* path = getenv("GREENOVERCAST_CEDAR_LIBRARY");
- if (!path || !path[0])
- path = "libgreenovercast-cedar.so";
- library->handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
- if (!library->handle) {
- set_loader_error(library, dlerror());
- return library;
- }
-
- dlerror();
- CreateDecoder create = (CreateDecoder)dlsym(library->handle, "go_cedar_v1_create");
- library->feed = (FeedDecoder)dlsym(library->handle, "go_cedar_v1_feed");
- library->decoder_error = (DecoderError)dlsym(library->handle, "go_cedar_v1_last_error");
- library->destroy = (DestroyDecoder)dlsym(library->handle, "go_cedar_v1_destroy");
- const char* symbol_error = dlerror();
- if (symbol_error || !create || !library->feed || !library->decoder_error || !library->destroy) {
- set_loader_error(library, symbol_error ? symbol_error : "incomplete Cedar decoder ABI");
- dlclose(library->handle);
- library->handle = NULL;
- return library;
- }
-
- library->decoder = create(width, height);
- if (!library->decoder) {
- set_loader_error(library, "Cedar decoder initialization failed");
- return library;
- }
- return library;
-}
-
-int go_cedar_library_ready(const GoCedarLibrary* library) {
- return library && library->decoder && library->feed && library->destroy;
-}
-
-int go_cedar_library_feed(GoCedarLibrary* library, const uint8_t* annex_b, size_t length,
- GoCedarFrame* frame) {
- if (!library || !library->decoder)
- return -1;
- int result = library->feed(library->decoder, annex_b, length, frame);
- if (result < 0)
- set_loader_error(library, library->decoder_error(library->decoder));
- return result;
-}
-
-const char* go_cedar_library_error(const GoCedarLibrary* library) {
- return library && library->error[0] ? library->error : "Cedar decoder unavailable";
-}
-
-void go_cedar_library_close(GoCedarLibrary* 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/cedar_loader.zig b/src/media/video/cedar_loader.zig
new file mode 100644
index 0000000..70a1a63
--- /dev/null
+++ b/src/media/video/cedar_loader.zig
@@ -0,0 +1,112 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("cedar_decoder.h");
+ @cInclude("dlfcn.h");
+});
+
+const CreateDecoder = *const fn (c_int, c_int) callconv(.c) ?*c.GoCedarDecoder;
+const FeedDecoder = *const fn (*c.GoCedarDecoder, [*]const u8, usize, *c.GoCedarFrame) callconv(.c) c_int;
+const DecoderError = *const fn (*const c.GoCedarDecoder) callconv(.c) [*:0]const u8;
+const DestroyDecoder = *const fn (*c.GoCedarDecoder) callconv(.c) void;
+
+const Library = struct {
+ handle: ?*anyopaque = null,
+ decoder: ?*c.GoCedarDecoder = null,
+ feed: ?FeedDecoder = null,
+ decoder_error: ?DecoderError = null,
+ destroy: ?DestroyDecoder = null,
+ error_message: [256]u8 = [_]u8{0} ** 256,
+};
+
+fn copyZ(destination: []u8, message: []const u8) void {
+ @memset(destination, 0);
+ const length = @min(destination.len - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+}
+
+fn setError(library: *Library, message: ?[*:0]const u8) void {
+ copyZ(&library.error_message, if (message) |value| std.mem.span(value) else "unknown error");
+}
+
+fn loadSymbol(library: *Library, comptime T: type, name: [*:0]const u8) ?T {
+ _ = c.dlerror();
+ const symbol = c.dlsym(library.handle, name);
+ const load_error = c.dlerror();
+ if (load_error != null or symbol == null) {
+ setError(
+ library,
+ if (load_error != null) @ptrCast(load_error) else "incomplete Cedar decoder ABI",
+ );
+ return null;
+ }
+ return @ptrCast(@alignCast(symbol));
+}
+
+fn ready(library: ?*const Library) bool {
+ const value = library orelse return false;
+ return value.decoder != null and value.feed != null and value.destroy != null;
+}
+
+pub export fn go_cedar_library_open(width: c_int, height: c_int) ?*Library {
+ const library = std.heap.c_allocator.create(Library) catch return null;
+ library.* = .{};
+ const path = std.posix.getenv("GREENOVERCAST_CEDAR_LIBRARY") orelse
+ "libgreenovercast-cedar.so";
+ library.handle = c.dlopen(path.ptr, c.RTLD_NOW | c.RTLD_LOCAL);
+ if (library.handle == null) {
+ const load_error = c.dlerror();
+ setError(library, if (load_error != null) @ptrCast(load_error) else null);
+ return library;
+ }
+
+ const create = loadSymbol(library, CreateDecoder, "go_cedar_v1_create");
+ library.feed = loadSymbol(library, FeedDecoder, "go_cedar_v1_feed");
+ library.decoder_error = loadSymbol(library, DecoderError, "go_cedar_v1_last_error");
+ library.destroy = loadSymbol(library, DestroyDecoder, "go_cedar_v1_destroy");
+ if (create == null or library.feed == null or library.decoder_error == null or
+ library.destroy == null)
+ {
+ _ = c.dlclose(library.handle);
+ library.handle = null;
+ return library;
+ }
+ library.decoder = create.?(width, height);
+ if (library.decoder == null) copyZ(&library.error_message, "Cedar decoder initialization failed");
+ return library;
+}
+
+pub export fn go_cedar_library_ready(library: ?*const Library) c_int {
+ return @intFromBool(ready(library));
+}
+
+pub export fn go_cedar_library_feed(
+ library_pointer: ?*Library,
+ annex_b: ?[*]const u8,
+ length: usize,
+ frame: ?*c.GoCedarFrame,
+) c_int {
+ const library = library_pointer orelse return -1;
+ const data = annex_b orelse return -1;
+ const output = frame orelse return -1;
+ if (!ready(library)) return -1;
+ const result = library.feed.?(library.decoder.?, data, length, output);
+ if (result < 0) setError(library, library.decoder_error.?(@ptrCast(library.decoder.?)));
+ return result;
+}
+
+pub export fn go_cedar_library_error(library: ?*const Library) [*:0]const u8 {
+ if (library) |value| {
+ if (value.error_message[0] != 0) return @ptrCast(&value.error_message);
+ }
+ return "Cedar decoder unavailable";
+}
+
+pub export fn go_cedar_library_close(library_pointer: ?*Library) void {
+ const library = library_pointer orelse return;
+ if (library.decoder) |decoder| {
+ if (library.destroy) |destroy| destroy(decoder);
+ }
+ if (library.handle != null) _ = c.dlclose(library.handle);
+ std.heap.c_allocator.destroy(library);
+}
diff --git a/src/media/video/mpp_loader.c b/src/media/video/mpp_loader.c
deleted file mode 100644
index e57c743..0000000
--- a/src/media/video/mpp_loader.c
+++ /dev/null
@@ -1,155 +0,0 @@
-#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.zig b/src/media/video/mpp_loader.zig
new file mode 100644
index 0000000..11dbef1
--- /dev/null
+++ b/src/media/video/mpp_loader.zig
@@ -0,0 +1,184 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("dlfcn.h");
+ @cInclude("mpp_decoder.h");
+});
+
+const AbiVersion = *const fn () callconv(.c) u32;
+const CreateDecoder = *const fn (c_int, c_int) callconv(.c) ?*c.GoMppDecoder;
+const SubmitDecoder = *const fn (*c.GoMppDecoder, [*]const u8, usize) callconv(.c) c_int;
+const ReceiveDecoder = *const fn (*c.GoMppDecoder, *c.GoMppFrame) callconv(.c) c_int;
+const ReleaseFrame = *const fn (*c.GoMppDecoder, *c.GoMppFrame) callconv(.c) void;
+const ResetDecoder = *const fn (*c.GoMppDecoder) callconv(.c) c_int;
+const DecoderError = *const fn (?*c.GoMppDecoder) callconv(.c) [*:0]const u8;
+const DestroyDecoder = *const fn (*c.GoMppDecoder) callconv(.c) void;
+
+const Library = struct {
+ handle: ?*anyopaque = null,
+ decoder: ?*c.GoMppDecoder = null,
+ submit: ?SubmitDecoder = null,
+ receive: ?ReceiveDecoder = null,
+ release_frame: ?ReleaseFrame = null,
+ reset: ?ResetDecoder = null,
+ decoder_error: ?DecoderError = null,
+ destroy: ?DestroyDecoder = null,
+ path: [512]u8 = [_]u8{0} ** 512,
+ error_message: [256]u8 = [_]u8{0} ** 256,
+};
+
+fn copyZ(destination: []u8, message: []const u8) void {
+ @memset(destination, 0);
+ const length = @min(destination.len - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+}
+
+fn setError(library: *Library, message: ?[*:0]const u8) void {
+ copyZ(&library.error_message, if (message) |value| std.mem.span(value) else "unknown error");
+}
+
+fn loadSymbol(library: *Library, comptime T: type, name: [*:0]const u8) ?T {
+ _ = c.dlerror();
+ const symbol = c.dlsym(library.handle, name);
+ const load_error = c.dlerror();
+ if (load_error != null or symbol == null) {
+ setError(
+ library,
+ if (load_error != null) @ptrCast(load_error) else "MPP plugin symbol is missing",
+ );
+ return null;
+ }
+ return @ptrCast(@alignCast(symbol));
+}
+
+fn ready(library: ?*const Library) bool {
+ const value = library orelse return false;
+ return value.decoder != null and value.submit != null and value.receive != null and
+ value.release_frame != null and value.reset != null and value.decoder_error != null and
+ value.destroy != null;
+}
+
+pub export fn go_mpp_library_open(max_width: c_int, max_height: c_int) ?*Library {
+ const library = std.heap.c_allocator.create(Library) catch return null;
+ library.* = .{};
+
+ const path = std.posix.getenv("GREENOVERCAST_MPP_LIBRARY") orelse "libgreenovercast-mpp.so";
+ if (path.len >= library.path.len) {
+ copyZ(&library.error_message, "MPP plugin path is too long");
+ return library;
+ }
+ copyZ(&library.path, path);
+
+ var flags: c_int = c.RTLD_NOW | c.RTLD_LOCAL;
+ // Older MPP builds can run process-lifetime cleanup after decoder teardown.
+ if (@hasDecl(c, "RTLD_NODELETE")) flags |= c.RTLD_NODELETE;
+ library.handle = c.dlopen(@ptrCast(&library.path), flags);
+ if (library.handle == null) {
+ const load_error = c.dlerror();
+ setError(library, if (load_error != null) @ptrCast(load_error) else null);
+ return library;
+ }
+
+ const abi_version = loadSymbol(library, AbiVersion, "go_mpp_decoder_abi_version") orelse {
+ _ = c.dlclose(library.handle);
+ library.handle = null;
+ return library;
+ };
+ const version = abi_version();
+ if (version != c.GO_MPP_DECODER_ABI_VERSION) {
+ _ = std.fmt.bufPrintZ(
+ &library.error_message,
+ "MPP plugin ABI {d} is incompatible with required ABI {d}",
+ .{ version, c.GO_MPP_DECODER_ABI_VERSION },
+ ) catch copyZ(&library.error_message, "MPP plugin ABI is incompatible");
+ _ = c.dlclose(library.handle);
+ library.handle = null;
+ return library;
+ }
+
+ const create = loadSymbol(library, CreateDecoder, "go_mpp_decoder_create");
+ library.submit = loadSymbol(library, SubmitDecoder, "go_mpp_decoder_submit");
+ library.receive = loadSymbol(library, ReceiveDecoder, "go_mpp_decoder_receive");
+ library.release_frame = loadSymbol(library, ReleaseFrame, "go_mpp_decoder_release_frame");
+ library.reset = loadSymbol(library, ResetDecoder, "go_mpp_decoder_reset");
+ library.decoder_error = loadSymbol(library, DecoderError, "go_mpp_decoder_last_error");
+ library.destroy = loadSymbol(library, DestroyDecoder, "go_mpp_decoder_destroy");
+ if (create == null or library.submit == null or library.receive == null or
+ library.release_frame == null or library.reset == null or library.decoder_error == null or
+ library.destroy == null)
+ {
+ _ = c.dlclose(library.handle);
+ library.handle = null;
+ return library;
+ }
+
+ library.decoder = create.?(max_width, max_height);
+ if (library.decoder == null) setError(library, library.decoder_error.?(null));
+ return library;
+}
+
+pub export fn go_mpp_library_ready(library: ?*const Library) c_int {
+ return @intFromBool(ready(library));
+}
+
+pub export fn go_mpp_library_submit(
+ library_pointer: ?*Library,
+ data: ?[*]const u8,
+ length: usize,
+) c_int {
+ const library = library_pointer orelse return -1;
+ const bytes = data orelse return -1;
+ if (!ready(library)) return -1;
+ const result = library.submit.?(library.decoder.?, bytes, length);
+ if (result < 0) setError(library, library.decoder_error.?(library.decoder));
+ return result;
+}
+
+pub export fn go_mpp_library_receive(library_pointer: ?*Library, frame: ?*c.GoMppFrame) c_int {
+ const library = library_pointer orelse return -1;
+ const output = frame orelse return -1;
+ if (!ready(library)) return -1;
+ const result = library.receive.?(library.decoder.?, output);
+ if (result < 0) setError(library, library.decoder_error.?(library.decoder));
+ return result;
+}
+
+pub export fn go_mpp_library_release_frame(
+ library_pointer: ?*Library,
+ frame: ?*c.GoMppFrame,
+) void {
+ const library = library_pointer orelse return;
+ const output = frame orelse return;
+ if (ready(library)) library.release_frame.?(library.decoder.?, output);
+}
+
+pub export fn go_mpp_library_reset(library_pointer: ?*Library) c_int {
+ const library = library_pointer orelse return -1;
+ if (!ready(library)) return -1;
+ const result = library.reset.?(library.decoder.?);
+ if (result < 0) setError(library, library.decoder_error.?(library.decoder));
+ return result;
+}
+
+pub export fn go_mpp_library_error(library: ?*const Library) [*:0]const u8 {
+ if (library) |value| {
+ if (value.error_message[0] != 0) return @ptrCast(&value.error_message);
+ }
+ return "MPP decoder unavailable";
+}
+
+pub export fn go_mpp_library_path(library: ?*const Library) [*:0]const u8 {
+ if (library) |value| {
+ if (value.path[0] != 0) return @ptrCast(&value.path);
+ }
+ return "libgreenovercast-mpp.so";
+}
+
+pub export fn go_mpp_library_close(library_pointer: ?*Library) void {
+ const library = library_pointer orelse return;
+ if (library.decoder) |decoder| {
+ if (library.destroy) |destroy| destroy(decoder);
+ }
+ if (library.handle != null) _ = c.dlclose(library.handle);
+ std.heap.c_allocator.destroy(library);
+}
diff --git a/src/media/video/video_decoder.h b/src/media/video/video_decoder.h
index c135737..17bff2f 100644
--- a/src/media/video/video_decoder.h
+++ b/src/media/video/video_decoder.h
@@ -15,6 +15,7 @@ typedef enum {
GO_VIDEO_DECODER_BACKEND_SOFTWARE,
GO_VIDEO_DECODER_BACKEND_CEDAR,
GO_VIDEO_DECODER_BACKEND_MPP,
+ GO_VIDEO_DECODER_BACKEND_V4L2_REQUEST,
} GoVideoDecoderBackend;
typedef enum {
@@ -22,6 +23,7 @@ typedef enum {
GO_VIDEO_DECODER_PREFERENCE_MPP,
GO_VIDEO_DECODER_PREFERENCE_CEDAR,
GO_VIDEO_DECODER_PREFERENCE_SOFTWARE,
+ GO_VIDEO_DECODER_PREFERENCE_V4L2_REQUEST,
} GoVideoDecoderPreference;
typedef enum {
@@ -117,5 +119,7 @@ GoVideoDecoder* go_video_decoder_cedar_create(int max_width, int max_height, cha
size_t error_capacity);
GoVideoDecoder* go_video_decoder_mpp_create(int max_width, int max_height, char* error,
size_t error_capacity);
+GoVideoDecoder* go_video_decoder_v4l2_request_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
index f5b5d81..3250aad 100644
--- a/src/media/video/video_decoder.zig
+++ b/src/media/video/video_decoder.zig
@@ -28,6 +28,8 @@ pub export fn go_video_decoder_preference_parse(
c.GO_VIDEO_DECODER_PREFERENCE_CEDAR
else if (std.mem.eql(u8, name, "software"))
c.GO_VIDEO_DECODER_PREFERENCE_SOFTWARE
+ else if (std.mem.eql(u8, name, "v4l2") or std.mem.eql(u8, name, "v4l2-request"))
+ c.GO_VIDEO_DECODER_PREFERENCE_V4L2_REQUEST
else
return -1;
return 0;
@@ -60,7 +62,7 @@ pub export fn go_video_decoder_candidate_order(
output: ?[*]c.GoVideoDecoderBackend,
capacity: usize,
) usize {
- var candidates: [3]c.GoVideoDecoderBackend = undefined;
+ var candidates: [4]c.GoVideoDecoderBackend = undefined;
var count: usize = 0;
if (preference == c.GO_VIDEO_DECODER_PREFERENCE_MPP) {
@@ -72,12 +74,19 @@ pub export fn go_video_decoder_candidate_order(
} else if (preference == c.GO_VIDEO_DECODER_PREFERENCE_SOFTWARE) {
candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
count += 1;
+ } else if (preference == c.GO_VIDEO_DECODER_PREFERENCE_V4L2_REQUEST) {
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_V4L2_REQUEST;
+ 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_V4L2_REQUEST;
+ 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_V4L2_REQUEST;
+ count += 1;
candidates[count] = c.GO_VIDEO_DECODER_BACKEND_CEDAR;
count += 1;
candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
@@ -88,6 +97,8 @@ pub export fn go_video_decoder_candidate_order(
} else {
candidates[count] = c.GO_VIDEO_DECODER_BACKEND_MPP;
count += 1;
+ candidates[count] = c.GO_VIDEO_DECODER_BACKEND_V4L2_REQUEST;
+ count += 1;
candidates[count] = c.GO_VIDEO_DECODER_BACKEND_CEDAR;
count += 1;
candidates[count] = c.GO_VIDEO_DECODER_BACKEND_SOFTWARE;
diff --git a/src/media/video/video_decoder_cedar.c b/src/media/video/video_decoder_cedar.c
deleted file mode 100644
index 37d3221..0000000
--- a/src/media/video/video_decoder_cedar.c
+++ /dev/null
@@ -1,147 +0,0 @@
-#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_cedar.zig b/src/media/video/video_decoder_cedar.zig
new file mode 100644
index 0000000..ac5f86c
--- /dev/null
+++ b/src/media/video/video_decoder_cedar.zig
@@ -0,0 +1,177 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("cedar_loader.h");
+ @cInclude("video_decoder.h");
+});
+
+const Decoder = struct {
+ base: c.GoVideoDecoder = .{ .ops = null },
+ library: ?*c.GoCedarLibrary = null,
+ pending_frame: c.GoCedarFrame = std.mem.zeroes(c.GoCedarFrame),
+ max_width: c_int,
+ max_height: c_int,
+ frame_pending: bool = false,
+ frame_outstanding: bool = false,
+ last_width: c_int = 0,
+ last_height: c_int = 0,
+ last_y_stride: c_int = 0,
+ last_uv_stride: c_int = 0,
+ error_message: [256]u8 = [_]u8{0} ** 256,
+};
+
+fn fromBase(base: *c.GoVideoDecoder) *Decoder {
+ return @fieldParentPtr("base", base);
+}
+
+fn fromConstBase(base: *const c.GoVideoDecoder) *const Decoder {
+ return @fieldParentPtr("base", base);
+}
+
+fn copyZ(destination: []u8, message: []const u8) void {
+ @memset(destination, 0);
+ const length = @min(destination.len - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+}
+
+fn writeError(destination: [*c]u8, capacity: usize, message: []const u8) void {
+ if (destination == null or capacity == 0) return;
+ const length = @min(capacity - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+ destination[length] = 0;
+}
+
+fn openCedar(decoder: *Decoder) c_int {
+ decoder.library = c.go_cedar_library_open(decoder.max_width, decoder.max_height);
+ if (c.go_cedar_library_ready(decoder.library) == 0) {
+ copyZ(&decoder.error_message, std.mem.span(c.go_cedar_library_error(decoder.library)));
+ c.go_cedar_library_close(decoder.library);
+ decoder.library = null;
+ return -1;
+ }
+ decoder.error_message[0] = 0;
+ return 0;
+}
+
+fn cedarSubmit(
+ base: ?*c.GoVideoDecoder,
+ data: [*c]const u8,
+ length: usize,
+) callconv(.c) c.GoVideoDecoderResult {
+ const decoder = fromBase(base orelse return c.GO_VIDEO_DECODER_RESULT_FATAL);
+ if (decoder.frame_pending or decoder.frame_outstanding)
+ return c.GO_VIDEO_DECODER_RESULT_AGAIN;
+ const result = c.go_cedar_library_feed(decoder.library, data, length, &decoder.pending_frame);
+ if (result < 0) {
+ copyZ(&decoder.error_message, std.mem.span(c.go_cedar_library_error(decoder.library)));
+ return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ decoder.frame_pending = result > 0;
+ return c.GO_VIDEO_DECODER_RESULT_OK;
+}
+
+fn cedarReceive(
+ base: ?*c.GoVideoDecoder,
+ output_pointer: ?*c.GoDecodedVideoFrame,
+) callconv(.c) c.GoVideoDecoderResult {
+ const decoder = fromBase(base orelse return c.GO_VIDEO_DECODER_RESULT_FATAL);
+ const output = output_pointer orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ if (!decoder.frame_pending) return c.GO_VIDEO_DECODER_RESULT_AGAIN;
+ const frame = &decoder.pending_frame;
+ output.format = c.GO_VIDEO_PIXEL_FORMAT_NV12;
+ output.color_range = c.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 = @intFromBool(
+ decoder.last_width != output.width or decoder.last_height != output.height or
+ decoder.last_y_stride != output.strides[0] or
+ 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 = false;
+ decoder.frame_outstanding = true;
+ return c.GO_VIDEO_DECODER_RESULT_OK;
+}
+
+fn cedarRelease(
+ base: ?*c.GoVideoDecoder,
+ frame_pointer: ?*c.GoDecodedVideoFrame,
+) callconv(.c) void {
+ const decoder = fromBase(base orelse return);
+ const frame = frame_pointer orelse return;
+ if (frame.backend_frame == @as(?*anyopaque, @ptrCast(decoder)))
+ decoder.frame_outstanding = false;
+}
+
+fn cedarReset(base: ?*c.GoVideoDecoder) callconv(.c) c_int {
+ const decoder = fromBase(base orelse return -1);
+ if (decoder.frame_outstanding) {
+ copyZ(&decoder.error_message, "Cedar reset refused with a frame outstanding");
+ return -1;
+ }
+ c.go_cedar_library_close(decoder.library);
+ decoder.library = null;
+ decoder.frame_pending = false;
+ decoder.last_width = 0;
+ decoder.last_height = 0;
+ decoder.last_y_stride = 0;
+ decoder.last_uv_stride = 0;
+ decoder.pending_frame = std.mem.zeroes(c.GoCedarFrame);
+ return openCedar(decoder);
+}
+
+fn cedarLastError(base: ?*const c.GoVideoDecoder) callconv(.c) [*c]const u8 {
+ const decoder = fromConstBase(base orelse return "Cedar H.264 decoder failure");
+ return if (decoder.error_message[0] != 0)
+ @ptrCast(&decoder.error_message)
+ else
+ "Cedar H.264 decoder failure";
+}
+
+fn cedarDestroy(base: ?*c.GoVideoDecoder) callconv(.c) void {
+ const decoder = fromBase(base orelse return);
+ c.go_cedar_library_close(decoder.library);
+ std.heap.c_allocator.destroy(decoder);
+}
+
+const ops = c.GoVideoDecoderOps{
+ .name = "cedar-h616",
+ .backend = c.GO_VIDEO_DECODER_BACKEND_CEDAR,
+ .submit_access_unit = cedarSubmit,
+ .receive_frame = cedarReceive,
+ .release_frame = cedarRelease,
+ .reset = cedarReset,
+ .last_error = cedarLastError,
+ .destroy = cedarDestroy,
+};
+
+pub export fn go_video_decoder_cedar_create(
+ max_width: c_int,
+ max_height: c_int,
+ error_buffer: [*c]u8,
+ error_capacity: usize,
+) ?*c.GoVideoDecoder {
+ if (max_width <= 0 or max_height <= 0) {
+ writeError(error_buffer, error_capacity, "invalid Cedar decoder dimensions");
+ return null;
+ }
+ const decoder = std.heap.c_allocator.create(Decoder) catch return null;
+ decoder.* = .{ .max_width = max_width, .max_height = max_height };
+ c.go_video_decoder_initialize(&decoder.base, &ops);
+ if (openCedar(decoder) != 0) {
+ writeError(error_buffer, error_capacity, std.mem.sliceTo(&decoder.error_message, 0));
+ cedarDestroy(&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
deleted file mode 100644
index 573f7b8..0000000
--- a/src/media/video/video_decoder_mpp.c
+++ /dev/null
@@ -1,141 +0,0 @@
-#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_mpp.zig b/src/media/video/video_decoder_mpp.zig
new file mode 100644
index 0000000..f14b00b
--- /dev/null
+++ b/src/media/video/video_decoder_mpp.zig
@@ -0,0 +1,181 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("mpp_loader.h");
+ @cInclude("video_decoder.h");
+});
+
+const Decoder = struct {
+ base: c.GoVideoDecoder = .{ .ops = null },
+ library: *c.GoMppLibrary,
+ active_frame: c.GoMppFrame = std.mem.zeroes(c.GoMppFrame),
+ frame_outstanding: bool = false,
+ last_width: c_int = 0,
+ last_height: c_int = 0,
+ last_y_stride: c_int = 0,
+ last_uv_stride: c_int = 0,
+ error_message: [256]u8 = [_]u8{0} ** 256,
+};
+
+fn fromBase(base: *c.GoVideoDecoder) *Decoder {
+ return @fieldParentPtr("base", base);
+}
+
+fn fromConstBase(base: *const c.GoVideoDecoder) *const Decoder {
+ return @fieldParentPtr("base", base);
+}
+
+fn copyZ(destination: []u8, message: []const u8) void {
+ @memset(destination, 0);
+ const length = @min(destination.len - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+}
+
+fn copyLibraryError(decoder: *Decoder) void {
+ copyZ(&decoder.error_message, std.mem.span(c.go_mpp_library_error(decoder.library)));
+}
+
+fn writeError(destination: [*c]u8, capacity: usize, message: []const u8) void {
+ if (destination == null or capacity == 0) return;
+ const length = @min(capacity - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+ destination[length] = 0;
+}
+
+fn mppSubmit(
+ base: ?*c.GoVideoDecoder,
+ data: [*c]const u8,
+ length: usize,
+) callconv(.c) c.GoVideoDecoderResult {
+ const decoder = fromBase(base orelse return c.GO_VIDEO_DECODER_RESULT_FATAL);
+ const result = c.go_mpp_library_submit(decoder.library, data, length);
+ if (result > 0) return c.GO_VIDEO_DECODER_RESULT_OK;
+ if (result == 0) return c.GO_VIDEO_DECODER_RESULT_AGAIN;
+ copyLibraryError(decoder);
+ return c.GO_VIDEO_DECODER_RESULT_FATAL;
+}
+
+fn mppReceive(
+ base: ?*c.GoVideoDecoder,
+ output_pointer: ?*c.GoDecodedVideoFrame,
+) callconv(.c) c.GoVideoDecoderResult {
+ const decoder = fromBase(base orelse return c.GO_VIDEO_DECODER_RESULT_FATAL);
+ const output = output_pointer orelse return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ if (decoder.frame_outstanding) {
+ copyZ(&decoder.error_message, "MPP frame was not released");
+ return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ const result = c.go_mpp_library_receive(decoder.library, &decoder.active_frame);
+ if (result == 0) return c.GO_VIDEO_DECODER_RESULT_AGAIN;
+ if (result < 0) {
+ copyLibraryError(decoder);
+ return c.GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+
+ output.format = c.GO_VIDEO_PIXEL_FORMAT_NV12;
+ output.color_range = c.GO_VIDEO_COLOR_RANGE_LIMITED;
+ output.width = decoder.active_frame.width;
+ output.height = decoder.active_frame.height;
+ output.coded_width = output.width;
+ output.coded_height = output.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 = @intFromBool(
+ decoder.last_width != output.width or decoder.last_height != output.height or
+ decoder.last_y_stride != output.strides[0] or
+ 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 = true;
+ return c.GO_VIDEO_DECODER_RESULT_OK;
+}
+
+fn mppRelease(
+ base: ?*c.GoVideoDecoder,
+ frame_pointer: ?*c.GoDecodedVideoFrame,
+) callconv(.c) void {
+ const decoder = fromBase(base orelse return);
+ const frame = frame_pointer orelse return;
+ if (decoder.frame_outstanding and
+ frame.backend_frame == @as(?*anyopaque, @ptrCast(&decoder.active_frame)))
+ {
+ c.go_mpp_library_release_frame(decoder.library, &decoder.active_frame);
+ decoder.active_frame = std.mem.zeroes(c.GoMppFrame);
+ decoder.frame_outstanding = false;
+ }
+}
+
+fn mppReset(base: ?*c.GoVideoDecoder) callconv(.c) c_int {
+ const decoder = fromBase(base orelse return -1);
+ if (decoder.frame_outstanding) {
+ copyZ(&decoder.error_message, "MPP reset refused with a frame outstanding");
+ return -1;
+ }
+ const result = c.go_mpp_library_reset(decoder.library);
+ if (result < 0) {
+ copyLibraryError(decoder);
+ } else {
+ decoder.last_width = 0;
+ decoder.last_height = 0;
+ decoder.last_y_stride = 0;
+ decoder.last_uv_stride = 0;
+ decoder.error_message[0] = 0;
+ }
+ return result;
+}
+
+fn mppLastError(base: ?*const c.GoVideoDecoder) callconv(.c) [*c]const u8 {
+ const decoder = fromConstBase(base orelse return "MPP H.264 decoder failure");
+ return if (decoder.error_message[0] != 0)
+ @ptrCast(&decoder.error_message)
+ else
+ "MPP H.264 decoder failure";
+}
+
+fn mppDestroy(base: ?*c.GoVideoDecoder) callconv(.c) void {
+ const decoder = fromBase(base orelse return);
+ if (decoder.frame_outstanding)
+ c.go_mpp_library_release_frame(decoder.library, &decoder.active_frame);
+ c.go_mpp_library_close(decoder.library);
+ std.heap.c_allocator.destroy(decoder);
+}
+
+const ops = c.GoVideoDecoderOps{
+ .name = "rockchip-mpp",
+ .backend = c.GO_VIDEO_DECODER_BACKEND_MPP,
+ .submit_access_unit = mppSubmit,
+ .receive_frame = mppReceive,
+ .release_frame = mppRelease,
+ .reset = mppReset,
+ .last_error = mppLastError,
+ .destroy = mppDestroy,
+};
+
+pub export fn go_video_decoder_mpp_create(
+ max_width: c_int,
+ max_height: c_int,
+ error_buffer: [*c]u8,
+ error_capacity: usize,
+) ?*c.GoVideoDecoder {
+ const decoder = std.heap.c_allocator.create(Decoder) catch return null;
+ const library = c.go_mpp_library_open(max_width, max_height) orelse {
+ std.heap.c_allocator.destroy(decoder);
+ return null;
+ };
+ decoder.* = .{ .library = library };
+ c.go_video_decoder_initialize(&decoder.base, &ops);
+ if (c.go_mpp_library_ready(library) == 0) {
+ copyLibraryError(decoder);
+ writeError(error_buffer, error_capacity, std.mem.sliceTo(&decoder.error_message, 0));
+ mppDestroy(&decoder.base);
+ return null;
+ }
+ std.debug.print("MPP plugin: {s}\n", .{std.mem.span(c.go_mpp_library_path(library))});
+ return &decoder.base;
+}
diff --git a/src/media/video/video_decoder_selection.zig b/src/media/video/video_decoder_selection.zig
index a81298e..01ce9b6 100644
--- a/src/media/video/video_decoder_selection.zig
+++ b/src/media/video/video_decoder_selection.zig
@@ -37,6 +37,8 @@ fn isArm() c_int {
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_V4L2_REQUEST)
+ "v4l2-request"
else if (backend == c.GO_VIDEO_DECODER_BACKEND_CEDAR)
"cedar"
else if (backend == c.GO_VIDEO_DECODER_BACKEND_SOFTWARE)
@@ -67,6 +69,14 @@ fn createBackend(
error_buffer.len,
);
}
+ if (backend == c.GO_VIDEO_DECODER_BACKEND_V4L2_REQUEST) {
+ return c.go_video_decoder_v4l2_request_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);
}
@@ -88,7 +98,7 @@ pub export fn go_video_decoder_selection_create(
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)
+ settings.preference > c.GO_VIDEO_DECODER_PREFERENCE_V4L2_REQUEST)
{
writeError(error_output, error_capacity, "invalid video decoder configuration");
return -1;
@@ -97,7 +107,7 @@ pub export fn go_video_decoder_selection_create(
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;
+ var candidates: [4]c.GoVideoDecoderBackend = undefined;
const candidate_count = c.go_video_decoder_candidate_order(
settings.preference,
platform,
diff --git a/src/media/video/video_decoder_v4l2_request.c b/src/media/video/video_decoder_v4l2_request.c
new file mode 100644
index 0000000..42e9bd3
--- /dev/null
+++ b/src/media/video/video_decoder_v4l2_request.c
@@ -0,0 +1,328 @@
+#include "video_decoder.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+typedef struct {
+ GoVideoDecoder base;
+ const AVCodec* codec;
+ AVCodecContext* context;
+ AVBufferRef* device;
+ AVPacket* packet;
+ AVFrame* hardware_frame;
+ AVFrame* mapped_frame;
+ enum AVHWDeviceType device_type;
+ int max_width;
+ int max_height;
+ int last_width;
+ int last_height;
+ int last_format;
+ int last_strides[3];
+ char error[256];
+} GoV4l2RequestVideoDecoder;
+
+static void copy_error(char* destination, size_t capacity, const char* message) {
+ if (destination && capacity > 0)
+ snprintf(destination, capacity, "%s", message);
+}
+
+static void set_ffmpeg_error(GoV4l2RequestVideoDecoder* 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 int has_usable_media_device(void) {
+ char path[32];
+ for (int index = 0; index < 64; ++index) {
+ snprintf(path, sizeof(path), "/dev/media%d", index);
+ int descriptor = open(path, O_RDWR | O_CLOEXEC);
+ if (descriptor >= 0) {
+ close(descriptor);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int video_device_supports_h264_request(int descriptor) {
+ struct v4l2_capability capability;
+ memset(&capability, 0, sizeof(capability));
+ if (ioctl(descriptor, VIDIOC_QUERYCAP, &capability) < 0)
+ return 0;
+
+ uint32_t capabilities = (capability.capabilities & V4L2_CAP_DEVICE_CAPS) != 0
+ ? capability.device_caps
+ : capability.capabilities;
+ if ((capabilities & V4L2_CAP_STREAMING) == 0)
+ return 0;
+
+ struct v4l2_fmtdesc format;
+ memset(&format, 0, sizeof(format));
+ if ((capabilities & V4L2_CAP_VIDEO_M2M_MPLANE) != 0)
+ format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
+ else if ((capabilities & V4L2_CAP_VIDEO_M2M) != 0)
+ format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+ else
+ return 0;
+ while (ioctl(descriptor, VIDIOC_ENUM_FMT, &format) == 0) {
+ if (format.pixelformat == V4L2_PIX_FMT_H264_SLICE)
+ return 1;
+ ++format.index;
+ }
+ return 0;
+}
+
+static int has_usable_h264_request_decoder(void) {
+ char path[32];
+ for (int index = 0; index < 64; ++index) {
+ snprintf(path, sizeof(path), "/dev/video%d", index);
+ int descriptor = open(path, O_RDWR | O_CLOEXEC);
+ if (descriptor < 0)
+ continue;
+ int supported = video_device_supports_h264_request(descriptor);
+ close(descriptor);
+ if (supported)
+ return 1;
+ }
+ return 0;
+}
+
+static int has_usable_v4l2_request_devices(void) {
+ return has_usable_media_device() && has_usable_h264_request_decoder();
+}
+
+static int codec_supports_device(const AVCodec* codec, enum AVHWDeviceType type) {
+ for (int index = 0;; ++index) {
+ const AVCodecHWConfig* config = avcodec_get_hw_config(codec, index);
+ if (!config)
+ return 0;
+ if (config->device_type == type && config->pix_fmt == AV_PIX_FMT_DRM_PRIME &&
+ (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) != 0)
+ return 1;
+ }
+}
+
+static enum AVPixelFormat select_hardware_format(AVCodecContext* context,
+ const enum AVPixelFormat* formats) {
+ GoV4l2RequestVideoDecoder* decoder = context->opaque;
+ for (const enum AVPixelFormat* format = formats; *format != AV_PIX_FMT_NONE; ++format) {
+ if (*format == AV_PIX_FMT_DRM_PRIME)
+ return *format;
+ }
+ snprintf(decoder->error, sizeof(decoder->error),
+ "V4L2 request decoder did not offer DRM PRIME output");
+ return AV_PIX_FMT_NONE;
+}
+
+static GoVideoDecoderResult v4l2_request_submit(GoVideoDecoder* base, const uint8_t* data,
+ size_t length) {
+ GoV4l2RequestVideoDecoder* decoder = (GoV4l2RequestVideoDecoder*)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, "V4L2 request packet submission failed", result);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+}
+
+static GoVideoDecoderResult v4l2_request_receive(GoVideoDecoder* base,
+ GoDecodedVideoFrame* output) {
+ GoV4l2RequestVideoDecoder* decoder = (GoV4l2RequestVideoDecoder*)base;
+ int result = avcodec_receive_frame(decoder->context, decoder->hardware_frame);
+ if (result == AVERROR(EAGAIN) || result == AVERROR_EOF)
+ return GO_VIDEO_DECODER_RESULT_AGAIN;
+ if (result < 0) {
+ set_ffmpeg_error(decoder, "V4L2 request frame receive failed", result);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ if (decoder->hardware_frame->format != AV_PIX_FMT_DRM_PRIME) {
+ snprintf(decoder->error, sizeof(decoder->error),
+ "V4L2 request decoder returned pixel format %d",
+ decoder->hardware_frame->format);
+ av_frame_unref(decoder->hardware_frame);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+
+ result = av_hwframe_transfer_data(decoder->mapped_frame, decoder->hardware_frame, 0);
+ if (result < 0) {
+ set_ffmpeg_error(decoder, "V4L2 request frame mapping failed", result);
+ av_frame_unref(decoder->hardware_frame);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ if (decoder->mapped_frame->format != AV_PIX_FMT_NV12) {
+ snprintf(decoder->error, sizeof(decoder->error),
+ "V4L2 request decoder returned unsupported mapped format %d",
+ decoder->mapped_frame->format);
+ av_frame_unref(decoder->mapped_frame);
+ av_frame_unref(decoder->hardware_frame);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+ if (decoder->mapped_frame->width <= 0 || decoder->mapped_frame->height <= 0 ||
+ decoder->mapped_frame->width > decoder->max_width ||
+ decoder->mapped_frame->height > decoder->max_height) {
+ snprintf(decoder->error, sizeof(decoder->error),
+ "V4L2 request decoder returned invalid dimensions %dx%d",
+ decoder->mapped_frame->width, decoder->mapped_frame->height);
+ av_frame_unref(decoder->mapped_frame);
+ av_frame_unref(decoder->hardware_frame);
+ return GO_VIDEO_DECODER_RESULT_FATAL;
+ }
+
+ output->format = GO_VIDEO_PIXEL_FORMAT_NV12;
+ output->color_range = decoder->mapped_frame->color_range == AVCOL_RANGE_JPEG
+ ? GO_VIDEO_COLOR_RANGE_FULL
+ : GO_VIDEO_COLOR_RANGE_LIMITED;
+ output->width = decoder->mapped_frame->width;
+ output->height = decoder->mapped_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->mapped_frame->data[plane];
+ output->strides[plane] = decoder->mapped_frame->linesize[plane];
+ }
+ output->info_changed =
+ decoder->last_width != output->width || decoder->last_height != output->height ||
+ decoder->last_format != decoder->mapped_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->mapped_frame->format;
+ memcpy(decoder->last_strides, output->strides, sizeof(decoder->last_strides));
+ output->corrupt = (decoder->hardware_frame->flags & AV_FRAME_FLAG_CORRUPT) != 0;
+ output->backend_frame = decoder->mapped_frame;
+ return GO_VIDEO_DECODER_RESULT_OK;
+}
+
+static void v4l2_request_release(GoVideoDecoder* base, GoDecodedVideoFrame* frame) {
+ GoV4l2RequestVideoDecoder* decoder = (GoV4l2RequestVideoDecoder*)base;
+ if (frame->backend_frame == decoder->mapped_frame) {
+ av_frame_unref(decoder->mapped_frame);
+ av_frame_unref(decoder->hardware_frame);
+ }
+}
+
+static int v4l2_request_reset(GoVideoDecoder* base) {
+ GoV4l2RequestVideoDecoder* decoder = (GoV4l2RequestVideoDecoder*)base;
+ av_frame_unref(decoder->mapped_frame);
+ av_frame_unref(decoder->hardware_frame);
+ avcodec_flush_buffers(decoder->context);
+ 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* v4l2_request_last_error(const GoVideoDecoder* base) {
+ const GoV4l2RequestVideoDecoder* decoder = (const GoV4l2RequestVideoDecoder*)base;
+ return decoder->error[0] ? decoder->error : "V4L2 request H.264 decoder failure";
+}
+
+static void v4l2_request_destroy(GoVideoDecoder* base) {
+ GoV4l2RequestVideoDecoder* decoder = (GoV4l2RequestVideoDecoder*)base;
+ av_frame_free(&decoder->mapped_frame);
+ av_frame_free(&decoder->hardware_frame);
+ av_packet_free(&decoder->packet);
+ avcodec_free_context(&decoder->context);
+ av_buffer_unref(&decoder->device);
+ free(decoder);
+}
+
+static const GoVideoDecoderOps v4l2_request_ops = {
+ .name = "v4l2-request",
+ .backend = GO_VIDEO_DECODER_BACKEND_V4L2_REQUEST,
+ .submit_access_unit = v4l2_request_submit,
+ .receive_frame = v4l2_request_receive,
+ .release_frame = v4l2_request_release,
+ .reset = v4l2_request_reset,
+ .last_error = v4l2_request_last_error,
+ .destroy = v4l2_request_destroy,
+};
+
+GoVideoDecoder* go_video_decoder_v4l2_request_create(int max_width, int max_height, char* error,
+ size_t error_capacity) {
+ if (max_width <= 0 || max_height <= 0) {
+ copy_error(error, error_capacity, "invalid V4L2 request decoder dimensions");
+ return NULL;
+ }
+ if (!has_usable_v4l2_request_devices()) {
+ copy_error(error, error_capacity, "no writable H.264 V4L2 Request decoder");
+ return NULL;
+ }
+
+ GoV4l2RequestVideoDecoder* decoder = calloc(1, sizeof(*decoder));
+ if (!decoder) {
+ copy_error(error, error_capacity, "V4L2 request decoder allocation failed");
+ return NULL;
+ }
+ go_video_decoder_initialize(&decoder->base, &v4l2_request_ops);
+ decoder->max_width = max_width;
+ decoder->max_height = max_height;
+ decoder->device_type = av_hwdevice_find_type_by_name("v4l2request");
+ decoder->codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+
+ if (decoder->device_type == AV_HWDEVICE_TYPE_NONE)
+ snprintf(decoder->error, sizeof(decoder->error),
+ "FFmpeg was built without V4L2 request support");
+ else if (!decoder->codec || !codec_supports_device(decoder->codec, decoder->device_type))
+ snprintf(decoder->error, sizeof(decoder->error),
+ "FFmpeg H.264 V4L2 request decoder is unavailable");
+ else {
+ int result = av_hwdevice_ctx_create(&decoder->device, decoder->device_type, NULL, NULL, 0);
+ if (result < 0)
+ set_ffmpeg_error(decoder, "V4L2 request device initialization failed", result);
+ }
+
+ if (decoder->device && decoder->codec) {
+ decoder->context = avcodec_alloc_context3(decoder->codec);
+ if (decoder->context) {
+ decoder->context->opaque = decoder;
+ decoder->context->get_format = select_hardware_format;
+ decoder->context->hw_device_ctx = av_buffer_ref(decoder->device);
+ decoder->context->thread_count = 1;
+ decoder->context->flags |= AV_CODEC_FLAG_LOW_DELAY;
+ int result = avcodec_open2(decoder->context, decoder->codec, NULL);
+ if (result < 0)
+ set_ffmpeg_error(decoder, "V4L2 request H.264 initialization failed", result);
+ else {
+ decoder->packet = av_packet_alloc();
+ decoder->hardware_frame = av_frame_alloc();
+ decoder->mapped_frame = av_frame_alloc();
+ }
+ }
+ }
+
+ if (!decoder->context || !decoder->context->hw_device_ctx || !decoder->packet ||
+ !decoder->hardware_frame || !decoder->mapped_frame) {
+ if (!decoder->error[0])
+ snprintf(decoder->error, sizeof(decoder->error),
+ "V4L2 request H.264 decoder initialization failed");
+ copy_error(error, error_capacity, decoder->error);
+ v4l2_request_destroy(&decoder->base);
+ return NULL;
+ }
+ return &decoder->base;
+}
diff --git a/src/media/video/video_pipeline.c b/src/media/video/video_pipeline.c
deleted file mode 100644
index 2368fc0..0000000
--- a/src/media/video/video_pipeline.c
+++ /dev/null
@@ -1,728 +0,0 @@
-#include "video_pipeline.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "../../util/log.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;
- uint8_t data[VIDEO_PACKET_MAX_SIZE];
-} VideoPacket;
-
-struct GoVideoPipeline {
- SDL_Renderer* renderer;
- GoH264Depacketizer* depacketizer;
- uint8_t bootstrap[H264_BOOTSTRAP_MAX_SIZE];
- size_t bootstrap_length;
- int parameter_sets_dirty;
- char bootstrap_path[512];
-
- VideoPacket packet_queue[VIDEO_PACKET_QUEUE_CAPACITY];
- int packet_queue_head;
- int packet_queue_tail;
- int packet_queue_count;
- pthread_mutex_t packet_lock;
- pthread_cond_t packet_condition;
- int packet_sync_initialized;
- pthread_t thread;
- int thread_started;
- atomic_int stop;
- atomic_int accepting_packets;
- atomic_int restart_epoch_pending;
-
- GoVideoDecoderSelection decoders;
- AVFrame* decoded_frame;
- int max_width;
- int max_height;
- char decoder_failure[256];
- atomic_int failed;
-
- SDL_Texture* texture;
- struct SwsContext* scaler;
- uint8_t* rgb_buffer;
- int rgb_linesize;
- int texture_width;
- int texture_height;
- unsigned int crop_aspect_width;
- unsigned int crop_aspect_height;
- Uint32 texture_format;
- int direct_nv12_available;
- int upload_error_reported;
-
- pthread_mutex_t frame_lock;
- int frame_lock_initialized;
- AVFrame* display_frame;
- AVFrame* render_frame;
- atomic_int frame_ready;
-
- atomic_int synced;
- atomic_int keyframe_pending;
- atomic_int rtp_packets;
- atomic_int payload_packets;
- atomic_int rejected_packets;
- atomic_int last_rejected_payload_type;
- atomic_int access_units;
- atomic_int decoded_frames;
- atomic_int rendered_frames;
- atomic_int frame_nals;
- atomic_int idr_nals;
- atomic_int parameter_nals;
- atomic_int auxiliary_nals;
- atomic_uint last_timestamp;
- atomic_int discontinuities;
- atomic_int missing_packets;
- atomic_int late_packets;
- 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;
-};
-
-static int load_bootstrap(GoVideoPipeline* pipeline) {
- if (!pipeline->bootstrap_path[0])
- return 0;
- struct stat status;
- if (stat(pipeline->bootstrap_path, &status) != 0)
- return errno == ENOENT ? 0 : -1;
- if (status.st_size <= 0 || status.st_size > H264_BOOTSTRAP_MAX_SIZE)
- return -1;
- FILE* file = fopen(pipeline->bootstrap_path, "rb");
- if (!file)
- return -1;
- uint8_t data[H264_BOOTSTRAP_MAX_SIZE];
- size_t length = fread(data, 1, (size_t)status.st_size, file);
- int failed = ferror(file) || length != (size_t)status.st_size;
- fclose(file);
- if (failed || go_h264_depacketizer_set_bootstrap(pipeline->depacketizer, data, length) != 0)
- return -1;
- go_dbg("H.264 bootstrap loaded (%zu bytes)\n", length);
- return 0;
-}
-
-static int persist_bootstrap(GoVideoPipeline* pipeline) {
- if (!pipeline->parameter_sets_dirty || !pipeline->bootstrap_path[0] ||
- pipeline->bootstrap_length == 0)
- return 0;
- char temporary_path[sizeof(pipeline->bootstrap_path) + 5];
- if (snprintf(temporary_path, sizeof(temporary_path), "%s.tmp", pipeline->bootstrap_path) >=
- (int)sizeof(temporary_path))
- return -1;
- FILE* file = fopen(temporary_path, "wb");
- if (!file)
- return -1;
- int failed = fchmod(fileno(file), 0600) != 0 ||
- fwrite(pipeline->bootstrap, 1, pipeline->bootstrap_length, file) !=
- pipeline->bootstrap_length ||
- fflush(file) != 0 || fsync(fileno(file)) != 0;
- if (fclose(file) != 0)
- failed = 1;
- if (failed || rename(temporary_path, pipeline->bootstrap_path) != 0) {
- unlink(temporary_path);
- return -1;
- }
- pipeline->parameter_sets_dirty = 0;
- go_dbg("H.264 bootstrap updated\n");
- return 0;
-}
-
-static void publish_frame(GoVideoPipeline* pipeline, AVFrame* frame, const char* decoder_name) {
- atomic_fetch_add(&pipeline->decoded_frames, 1);
- atomic_store(&pipeline->keyframe_pending, 0);
- if (!atomic_exchange(&pipeline->synced, 1)) {
- go_dbg("Video output active: %dx%d format=%d decoder=%s\n", frame->width, frame->height,
- frame->format, decoder_name);
- }
- pthread_mutex_lock(&pipeline->frame_lock);
- av_frame_unref(pipeline->display_frame);
- if (av_frame_ref(pipeline->display_frame, frame) == 0)
- atomic_store(&pipeline->frame_ready, 1);
- pthread_mutex_unlock(&pipeline->frame_lock);
-}
-
-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;
- 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 = format;
- target->width = frame->width;
- target->height = frame->height;
- if (av_frame_get_buffer(target, 32) < 0)
- return -1;
- }
- if (av_frame_make_writable(target) < 0)
- return -1;
- 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, go_video_decoder_name(pipeline->decoders.active));
- return 0;
-}
-
-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;
- }
- }
-}
-
-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 (!pipeline->decoders.active || atomic_load(&pipeline->failed))
- return;
- 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) {
- if (length < 12 || !pipeline->depacketizer)
- return;
- atomic_fetch_add(&pipeline->rtp_packets, 1);
- GoH264FeedResult result = go_h264_depacketizer_feed(pipeline->depacketizer, packet, length,
- decode_access_unit, pipeline);
- if (!result.accepted) {
- atomic_fetch_add(&pipeline->rejected_packets, 1);
- atomic_store(&pipeline->last_rejected_payload_type, packet[1] & 0x7F);
- return;
- }
- atomic_fetch_add(&pipeline->payload_packets, 1);
- atomic_store(&pipeline->last_timestamp, result.timestamp);
- atomic_fetch_add(&pipeline->missing_packets, result.missing_packets);
- atomic_fetch_add(&pipeline->late_packets, result.late_packets);
- atomic_fetch_add(&pipeline->discontinuities, result.discontinuities);
- atomic_fetch_add(&pipeline->access_units, result.access_units);
- atomic_fetch_add(&pipeline->frame_nals, result.frame_nals);
- atomic_fetch_add(&pipeline->idr_nals, result.idr_nals);
- atomic_fetch_add(&pipeline->parameter_nals, result.parameter_nals);
- atomic_fetch_add(&pipeline->auxiliary_nals, result.auxiliary_nals);
- if (result.requires_keyframe)
- atomic_store(&pipeline->keyframe_pending, 1);
-
- size_t bootstrap_length = 0;
- int bootstrap_result =
- go_h264_depacketizer_take_bootstrap(pipeline->depacketizer, pipeline->bootstrap,
- sizeof(pipeline->bootstrap), &bootstrap_length);
- if (bootstrap_result > 0) {
- pipeline->bootstrap_length = bootstrap_length;
- pipeline->parameter_sets_dirty = 1;
- } else if (bootstrap_result < 0) {
- fprintf(stderr, "H.264 parameter sets exceed the bootstrap limit\n");
- }
-}
-
-static void* video_worker(void* context) {
- GoVideoPipeline* pipeline = context;
- for (;;) {
- VideoPacket packet;
- pthread_mutex_lock(&pipeline->packet_lock);
- while (pipeline->packet_queue_count == 0 && !atomic_load(&pipeline->stop))
- pthread_cond_wait(&pipeline->packet_condition, &pipeline->packet_lock);
- if (atomic_load(&pipeline->stop)) {
- pthread_mutex_unlock(&pipeline->packet_lock);
- break;
- }
- packet = pipeline->packet_queue[pipeline->packet_queue_head];
- pipeline->packet_queue_head =
- (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;
-}
-
-static void destroy_texture(GoVideoPipeline* pipeline) {
- if (pipeline->texture)
- SDL_DestroyTexture(pipeline->texture);
- pipeline->texture = NULL;
- pipeline->texture_format = SDL_PIXELFORMAT_UNKNOWN;
-}
-
-static int configure_renderer(GoVideoPipeline* pipeline, const AVFrame* frame) {
- int width = frame->width;
- int height = frame->height;
- int source_format = frame->format;
- int source_full_range = frame->color_range == AVCOL_RANGE_JPEG;
- if (width <= 0 || height <= 0 || width > 8192 || height > 8192)
- return -1;
- int direct_nv12 = source_format == AV_PIX_FMT_NV12 && pipeline->direct_nv12_available;
- Uint32 requested_format = direct_nv12 ? SDL_PIXELFORMAT_NV12 : SDL_PIXELFORMAT_RGB24;
- if (pipeline->texture &&
- (width != pipeline->texture_width || height != pipeline->texture_height ||
- requested_format != pipeline->texture_format))
- destroy_texture(pipeline);
- if (!pipeline->texture) {
- if (direct_nv12) {
- SDL_SetYUVConversionMode(SDL_YUV_CONVERSION_BT709);
- pipeline->texture = SDL_CreateTexture(pipeline->renderer, SDL_PIXELFORMAT_NV12,
- SDL_TEXTUREACCESS_STREAMING, width, height);
- if (!pipeline->texture) {
- fprintf(stderr, "Direct NV12 texture unavailable: %s\n", SDL_GetError());
- pipeline->direct_nv12_available = 0;
- requested_format = SDL_PIXELFORMAT_RGB24;
- }
- }
- if (!pipeline->texture)
- pipeline->texture = SDL_CreateTexture(pipeline->renderer, SDL_PIXELFORMAT_RGB24,
- SDL_TEXTUREACCESS_STREAMING, width, height);
- if (!pipeline->texture) {
- fprintf(stderr, "SDL_CreateTexture: %s\n", SDL_GetError());
- return -1;
- }
- pipeline->texture_width = width;
- pipeline->texture_height = height;
- pipeline->texture_format = requested_format;
- go_dbg("Created %s texture %dx%d\n",
- requested_format == SDL_PIXELFORMAT_NV12 ? "NV12" : "RGB24", width, height);
- }
- if (pipeline->texture_format == SDL_PIXELFORMAT_NV12)
- return 0;
-
- struct SwsContext* scaler =
- sws_getCachedContext(pipeline->scaler, width, height, (enum AVPixelFormat)source_format,
- width, height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
- if (!scaler) {
- fprintf(stderr, "Failed to create YUV-to-RGB converter\n");
- return -1;
- }
- pipeline->scaler = scaler;
- const int* coefficients = sws_getCoefficients(SWS_CS_ITU709);
- sws_setColorspaceDetails(pipeline->scaler, coefficients, source_full_range, coefficients, 1, 0,
- 1 << 16, 1 << 16);
- size_t required = (size_t)width * (size_t)height * 3;
- uint8_t* buffer = realloc(pipeline->rgb_buffer, required);
- if (!buffer) {
- fprintf(stderr, "Failed to allocate %zu-byte RGB frame\n", required);
- return -1;
- }
- pipeline->rgb_buffer = buffer;
- pipeline->rgb_linesize = width * 3;
- return 0;
-}
-
-static int upload_frame(GoVideoPipeline* pipeline, const AVFrame* frame) {
- if (configure_renderer(pipeline, frame) < 0)
- return -1;
- if (pipeline->texture_format == SDL_PIXELFORMAT_NV12) {
- if (SDL_UpdateNVTexture(pipeline->texture, NULL, frame->data[0], frame->linesize[0],
- frame->data[1], frame->linesize[1]) == 0)
- return 0;
- fprintf(stderr, "Direct NV12 upload disabled: %s\n", SDL_GetError());
- pipeline->direct_nv12_available = 0;
- destroy_texture(pipeline);
- if (configure_renderer(pipeline, frame) < 0)
- return -1;
- }
- const uint8_t* source[4] = {frame->data[0], frame->data[1], frame->data[2], frame->data[3]};
- uint8_t* destination[4] = {pipeline->rgb_buffer, NULL, NULL, NULL};
- int destination_linesize[4] = {pipeline->rgb_linesize, 0, 0, 0};
- sws_scale(pipeline->scaler, source, frame->linesize, 0, frame->height, destination,
- destination_linesize);
- return SDL_UpdateTexture(pipeline->texture, NULL, pipeline->rgb_buffer, pipeline->rgb_linesize);
-}
-
-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 = 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 (config->bootstrap_path &&
- snprintf(pipeline->bootstrap_path, sizeof(pipeline->bootstrap_path), "%s",
- config->bootstrap_path) >= (int)sizeof(pipeline->bootstrap_path)) {
- free(pipeline);
- return NULL;
- }
- if (pthread_mutex_init(&pipeline->packet_lock, NULL) != 0) {
- free(pipeline);
- return NULL;
- }
- if (pthread_cond_init(&pipeline->packet_condition, NULL) != 0) {
- pthread_mutex_destroy(&pipeline->packet_lock);
- free(pipeline);
- return NULL;
- }
- pipeline->packet_sync_initialized = 1;
- if (pthread_mutex_init(&pipeline->frame_lock, NULL) != 0) {
- go_video_pipeline_destroy(pipeline);
- return NULL;
- }
- pipeline->frame_lock_initialized = 1;
- pipeline->depacketizer = go_h264_depacketizer_create(GO_VIDEO_PAYLOAD_TYPE);
- if (!pipeline->depacketizer) {
- go_video_pipeline_destroy(pipeline);
- return NULL;
- }
- if (load_bootstrap(pipeline) != 0) {
- go_video_pipeline_destroy(pipeline);
- return NULL;
- }
- pipeline->decoded_frame = av_frame_alloc();
- pipeline->display_frame = av_frame_alloc();
- pipeline->render_frame = av_frame_alloc();
- if (!pipeline->decoded_frame || !pipeline->display_frame || !pipeline->render_frame) {
- go_video_pipeline_destroy(pipeline);
- return NULL;
- }
- if (select_decoder(pipeline, config->decoder_preference) != 0) {
- go_video_pipeline_destroy(pipeline);
- return NULL;
- }
- return pipeline;
-}
-
-int go_video_pipeline_start(GoVideoPipeline* pipeline) {
- if (!pipeline || pipeline->thread_started)
- return -1;
- atomic_store(&pipeline->stop, 0);
- atomic_store(&pipeline->accepting_packets, 1);
- if (pthread_create(&pipeline->thread, NULL, video_worker, pipeline) != 0) {
- atomic_store(&pipeline->accepting_packets, 0);
- return -1;
- }
- pipeline->thread_started = 1;
- return 0;
-}
-
-void go_video_pipeline_stop(GoVideoPipeline* pipeline) {
- if (!pipeline)
- return;
- atomic_store(&pipeline->accepting_packets, 0);
- if (!pipeline->thread_started)
- return;
- atomic_store(&pipeline->stop, 1);
- pthread_mutex_lock(&pipeline->packet_lock);
- pthread_cond_broadcast(&pipeline->packet_condition);
- pthread_mutex_unlock(&pipeline->packet_lock);
- pthread_join(pipeline->thread, NULL);
- pipeline->thread_started = 0;
- pthread_mutex_lock(&pipeline->packet_lock);
- pipeline->packet_queue_head = 0;
- pipeline->packet_queue_tail = 0;
- pipeline->packet_queue_count = 0;
- pthread_mutex_unlock(&pipeline->packet_lock);
-}
-
-void go_video_pipeline_set_crop_aspect(GoVideoPipeline* pipeline, unsigned int width,
- unsigned int height) {
- if (!pipeline)
- return;
- pipeline->crop_aspect_width = width;
- pipeline->crop_aspect_height = height;
-}
-
-void go_video_pipeline_push_rtp(GoVideoPipeline* pipeline, const uint8_t* packet, size_t length) {
- if (!pipeline || !packet || length < 12 || !atomic_load(&pipeline->accepting_packets))
- return;
- 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);
- if (pipeline->packet_queue_count == VIDEO_PACKET_QUEUE_CAPACITY) {
- pipeline->packet_queue_head =
- (pipeline->packet_queue_head + 1) % VIDEO_PACKET_QUEUE_CAPACITY;
- 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;
- memcpy(target->data, packet, length);
- pipeline->packet_queue_tail = (pipeline->packet_queue_tail + 1) % VIDEO_PACKET_QUEUE_CAPACITY;
- pipeline->packet_queue_count++;
- pthread_cond_signal(&pipeline->packet_condition);
- pthread_mutex_unlock(&pipeline->packet_lock);
-}
-
-void go_video_pipeline_render(GoVideoPipeline* pipeline) {
- if (!pipeline || !atomic_load(&pipeline->frame_ready))
- return;
- pthread_mutex_lock(&pipeline->frame_lock);
- if (atomic_load(&pipeline->frame_ready)) {
- av_frame_unref(pipeline->render_frame);
- av_frame_move_ref(pipeline->render_frame, pipeline->display_frame);
- atomic_store(&pipeline->frame_ready, 0);
- }
- pthread_mutex_unlock(&pipeline->frame_lock);
- if (!pipeline->render_frame->data[0] || upload_frame(pipeline, pipeline->render_frame) < 0) {
- if (!pipeline->upload_error_reported) {
- fprintf(stderr, "Frame upload failed: %s\n", SDL_GetError());
- pipeline->upload_error_reported = 1;
- }
- av_frame_unref(pipeline->render_frame);
- return;
- }
- int output_width = 0;
- int output_height = 0;
- SDL_GetRendererOutputSize(pipeline->renderer, &output_width, &output_height);
- SDL_Rect source = {0, 0, pipeline->texture_width, pipeline->texture_height};
- if (pipeline->crop_aspect_width > 0 && pipeline->crop_aspect_height > 0 &&
- (int64_t)source.w * pipeline->crop_aspect_height >
- (int64_t)source.h * pipeline->crop_aspect_width) {
- source.w =
- (int)((int64_t)source.h * pipeline->crop_aspect_width / pipeline->crop_aspect_height) &
- ~1;
- source.x = (pipeline->texture_width - source.w) / 2;
- }
- SDL_Rect destination = {0, 0, output_width, output_height};
- if ((int64_t)output_width * source.h > (int64_t)output_height * source.w) {
- destination.w = output_height * source.w / source.h;
- destination.x = (output_width - destination.w) / 2;
- } else {
- destination.h = output_width * source.h / source.w;
- destination.y = (output_height - destination.h) / 2;
- }
- SDL_SetRenderDrawColor(pipeline->renderer, 0, 0, 0, 255);
- SDL_RenderClear(pipeline->renderer);
- SDL_RenderCopy(pipeline->renderer, pipeline->texture, &source, &destination);
- SDL_RenderPresent(pipeline->renderer);
- atomic_fetch_add(&pipeline->rendered_frames, 1);
- av_frame_unref(pipeline->render_frame);
-}
-
-int go_video_pipeline_needs_keyframe(const GoVideoPipeline* pipeline) {
- return pipeline &&
- (!atomic_load(&pipeline->synced) || atomic_load(&pipeline->keyframe_pending));
-}
-
-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);
-}
-
-GoVideoStats go_video_pipeline_stats(GoVideoPipeline* pipeline) {
- GoVideoStats stats = {0};
- if (!pipeline)
- return stats;
- stats.rtp_packets = atomic_load(&pipeline->rtp_packets);
- stats.payload_packets = atomic_load(&pipeline->payload_packets);
- stats.rejected_packets = atomic_load(&pipeline->rejected_packets);
- stats.last_rejected_payload_type = atomic_load(&pipeline->last_rejected_payload_type);
- stats.access_units = atomic_load(&pipeline->access_units);
- stats.decoded_frames = atomic_load(&pipeline->decoded_frames);
- stats.rendered_frames = atomic_load(&pipeline->rendered_frames);
- stats.frame_nals = atomic_load(&pipeline->frame_nals);
- stats.idr_nals = atomic_load(&pipeline->idr_nals);
- stats.parameter_nals = atomic_load(&pipeline->parameter_nals);
- stats.auxiliary_nals = atomic_load(&pipeline->auxiliary_nals);
- stats.last_timestamp = atomic_load(&pipeline->last_timestamp);
- stats.synced = atomic_load(&pipeline->synced);
- stats.discontinuities = atomic_load(&pipeline->discontinuities);
- stats.missing_packets = atomic_load(&pipeline->missing_packets);
- stats.late_packets = atomic_load(&pipeline->late_packets);
- 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);
- stats.pending_packets = pipeline->packet_queue_count;
- pthread_mutex_unlock(&pipeline->packet_lock);
- return stats;
-}
-
-int go_video_pipeline_destroy(GoVideoPipeline* pipeline) {
- if (!pipeline)
- return 0;
- go_video_pipeline_stop(pipeline);
- int persist_result = persist_bootstrap(pipeline);
- if (pipeline->frame_lock_initialized) {
- pthread_mutex_lock(&pipeline->frame_lock);
- atomic_store(&pipeline->frame_ready, 0);
- if (pipeline->display_frame)
- av_frame_unref(pipeline->display_frame);
- pthread_mutex_unlock(&pipeline->frame_lock);
- }
- if (pipeline->render_frame)
- av_frame_free(&pipeline->render_frame);
- if (pipeline->display_frame)
- av_frame_free(&pipeline->display_frame);
- if (pipeline->scaler)
- sws_freeContext(pipeline->scaler);
- free(pipeline->rgb_buffer);
- destroy_texture(pipeline);
- 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)
- pthread_mutex_destroy(&pipeline->frame_lock);
- if (pipeline->packet_sync_initialized) {
- pthread_cond_destroy(&pipeline->packet_condition);
- pthread_mutex_destroy(&pipeline->packet_lock);
- }
- free(pipeline);
- return persist_result;
-}
diff --git a/src/media/video/video_pipeline.h b/src/media/video/video_pipeline.h
index 578f822..9acf685 100644
--- a/src/media/video/video_pipeline.h
+++ b/src/media/video/video_pipeline.h
@@ -31,6 +31,8 @@ typedef struct {
int access_units;
int decoded_frames;
int rendered_frames;
+ int source_width;
+ int source_height;
int frame_nals;
int idr_nals;
int parameter_nals;
@@ -57,8 +59,6 @@ typedef struct {
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,
- unsigned int height);
void go_video_pipeline_push_rtp(GoVideoPipeline* pipeline, const uint8_t* packet, size_t length);
void go_video_pipeline_render(GoVideoPipeline* pipeline);
int go_video_pipeline_needs_keyframe(const GoVideoPipeline* pipeline);
diff --git a/src/media/video/video_pipeline.zig b/src/media/video/video_pipeline.zig
new file mode 100644
index 0000000..9dded60
--- /dev/null
+++ b/src/media/video/video_pipeline.zig
@@ -0,0 +1,749 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("SDL2/SDL.h");
+ @cInclude("libavutil/frame.h");
+ @cInclude("libavutil/pixfmt.h");
+ @cInclude("libswscale/swscale.h");
+ @cInclude("stdlib.h");
+ @cInclude("h264_depacketizer.h");
+ @cInclude("video_decoder.h");
+ @cInclude("video_frame_copy.h");
+ @cInclude("video_pipeline.h");
+});
+
+const bootstrap_capacity = 4104;
+const packet_capacity = 4096;
+const queue_capacity = 128;
+const backpressure_attempts = 64;
+
+const VideoPacket = struct {
+ length: u16 = 0,
+ data: [packet_capacity]u8 = undefined,
+};
+
+const Pipeline = struct {
+ renderer: *c.SDL_Renderer,
+ depacketizer: ?*c.GoH264Depacketizer = null,
+ bootstrap: [bootstrap_capacity]u8 = undefined,
+ bootstrap_length: usize = 0,
+ parameter_sets_dirty: bool = false,
+ bootstrap_path: [512]u8 = [_]u8{0} ** 512,
+
+ packet_queue: [queue_capacity]VideoPacket = undefined,
+ packet_queue_head: usize = 0,
+ packet_queue_tail: usize = 0,
+ packet_queue_count: usize = 0,
+ packet_mutex: std.Thread.Mutex = .{},
+ packet_condition: std.Thread.Condition = .{},
+ thread: ?std.Thread = null,
+ stop: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ accepting_packets: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ restart_epoch_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+
+ decoders: c.GoVideoDecoderSelection = std.mem.zeroes(c.GoVideoDecoderSelection),
+ decoded_frame: ?*c.AVFrame = null,
+ max_width: c_int,
+ max_height: c_int,
+ decoder_failure: [256]u8 = [_]u8{0} ** 256,
+ failed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+
+ texture: ?*c.SDL_Texture = null,
+ scaler: ?*c.SwsContext = null,
+ rgb_buffer: ?[*]u8 = null,
+ rgb_linesize: c_int = 0,
+ texture_width: c_int = 0,
+ texture_height: c_int = 0,
+ texture_format: c.Uint32 = c.SDL_PIXELFORMAT_UNKNOWN,
+ direct_nv12_available: bool = true,
+ upload_error_reported: bool = false,
+
+ frame_mutex: std.Thread.Mutex = .{},
+ display_frame: ?*c.AVFrame = null,
+ render_frame: ?*c.AVFrame = null,
+ frame_ready: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+
+ synced: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ keyframe_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(true),
+ rtp_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ payload_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ rejected_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ last_rejected_payload_type: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(-1),
+ access_units: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoded_frames: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ rendered_frames: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ source_width: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ source_height: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ frame_nals: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ idr_nals: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ parameter_nals: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ auxiliary_nals: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ last_timestamp: std.atomic.Value(c_uint) = std.atomic.Value(c_uint).init(0),
+ discontinuities: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ missing_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ late_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_send_errors: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_receive_errors: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ last_decoder_error: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_backend: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_init_failures: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_runtime_fallbacks: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_backpressure_events: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_corrupt_frames: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ decoder_info_changes: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ keyframe_requests: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+ dropped_packets: std.atomic.Value(c_int) = std.atomic.Value(c_int).init(0),
+};
+
+pub const Stats = extern struct {
+ rtp_packets: c_int = 0,
+ payload_packets: c_int = 0,
+ rejected_packets: c_int = 0,
+ last_rejected_payload_type: c_int = 0,
+ access_units: c_int = 0,
+ decoded_frames: c_int = 0,
+ rendered_frames: c_int = 0,
+ source_width: c_int = 0,
+ source_height: c_int = 0,
+ frame_nals: c_int = 0,
+ idr_nals: c_int = 0,
+ parameter_nals: c_int = 0,
+ auxiliary_nals: c_int = 0,
+ last_timestamp: c_uint = 0,
+ synced: c_int = 0,
+ discontinuities: c_int = 0,
+ missing_packets: c_int = 0,
+ late_packets: c_int = 0,
+ decoder_send_errors: c_int = 0,
+ decoder_receive_errors: c_int = 0,
+ last_decoder_error: c_int = 0,
+ decoder_backend: c.GoVideoDecoderBackend = c.GO_VIDEO_DECODER_BACKEND_NONE,
+ decoder_init_failures: c_int = 0,
+ decoder_runtime_fallbacks: c_int = 0,
+ decoder_backpressure_events: c_int = 0,
+ decoder_corrupt_frames: c_int = 0,
+ decoder_info_changes: c_int = 0,
+ keyframe_requests: c_int = 0,
+ pending_packets: c_int = 0,
+ dropped_packets: c_int = 0,
+};
+
+fn debugEnabled() bool {
+ return std.posix.getenv("GREENOVERCAST_DEBUG") != null;
+}
+
+fn copyZ(destination: []u8, message: []const u8) void {
+ @memset(destination, 0);
+ const length = @min(destination.len - 1, message.len);
+ @memcpy(destination[0..length], message[0..length]);
+}
+
+fn zSlice(buffer: []const u8) []const u8 {
+ return std.mem.sliceTo(buffer, 0);
+}
+
+fn loadBootstrap(pipeline: *Pipeline) c_int {
+ const path = zSlice(&pipeline.bootstrap_path);
+ if (path.len == 0) return 0;
+ const file = std.fs.cwd().openFile(path, .{}) catch |err| {
+ if (err != error.FileNotFound and debugEnabled())
+ std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)});
+ return 0;
+ };
+ defer file.close();
+ const size = file.getEndPos() catch return 0;
+ if (size == 0 or size > bootstrap_capacity) return 0;
+ var data: [bootstrap_capacity]u8 = undefined;
+ file.reader().readNoEof(data[0..@intCast(size)]) catch return 0;
+ if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, &data, @intCast(size)) != 0) {
+ if (debugEnabled()) std.debug.print("Ignoring invalid H.264 bootstrap cache\n", .{});
+ return 0;
+ }
+ if (debugEnabled()) std.debug.print("H.264 bootstrap loaded ({d} bytes)\n", .{size});
+ return 0;
+}
+
+fn persistBootstrap(pipeline: *Pipeline) c_int {
+ const path = zSlice(&pipeline.bootstrap_path);
+ if (!pipeline.parameter_sets_dirty or path.len == 0 or pipeline.bootstrap_length == 0)
+ return 0;
+ var temporary_buffer: [517]u8 = undefined;
+ const temporary = std.fmt.bufPrint(&temporary_buffer, "{s}.tmp", .{path}) catch return -1;
+ const cwd = std.fs.cwd();
+ var file = cwd.createFile(temporary, .{ .truncate = true, .mode = 0o600 }) catch return -1;
+ var closed = false;
+ defer if (!closed) file.close();
+ file.writeAll(pipeline.bootstrap[0..pipeline.bootstrap_length]) catch {
+ file.close();
+ closed = true;
+ cwd.deleteFile(temporary) catch {};
+ return -1;
+ };
+ file.sync() catch {
+ file.close();
+ closed = true;
+ cwd.deleteFile(temporary) catch {};
+ return -1;
+ };
+ file.close();
+ closed = true;
+ cwd.rename(temporary, path) catch {
+ cwd.deleteFile(temporary) catch {};
+ return -1;
+ };
+ pipeline.parameter_sets_dirty = false;
+ if (debugEnabled()) std.debug.print("H.264 bootstrap updated\n", .{});
+ return 0;
+}
+
+fn publishFrame(pipeline: *Pipeline, frame: *c.AVFrame, decoder_name: [*c]const u8) void {
+ _ = pipeline.decoded_frames.fetchAdd(1, .monotonic);
+ pipeline.source_width.store(frame.width, .monotonic);
+ pipeline.source_height.store(frame.height, .monotonic);
+ pipeline.keyframe_pending.store(false, .release);
+ if (!pipeline.synced.swap(true, .acq_rel) and debugEnabled()) {
+ std.debug.print("Video output active: {d}x{d} format={d} decoder={s}\n", .{
+ frame.width,
+ frame.height,
+ frame.format,
+ std.mem.span(decoder_name),
+ });
+ }
+ pipeline.frame_mutex.lock();
+ defer pipeline.frame_mutex.unlock();
+ c.av_frame_unref(pipeline.display_frame);
+ if (c.av_frame_ref(pipeline.display_frame, frame) == 0)
+ pipeline.frame_ready.store(true, .release);
+}
+
+fn publishDecodedFrame(pipeline: *Pipeline, frame: *const c.GoDecodedVideoFrame) c_int {
+ const target = pipeline.decoded_frame orelse return -1;
+ if (c.go_video_frame_validate(frame, pipeline.max_width, pipeline.max_height) != 0) return -1;
+ const format: c_int = if (frame.format == c.GO_VIDEO_PIXEL_FORMAT_NV12)
+ c.AV_PIX_FMT_NV12
+ else if (frame.format == c.GO_VIDEO_PIXEL_FORMAT_YUV420P)
+ c.AV_PIX_FMT_YUV420P
+ else
+ return -1;
+ if (target.format != format or target.width != frame.width or target.height != frame.height) {
+ c.av_frame_unref(target);
+ target.format = format;
+ target.width = frame.width;
+ target.height = frame.height;
+ if (c.av_frame_get_buffer(target, 32) < 0) return -1;
+ }
+ if (c.av_frame_make_writable(target) < 0) return -1;
+ if (c.go_video_frame_copy(
+ frame,
+ pipeline.max_width,
+ pipeline.max_height,
+ @ptrCast(&target.data),
+ @ptrCast(&target.linesize),
+ ) != 0) return -1;
+ target.color_range = if (frame.color_range == c.GO_VIDEO_COLOR_RANGE_FULL)
+ c.AVCOL_RANGE_JPEG
+ else
+ c.AVCOL_RANGE_MPEG;
+ target.colorspace = c.AVCOL_SPC_BT709;
+ publishFrame(pipeline, target, c.go_video_decoder_name(pipeline.decoders.active));
+ return 0;
+}
+
+fn drainDecoderFrames(pipeline: *Pipeline) c_int {
+ while (true) {
+ var frame: c.GoDecodedVideoFrame = undefined;
+ const result = c.go_video_decoder_receive_frame(pipeline.decoders.active, &frame);
+ if (result == c.GO_VIDEO_DECODER_RESULT_AGAIN) return 0;
+ if (result == c.GO_VIDEO_DECODER_RESULT_FATAL) {
+ _ = pipeline.decoder_receive_errors.fetchAdd(1, .monotonic);
+ pipeline.last_decoder_error.store(-1, .monotonic);
+ copyZ(&pipeline.decoder_failure, std.mem.span(c.go_video_decoder_last_error(pipeline.decoders.active)));
+ return -1;
+ }
+ if (frame.corrupt != 0) {
+ _ = pipeline.decoder_corrupt_frames.fetchAdd(1, .monotonic);
+ c.go_video_decoder_release_frame(pipeline.decoders.active, &frame);
+ continue;
+ }
+ if (frame.info_changed != 0) _ = pipeline.decoder_info_changes.fetchAdd(1, .monotonic);
+ const publish_result = publishDecodedFrame(pipeline, &frame);
+ c.go_video_decoder_release_frame(pipeline.decoders.active, &frame);
+ if (publish_result != 0) {
+ copyZ(&pipeline.decoder_failure, "decoder returned an invalid frame");
+ return -1;
+ }
+ }
+}
+
+fn switchToSoftwareDecoder(pipeline: *Pipeline) c_int {
+ if (pipeline.decoders.allow_runtime_fallback == 0 or pipeline.decoders.software == null or
+ pipeline.decoders.active == pipeline.decoders.software) return -1;
+ std.debug.print("{s} decoder disabled: {s}\n", .{
+ std.mem.span(c.go_video_decoder_name(pipeline.decoders.active)),
+ zSlice(&pipeline.decoder_failure),
+ });
+ if (c.go_video_decoder_selection_fallback(
+ &pipeline.decoders,
+ @ptrCast(&pipeline.decoder_failure),
+ pipeline.decoder_failure.len,
+ ) != 0) return -1;
+ pipeline.decoder_backend.store(@intCast(c.go_video_decoder_backend(pipeline.decoders.active)), .monotonic);
+ _ = pipeline.decoder_runtime_fallbacks.fetchAdd(1, .monotonic);
+ c.go_h264_depacketizer_restart_decode_epoch(pipeline.depacketizer);
+ pipeline.keyframe_pending.store(true, .release);
+ pipeline.synced.store(false, .release);
+ std.debug.print("Selected video decoder: {s}\n", .{
+ std.mem.span(c.go_video_decoder_name(pipeline.decoders.active)),
+ });
+ return 0;
+}
+
+fn markDecoderFailed(pipeline: *Pipeline) void {
+ if (pipeline.failed.swap(true, .acq_rel)) return;
+ pipeline.accepting_packets.store(false, .release);
+ std.debug.print("Video decoder failed: {s}\n", .{zSlice(&pipeline.decoder_failure)});
+}
+
+fn decodeWithActiveBackend(pipeline: *Pipeline, data: [*]const u8, length: usize) c_int {
+ for (0..backpressure_attempts) |attempt| {
+ if (pipeline.stop.load(.acquire)) return 0;
+ const result = c.go_video_decoder_submit_access_unit(pipeline.decoders.active, data, length);
+ if (result == c.GO_VIDEO_DECODER_RESULT_OK) return drainDecoderFrames(pipeline);
+ if (result == c.GO_VIDEO_DECODER_RESULT_FATAL) {
+ _ = pipeline.decoder_send_errors.fetchAdd(1, .monotonic);
+ pipeline.last_decoder_error.store(-1, .monotonic);
+ copyZ(&pipeline.decoder_failure, std.mem.span(c.go_video_decoder_last_error(pipeline.decoders.active)));
+ return -1;
+ }
+ _ = pipeline.decoder_backpressure_events.fetchAdd(1, .monotonic);
+ if (drainDecoderFrames(pipeline) != 0) return -1;
+ if (attempt + 1 < backpressure_attempts) c.SDL_Delay(1);
+ }
+ copyZ(&pipeline.decoder_failure, "decoder made no progress after 64 submit attempts");
+ return -1;
+}
+
+fn selectDecoder(pipeline: *Pipeline, preference: c.GoVideoDecoderPreference) c_int {
+ const config = c.GoVideoDecoderSelectionConfig{
+ .max_width = pipeline.max_width,
+ .max_height = pipeline.max_height,
+ .preference = preference,
+ };
+ if (c.go_video_decoder_selection_create(
+ &config,
+ &pipeline.decoders,
+ @ptrCast(&pipeline.decoder_failure),
+ pipeline.decoder_failure.len,
+ ) != 0) return -1;
+ pipeline.decoder_init_failures.store(@intCast(pipeline.decoders.init_failures), .monotonic);
+ pipeline.decoder_backend.store(@intCast(c.go_video_decoder_backend(pipeline.decoders.active)), .monotonic);
+ return 0;
+}
+
+fn decodeAccessUnit(
+ context: ?*anyopaque,
+ data: [*c]const u8,
+ length: usize,
+ _: [*c]const c.GoH264AccessUnit,
+) callconv(.c) void {
+ const pipeline: *Pipeline = @ptrCast(@alignCast(context orelse return));
+ if (pipeline.decoders.active == null or pipeline.failed.load(.acquire)) return;
+ if (decodeWithActiveBackend(pipeline, data, length) == 0) return;
+ if (switchToSoftwareDecoder(pipeline) != 0) markDecoderFailed(pipeline);
+}
+
+fn processPacket(pipeline: *Pipeline, packet: []const u8) void {
+ if (packet.len < 12 or pipeline.depacketizer == null) return;
+ _ = pipeline.rtp_packets.fetchAdd(1, .monotonic);
+ const result = c.go_h264_depacketizer_feed(
+ pipeline.depacketizer,
+ packet.ptr,
+ packet.len,
+ decodeAccessUnit,
+ pipeline,
+ );
+ if (result.accepted == 0) {
+ _ = pipeline.rejected_packets.fetchAdd(1, .monotonic);
+ pipeline.last_rejected_payload_type.store(packet[1] & 0x7f, .monotonic);
+ return;
+ }
+ _ = pipeline.payload_packets.fetchAdd(1, .monotonic);
+ pipeline.last_timestamp.store(result.timestamp, .monotonic);
+ _ = pipeline.missing_packets.fetchAdd(@intCast(result.missing_packets), .monotonic);
+ _ = pipeline.late_packets.fetchAdd(@intCast(result.late_packets), .monotonic);
+ _ = pipeline.discontinuities.fetchAdd(@intCast(result.discontinuities), .monotonic);
+ _ = pipeline.access_units.fetchAdd(@intCast(result.access_units), .monotonic);
+ _ = pipeline.frame_nals.fetchAdd(@intCast(result.frame_nals), .monotonic);
+ _ = pipeline.idr_nals.fetchAdd(@intCast(result.idr_nals), .monotonic);
+ _ = pipeline.parameter_nals.fetchAdd(@intCast(result.parameter_nals), .monotonic);
+ _ = pipeline.auxiliary_nals.fetchAdd(@intCast(result.auxiliary_nals), .monotonic);
+ if (result.requires_keyframe != 0) pipeline.keyframe_pending.store(true, .release);
+
+ var bootstrap_length: usize = 0;
+ const bootstrap_result = c.go_h264_depacketizer_take_bootstrap(
+ pipeline.depacketizer,
+ &pipeline.bootstrap,
+ pipeline.bootstrap.len,
+ &bootstrap_length,
+ );
+ if (bootstrap_result > 0) {
+ pipeline.bootstrap_length = bootstrap_length;
+ pipeline.parameter_sets_dirty = true;
+ } else if (bootstrap_result < 0) {
+ std.debug.print("H.264 parameter sets exceed the bootstrap limit\n", .{});
+ }
+}
+
+fn worker(pipeline: *Pipeline) void {
+ while (true) {
+ pipeline.packet_mutex.lock();
+ while (pipeline.packet_queue_count == 0 and !pipeline.stop.load(.acquire))
+ pipeline.packet_condition.wait(&pipeline.packet_mutex);
+ if (pipeline.stop.load(.acquire)) {
+ pipeline.packet_mutex.unlock();
+ break;
+ }
+ const packet = pipeline.packet_queue[pipeline.packet_queue_head];
+ pipeline.packet_queue_head = (pipeline.packet_queue_head + 1) % queue_capacity;
+ pipeline.packet_queue_count -= 1;
+ pipeline.packet_mutex.unlock();
+ if (pipeline.restart_epoch_pending.swap(false, .acq_rel))
+ c.go_h264_depacketizer_restart_decode_epoch(pipeline.depacketizer);
+ processPacket(pipeline, packet.data[0..packet.length]);
+ }
+}
+
+fn destroyTexture(pipeline: *Pipeline) void {
+ if (pipeline.texture) |texture| c.SDL_DestroyTexture(texture);
+ pipeline.texture = null;
+ pipeline.texture_format = c.SDL_PIXELFORMAT_UNKNOWN;
+}
+
+fn configureRenderer(pipeline: *Pipeline, frame: *const c.AVFrame) c_int {
+ const width = frame.width;
+ const height = frame.height;
+ const source_format = frame.format;
+ const source_full_range: c_int = @intFromBool(frame.color_range == c.AVCOL_RANGE_JPEG);
+ if (width <= 0 or height <= 0 or width > 8192 or height > 8192) return -1;
+ const direct_nv12 = source_format == c.AV_PIX_FMT_NV12 and pipeline.direct_nv12_available;
+ var requested_format: c.Uint32 = if (direct_nv12) c.SDL_PIXELFORMAT_NV12 else c.SDL_PIXELFORMAT_RGB24;
+ if (pipeline.texture != null and
+ (width != pipeline.texture_width or height != pipeline.texture_height or
+ requested_format != pipeline.texture_format)) destroyTexture(pipeline);
+ if (pipeline.texture == null) {
+ if (direct_nv12) {
+ pipeline.texture = c.SDL_CreateTexture(
+ pipeline.renderer,
+ c.SDL_PIXELFORMAT_NV12,
+ c.SDL_TEXTUREACCESS_STREAMING,
+ width,
+ height,
+ );
+ if (pipeline.texture == null) {
+ std.debug.print("Direct NV12 texture unavailable: {s}\n", .{std.mem.span(c.SDL_GetError())});
+ pipeline.direct_nv12_available = false;
+ requested_format = c.SDL_PIXELFORMAT_RGB24;
+ }
+ }
+ if (pipeline.texture == null) {
+ pipeline.texture = c.SDL_CreateTexture(
+ pipeline.renderer,
+ c.SDL_PIXELFORMAT_RGB24,
+ c.SDL_TEXTUREACCESS_STREAMING,
+ width,
+ height,
+ );
+ }
+ if (pipeline.texture == null) {
+ std.debug.print("SDL_CreateTexture: {s}\n", .{std.mem.span(c.SDL_GetError())});
+ return -1;
+ }
+ pipeline.texture_width = width;
+ pipeline.texture_height = height;
+ pipeline.texture_format = requested_format;
+ if (debugEnabled()) std.debug.print("Created {s} texture {d}x{d}\n", .{
+ if (requested_format == c.SDL_PIXELFORMAT_NV12) "NV12" else "RGB24",
+ width,
+ height,
+ });
+ }
+ if (pipeline.texture_format == c.SDL_PIXELFORMAT_NV12) return 0;
+
+ const scaler = c.sws_getCachedContext(
+ pipeline.scaler,
+ width,
+ height,
+ @intCast(source_format),
+ width,
+ height,
+ c.AV_PIX_FMT_RGB24,
+ c.SWS_BILINEAR,
+ null,
+ null,
+ null,
+ );
+ if (scaler == null) {
+ std.debug.print("Failed to create YUV-to-RGB converter\n", .{});
+ return -1;
+ }
+ pipeline.scaler = scaler;
+ const coefficients = c.sws_getCoefficients(c.SWS_CS_ITU709);
+ _ = c.sws_setColorspaceDetails(scaler, coefficients, source_full_range, coefficients, 1, 0, 1 << 16, 1 << 16);
+ const required = std.math.mul(usize, @intCast(width), @intCast(height)) catch return -1;
+ const byte_count = std.math.mul(usize, required, 3) catch return -1;
+ const buffer = c.realloc(pipeline.rgb_buffer, byte_count) orelse {
+ std.debug.print("Failed to allocate {d}-byte RGB frame\n", .{byte_count});
+ return -1;
+ };
+ pipeline.rgb_buffer = @ptrCast(buffer);
+ pipeline.rgb_linesize = width * 3;
+ return 0;
+}
+
+fn uploadFrame(pipeline: *Pipeline, frame: *const c.AVFrame) c_int {
+ if (configureRenderer(pipeline, frame) < 0) return -1;
+ if (pipeline.texture_format == c.SDL_PIXELFORMAT_NV12) {
+ c.SDL_SetYUVConversionMode(if (frame.color_range == c.AVCOL_RANGE_JPEG)
+ c.SDL_YUV_CONVERSION_JPEG
+ else
+ c.SDL_YUV_CONVERSION_BT709);
+ if (c.SDL_UpdateNVTexture(
+ pipeline.texture,
+ null,
+ frame.data[0],
+ frame.linesize[0],
+ frame.data[1],
+ frame.linesize[1],
+ ) == 0) return 0;
+ std.debug.print("Direct NV12 upload disabled: {s}\n", .{std.mem.span(c.SDL_GetError())});
+ pipeline.direct_nv12_available = false;
+ destroyTexture(pipeline);
+ if (configureRenderer(pipeline, frame) < 0) return -1;
+ }
+ var source = [4][*c]const u8{ frame.data[0], frame.data[1], frame.data[2], frame.data[3] };
+ var destination = [4][*c]u8{ pipeline.rgb_buffer.?, null, null, null };
+ var destination_linesize = [4]c_int{ pipeline.rgb_linesize, 0, 0, 0 };
+ _ = c.sws_scale(
+ pipeline.scaler,
+ @ptrCast(&source),
+ @ptrCast(&frame.linesize),
+ 0,
+ frame.height,
+ @ptrCast(&destination),
+ &destination_linesize,
+ );
+ return c.SDL_UpdateTexture(
+ pipeline.texture,
+ null,
+ pipeline.rgb_buffer,
+ pipeline.rgb_linesize,
+ );
+}
+
+pub export fn go_video_pipeline_create(config_pointer: ?*const c.GoVideoPipelineConfig) ?*Pipeline {
+ const config = config_pointer orelse return null;
+ if (config.renderer == null or config.max_width <= 0 or config.max_height <= 0 or
+ config.max_width > 8192 or config.max_height > 8192 or
+ config.decoder_preference < c.GO_VIDEO_DECODER_PREFERENCE_AUTO or
+ config.decoder_preference > c.GO_VIDEO_DECODER_PREFERENCE_V4L2_REQUEST) return null;
+ const pipeline = std.heap.c_allocator.create(Pipeline) catch return null;
+ pipeline.* = .{
+ .renderer = config.renderer.?,
+ .max_width = config.max_width,
+ .max_height = config.max_height,
+ };
+ errdefer _ = go_video_pipeline_destroy(pipeline);
+ if (config.bootstrap_path != null) {
+ const path = std.mem.span(config.bootstrap_path);
+ if (path.len >= pipeline.bootstrap_path.len) return null;
+ copyZ(&pipeline.bootstrap_path, path);
+ }
+ pipeline.depacketizer = c.go_h264_depacketizer_create(c.GO_VIDEO_PAYLOAD_TYPE);
+ if (pipeline.depacketizer == null or loadBootstrap(pipeline) != 0) return null;
+ pipeline.decoded_frame = c.av_frame_alloc();
+ pipeline.display_frame = c.av_frame_alloc();
+ pipeline.render_frame = c.av_frame_alloc();
+ if (pipeline.decoded_frame == null or pipeline.display_frame == null or pipeline.render_frame == null)
+ return null;
+ if (selectDecoder(pipeline, config.decoder_preference) != 0) return null;
+ return pipeline;
+}
+
+pub export fn go_video_pipeline_start(pipeline_pointer: ?*Pipeline) c_int {
+ const pipeline = pipeline_pointer orelse return -1;
+ if (pipeline.thread != null) return -1;
+ pipeline.stop.store(false, .release);
+ pipeline.accepting_packets.store(true, .release);
+ pipeline.thread = std.Thread.spawn(.{}, worker, .{pipeline}) catch {
+ pipeline.accepting_packets.store(false, .release);
+ return -1;
+ };
+ return 0;
+}
+
+pub export fn go_video_pipeline_stop(pipeline_pointer: ?*Pipeline) void {
+ const pipeline = pipeline_pointer orelse return;
+ pipeline.accepting_packets.store(false, .release);
+ const thread = pipeline.thread orelse return;
+ pipeline.stop.store(true, .release);
+ pipeline.packet_mutex.lock();
+ pipeline.packet_condition.broadcast();
+ pipeline.packet_mutex.unlock();
+ thread.join();
+ pipeline.thread = null;
+ pipeline.packet_mutex.lock();
+ pipeline.packet_queue_head = 0;
+ pipeline.packet_queue_tail = 0;
+ pipeline.packet_queue_count = 0;
+ pipeline.packet_mutex.unlock();
+}
+
+pub export fn go_video_pipeline_push_rtp(
+ pipeline_pointer: ?*Pipeline,
+ packet_pointer: ?[*]const u8,
+ length: usize,
+) void {
+ const pipeline = pipeline_pointer orelse return;
+ const packet = packet_pointer orelse return;
+ if (length < 12 or !pipeline.accepting_packets.load(.acquire)) return;
+ if (length > packet_capacity) {
+ _ = pipeline.dropped_packets.fetchAdd(1, .monotonic);
+ pipeline.keyframe_pending.store(true, .release);
+ pipeline.restart_epoch_pending.store(true, .release);
+ return;
+ }
+ pipeline.packet_mutex.lock();
+ defer pipeline.packet_mutex.unlock();
+ if (pipeline.packet_queue_count == queue_capacity) {
+ pipeline.packet_queue_head = (pipeline.packet_queue_head + 1) % queue_capacity;
+ pipeline.packet_queue_count -= 1;
+ _ = pipeline.dropped_packets.fetchAdd(1, .monotonic);
+ pipeline.keyframe_pending.store(true, .release);
+ pipeline.restart_epoch_pending.store(true, .release);
+ }
+ const target = &pipeline.packet_queue[pipeline.packet_queue_tail];
+ target.length = @intCast(length);
+ @memcpy(target.data[0..length], packet[0..length]);
+ pipeline.packet_queue_tail = (pipeline.packet_queue_tail + 1) % queue_capacity;
+ pipeline.packet_queue_count += 1;
+ pipeline.packet_condition.signal();
+}
+
+pub export fn go_video_pipeline_render(pipeline_pointer: ?*Pipeline) void {
+ const pipeline = pipeline_pointer orelse return;
+ if (!pipeline.frame_ready.load(.acquire)) return;
+ pipeline.frame_mutex.lock();
+ if (pipeline.frame_ready.load(.acquire)) {
+ c.av_frame_unref(pipeline.render_frame);
+ c.av_frame_move_ref(pipeline.render_frame, pipeline.display_frame);
+ pipeline.frame_ready.store(false, .release);
+ }
+ pipeline.frame_mutex.unlock();
+ const frame = pipeline.render_frame orelse return;
+ if (frame.data[0] == null or uploadFrame(pipeline, frame) < 0) {
+ if (!pipeline.upload_error_reported) {
+ std.debug.print("Frame upload failed: {s}\n", .{std.mem.span(c.SDL_GetError())});
+ pipeline.upload_error_reported = true;
+ }
+ c.av_frame_unref(frame);
+ return;
+ }
+ var output_width: c_int = 0;
+ var output_height: c_int = 0;
+ _ = c.SDL_GetRendererOutputSize(pipeline.renderer, &output_width, &output_height);
+ var source = c.SDL_Rect{ .x = 0, .y = 0, .w = pipeline.texture_width, .h = pipeline.texture_height };
+ var destination = c.SDL_Rect{ .x = 0, .y = 0, .w = output_width, .h = output_height };
+ if (@as(i64, output_width) * source.h > @as(i64, output_height) * source.w) {
+ destination.w = @divTrunc(output_height * source.w, source.h);
+ destination.x = @divTrunc(output_width - destination.w, 2);
+ } else {
+ destination.h = @divTrunc(output_width * source.h, source.w);
+ destination.y = @divTrunc(output_height - destination.h, 2);
+ }
+ _ = c.SDL_SetRenderDrawColor(pipeline.renderer, 0, 0, 0, 255);
+ _ = c.SDL_RenderClear(pipeline.renderer);
+ _ = c.SDL_RenderCopy(pipeline.renderer, pipeline.texture, &source, &destination);
+ c.SDL_RenderPresent(pipeline.renderer);
+ _ = pipeline.rendered_frames.fetchAdd(1, .monotonic);
+ c.av_frame_unref(frame);
+}
+
+pub export fn go_video_pipeline_needs_keyframe(pipeline: ?*const Pipeline) c_int {
+ const value = pipeline orelse return 0;
+ return @intFromBool(!value.synced.load(.acquire) or value.keyframe_pending.load(.acquire));
+}
+
+pub export fn go_video_pipeline_has_media(pipeline: ?*const Pipeline) c_int {
+ return @intFromBool(if (pipeline) |value| value.rtp_packets.load(.monotonic) > 0 else false);
+}
+
+pub export fn go_video_pipeline_failed(pipeline: ?*const Pipeline) c_int {
+ return @intFromBool(if (pipeline) |value| value.failed.load(.acquire) else false);
+}
+
+pub export fn go_video_pipeline_note_keyframe_request(pipeline: ?*Pipeline) void {
+ if (pipeline) |value| _ = value.keyframe_requests.fetchAdd(1, .monotonic);
+}
+
+pub export fn go_video_pipeline_stats(pipeline_pointer: ?*Pipeline) Stats {
+ const pipeline = pipeline_pointer orelse return .{};
+ var stats = Stats{
+ .rtp_packets = pipeline.rtp_packets.load(.monotonic),
+ .payload_packets = pipeline.payload_packets.load(.monotonic),
+ .rejected_packets = pipeline.rejected_packets.load(.monotonic),
+ .last_rejected_payload_type = pipeline.last_rejected_payload_type.load(.monotonic),
+ .access_units = pipeline.access_units.load(.monotonic),
+ .decoded_frames = pipeline.decoded_frames.load(.monotonic),
+ .rendered_frames = pipeline.rendered_frames.load(.monotonic),
+ .source_width = pipeline.source_width.load(.monotonic),
+ .source_height = pipeline.source_height.load(.monotonic),
+ .frame_nals = pipeline.frame_nals.load(.monotonic),
+ .idr_nals = pipeline.idr_nals.load(.monotonic),
+ .parameter_nals = pipeline.parameter_nals.load(.monotonic),
+ .auxiliary_nals = pipeline.auxiliary_nals.load(.monotonic),
+ .last_timestamp = pipeline.last_timestamp.load(.monotonic),
+ .synced = @intFromBool(pipeline.synced.load(.acquire)),
+ .discontinuities = pipeline.discontinuities.load(.monotonic),
+ .missing_packets = pipeline.missing_packets.load(.monotonic),
+ .late_packets = pipeline.late_packets.load(.monotonic),
+ .decoder_send_errors = pipeline.decoder_send_errors.load(.monotonic),
+ .decoder_receive_errors = pipeline.decoder_receive_errors.load(.monotonic),
+ .last_decoder_error = pipeline.last_decoder_error.load(.monotonic),
+ .decoder_backend = @intCast(pipeline.decoder_backend.load(.monotonic)),
+ .decoder_init_failures = pipeline.decoder_init_failures.load(.monotonic),
+ .decoder_runtime_fallbacks = pipeline.decoder_runtime_fallbacks.load(.monotonic),
+ .decoder_backpressure_events = pipeline.decoder_backpressure_events.load(.monotonic),
+ .decoder_corrupt_frames = pipeline.decoder_corrupt_frames.load(.monotonic),
+ .decoder_info_changes = pipeline.decoder_info_changes.load(.monotonic),
+ .keyframe_requests = pipeline.keyframe_requests.load(.monotonic),
+ .dropped_packets = pipeline.dropped_packets.load(.monotonic),
+ };
+ pipeline.packet_mutex.lock();
+ stats.pending_packets = @intCast(pipeline.packet_queue_count);
+ pipeline.packet_mutex.unlock();
+ return stats;
+}
+
+fn freeFrame(frame: *?*c.AVFrame) void {
+ if (frame.* != null) c.av_frame_free(@ptrCast(frame));
+}
+
+pub export fn go_video_pipeline_destroy(pipeline_pointer: ?*Pipeline) c_int {
+ const pipeline = pipeline_pointer orelse return 0;
+ go_video_pipeline_stop(pipeline);
+ const persist_result = persistBootstrap(pipeline);
+ pipeline.frame_mutex.lock();
+ pipeline.frame_ready.store(false, .release);
+ if (pipeline.display_frame) |frame| c.av_frame_unref(frame);
+ pipeline.frame_mutex.unlock();
+ freeFrame(&pipeline.render_frame);
+ freeFrame(&pipeline.display_frame);
+ if (pipeline.scaler) |scaler| c.sws_freeContext(scaler);
+ c.free(pipeline.rgb_buffer);
+ destroyTexture(pipeline);
+ c.go_video_decoder_selection_destroy(&pipeline.decoders);
+ freeFrame(&pipeline.decoded_frame);
+ c.go_h264_depacketizer_destroy(pipeline.depacketizer);
+ std.heap.c_allocator.destroy(pipeline);
+ return persist_result;
+}
diff --git a/src/net/form_writer.h b/src/net/form_writer.h
deleted file mode 100644
index adf4f00..0000000
--- a/src/net/form_writer.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#ifndef GREENOVERCAST_FORM_WRITER_H
-#define GREENOVERCAST_FORM_WRITER_H
-
-#include
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-int go_form_urlencode(const char* input, size_t input_length, char* output,
- size_t output_capacity);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/src/net/form_writer.zig b/src/net/form_writer.zig
index 62feaf9..7f73d4a 100644
--- a/src/net/form_writer.zig
+++ b/src/net/form_writer.zig
@@ -14,7 +14,7 @@ fn encodedLength(input: []const u8) ?usize {
return length;
}
-fn encode(input: []const u8, output: []u8) !usize {
+pub fn encode(input: []const u8, output: []u8) !usize {
const required = encodedLength(input) orelse return error.Overflow;
if (output.len < required + 1) return error.NoSpace;
@@ -38,14 +38,6 @@ fn encode(input: []const u8, output: []u8) !usize {
return cursor;
}
-export fn go_form_urlencode(input: ?[*]const u8, input_length: usize, output: ?[*]u8, output_capacity: usize) callconv(.c) c_int {
- const input_pointer = input orelse return -1;
- const output_pointer = output orelse return -1;
- const length = encode(input_pointer[0..input_length], output_pointer[0..output_capacity]) catch
- return -1;
- return std.math.cast(c_int, length) orelse -1;
-}
-
test "encodes application form values and terminates them" {
var output: [256]u8 = undefined;
const input = "xboxlive.signin openid service::http://Passport.NET/?a=b&c=d";
diff --git a/src/net/http_client.c b/src/net/http_client.c
index d4be377..f0b51f0 100644
--- a/src/net/http_client.c
+++ b/src/net/http_client.c
@@ -1,15 +1,63 @@
#include "http_client.h"
#include
+#include
#include
#include
#include
+#include
#include "../util/log.h"
+#define DEFAULT_RESPONSE_LIMIT (16 * 1024 * 1024)
+
+typedef struct {
+ GoHttpResponse* response;
+ size_t limit;
+} ResponseWriter;
+
+typedef struct {
+ GoHttpCancelRequested requested;
+ void* context;
+} TransferCancel;
+
+static const char* find_ca_bundle(void) {
+ const char* environment_paths[] = {
+ getenv("CURL_CA_BUNDLE"),
+ getenv("SSL_CERT_FILE"),
+ };
+ size_t environment_count = sizeof(environment_paths) / sizeof(environment_paths[0]);
+ for (size_t index = 0; index < environment_count; index++) {
+ const char* path = environment_paths[index];
+ if (path && path[0] != '\0' && access(path, R_OK) == 0)
+ return path;
+ }
+
+ static const char* system_paths[] = {
+ "/etc/ssl/certs/ca-certificates.crt",
+ "/etc/ssl/cert.pem",
+ "/etc/ssl/cacert.pem",
+ "/etc/pki/tls/certs/ca-bundle.crt",
+ "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
+ };
+ size_t system_count = sizeof(system_paths) / sizeof(system_paths[0]);
+ for (size_t index = 0; index < system_count; index++) {
+ if (access(system_paths[index], R_OK) == 0)
+ return system_paths[index];
+ }
+ return NULL;
+}
+
static size_t append_response(char* data, size_t size, size_t count, void* context) {
- GoHttpResponse* response = context;
+ ResponseWriter* writer = context;
+ GoHttpResponse* response = writer->response;
+ if (count != 0 && size > SIZE_MAX / count)
+ return 0;
size_t length = size * count;
+ if (length > writer->limit || response->len > writer->limit - length)
+ return 0;
+ if (length == SIZE_MAX || response->len > SIZE_MAX - length - 1)
+ return 0;
char* next = realloc(response->data, response->len + length + 1);
if (!next)
return 0;
@@ -20,6 +68,17 @@ static size_t append_response(char* data, size_t size, size_t count, void* conte
return length;
}
+static int transfer_cancelled(void* context, curl_off_t download_total,
+ curl_off_t download_current, curl_off_t upload_total,
+ curl_off_t upload_current) {
+ (void)download_total;
+ (void)download_current;
+ (void)upload_total;
+ (void)upload_current;
+ TransferCancel* cancel = context;
+ return cancel->requested(cancel->context) != 0;
+}
+
void go_http_response_destroy(GoHttpResponse* response) {
if (!response)
return;
@@ -31,9 +90,12 @@ int go_http_response_succeeded(const GoHttpResponse* response) {
return response && response->status >= 200 && response->status < 300;
}
-GoHttpResponse* go_http_request(const char* method, const char* url, const char* body,
- const char** headers, int header_count) {
- if (!method || !url || header_count < 0 || (header_count > 0 && !headers))
+GoHttpResponse* go_http_request_bounded_cancelable(
+ const char* method, const char* url, const char* body, const char** headers,
+ int header_count, size_t response_limit, GoHttpCancelRequested cancel_requested,
+ void* cancel_context) {
+ if (!method || !url || response_limit == 0 || header_count < 0 ||
+ (header_count > 0 && !headers))
return NULL;
CURL* request = curl_easy_init();
@@ -68,14 +130,26 @@ GoHttpResponse* go_http_request(const char* method, const char* url, const char*
}
if (request_headers)
curl_easy_setopt(request, CURLOPT_HTTPHEADER, request_headers);
+ ResponseWriter writer = {.response = response, .limit = response_limit};
curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, append_response);
- curl_easy_setopt(request, CURLOPT_WRITEDATA, response);
+ curl_easy_setopt(request, CURLOPT_WRITEDATA, &writer);
curl_easy_setopt(request, CURLOPT_TIMEOUT, 30L);
+ curl_easy_setopt(request, CURLOPT_NOSIGNAL, 1L);
+ const char* ca_bundle = find_ca_bundle();
+ if (ca_bundle)
+ curl_easy_setopt(request, CURLOPT_CAINFO, ca_bundle);
+ TransferCancel cancel = {.requested = cancel_requested, .context = cancel_context};
+ if (cancel_requested) {
+ curl_easy_setopt(request, CURLOPT_NOPROGRESS, 0L);
+ curl_easy_setopt(request, CURLOPT_XFERINFOFUNCTION, transfer_cancelled);
+ curl_easy_setopt(request, CURLOPT_XFERINFODATA, &cancel);
+ }
CURLcode result = curl_easy_perform(request);
curl_easy_getinfo(request, CURLINFO_RESPONSE_CODE, &response->status);
if (result != CURLE_OK) {
- fprintf(stderr, "HTTP %s %s: %s\n", method, url, curl_easy_strerror(result));
+ if (result != CURLE_ABORTED_BY_CALLBACK)
+ fprintf(stderr, "HTTP %s %s: %s\n", method, url, curl_easy_strerror(result));
go_http_response_destroy(response);
response = NULL;
} else {
@@ -86,3 +160,16 @@ GoHttpResponse* go_http_request(const char* method, const char* url, const char*
curl_easy_cleanup(request);
return response;
}
+
+GoHttpResponse* go_http_request_bounded(const char* method, const char* url, const char* body,
+ const char** headers, int header_count,
+ size_t response_limit) {
+ return go_http_request_bounded_cancelable(method, url, body, headers, header_count,
+ response_limit, NULL, NULL);
+}
+
+GoHttpResponse* go_http_request(const char* method, const char* url, const char* body,
+ const char** headers, int header_count) {
+ return go_http_request_bounded(method, url, body, headers, header_count,
+ DEFAULT_RESPONSE_LIMIT);
+}
diff --git a/src/net/http_client.h b/src/net/http_client.h
index 619bd7b..1d23585 100644
--- a/src/net/http_client.h
+++ b/src/net/http_client.h
@@ -13,8 +13,17 @@ typedef struct {
long status;
} GoHttpResponse;
+typedef int (*GoHttpCancelRequested)(void* context);
+
GoHttpResponse* go_http_request(const char* method, const char* url, const char* body,
const char** headers, int header_count);
+GoHttpResponse* go_http_request_bounded(const char* method, const char* url, const char* body,
+ const char** headers, int header_count,
+ size_t response_limit);
+GoHttpResponse* go_http_request_bounded_cancelable(
+ const char* method, const char* url, const char* body, const char** headers,
+ int header_count, size_t response_limit, GoHttpCancelRequested cancel_requested,
+ void* cancel_context);
int go_http_response_succeeded(const GoHttpResponse* response);
void go_http_response_destroy(GoHttpResponse* response);
diff --git a/src/net/json_reader.h b/src/net/json_reader.h
deleted file mode 100644
index cfde37a..0000000
--- a/src/net/json_reader.h
+++ /dev/null
@@ -1,36 +0,0 @@
-#ifndef GREENOVERCAST_JSON_READER_H
-#define GREENOVERCAST_JSON_READER_H
-
-#include
-#include
-#include
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-int go_json_copy_string(const char* data, size_t length, const char* key, char* output,
- size_t capacity);
-int go_json_unsigned(const char* data, size_t length, const char* key, unsigned int* output);
-
-// Heap-allocates a copy of the string value at `key`. Returns NULL when `data`
-// or `key` is NULL, the key is absent, or allocation fails. Caller frees it.
-static inline char* json_string(const char* data, const char* key) {
- if (!data || !key)
- return NULL;
- size_t capacity = strlen(data) + 1;
- char* value = malloc(capacity);
- if (!value)
- return NULL;
- if (go_json_copy_string(data, capacity - 1, key, value, capacity) < 0) {
- free(value);
- return NULL;
- }
- return value;
-}
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/src/net/json_reader.zig b/src/net/json_reader.zig
index 783e535..f57b39f 100644
--- a/src/net/json_reader.zig
+++ b/src/net/json_reader.zig
@@ -88,42 +88,20 @@ fn findUnsigned(
return null;
}
-fn parseString(data: []const u8, key: []const u8, output: []u8) !usize {
+pub fn parseString(data: []const u8, key: []const u8, output: []u8) !usize {
const parsed = try std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, data, .{});
defer parsed.deinit();
return try copyMatchingString(std.heap.page_allocator, parsed.value, key, output, 0) orelse
error.MissingField;
}
-fn parseUnsigned(data: []const u8, key: []const u8) !u32 {
+pub fn parseUnsigned(data: []const u8, key: []const u8) !u32 {
const parsed = try std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, data, .{});
defer parsed.deinit();
return try findUnsigned(std.heap.page_allocator, parsed.value, key, 0) orelse
error.MissingField;
}
-export fn go_json_copy_string(
- data: [*]const u8,
- length: usize,
- key: [*:0]const u8,
- output: [*]u8,
- capacity: usize,
-) c_int {
- const copied = parseString(data[0..length], std.mem.span(key), output[0..capacity]) catch
- return -1;
- return @intCast(copied);
-}
-
-export fn go_json_unsigned(
- data: [*]const u8,
- length: usize,
- key: [*:0]const u8,
- output: *c_uint,
-) c_int {
- output.* = parseUnsigned(data[0..length], std.mem.span(key)) catch return -1;
- return 0;
-}
-
test "copies decoded strings from direct and embedded JSON" {
var output: [128]u8 = undefined;
const direct =
diff --git a/src/net/json_writer.h b/src/net/json_writer.h
deleted file mode 100644
index 53d6f67..0000000
--- a/src/net/json_writer.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#ifndef GREENOVERCAST_JSON_WRITER_H
-#define GREENOVERCAST_JSON_WRITER_H
-
-#include
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-int go_json_escape_string(const char* input, size_t input_length, char* output,
- size_t output_capacity);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/src/net/json_writer.zig b/src/net/json_writer.zig
index 98f86dd..bb79491 100644
--- a/src/net/json_writer.zig
+++ b/src/net/json_writer.zig
@@ -13,7 +13,7 @@ fn escapedLength(input: []const u8) ?usize {
return length;
}
-fn escape(input: []const u8, output: []u8) !usize {
+pub fn escape(input: []const u8, output: []u8) !usize {
const required = escapedLength(input) orelse return error.Overflow;
if (output.len < required + 1) return error.NoSpace;
var cursor: usize = 0;
@@ -46,14 +46,6 @@ fn escape(input: []const u8, output: []u8) !usize {
return cursor;
}
-export fn go_json_escape_string(input: ?[*]const u8, input_length: usize, output: ?[*]u8, output_capacity: usize) callconv(.c) c_int {
- const input_pointer = input orelse return -1;
- const output_pointer = output orelse return -1;
- const length = escape(input_pointer[0..input_length], output_pointer[0..output_capacity]) catch
- return -1;
- return std.math.cast(c_int, length) orelse -1;
-}
-
test "escapes JSON string content and terminates it" {
var output: [128]u8 = undefined;
const input = "line 1\r\n\"line 2\"\\\t\x01";
diff --git a/src/platform/sdl_platform.c b/src/platform/sdl_platform.c
deleted file mode 100644
index 3d2d8d5..0000000
--- a/src/platform/sdl_platform.c
+++ /dev/null
@@ -1,125 +0,0 @@
-#include "sdl_platform.h"
-
-#include
-#include
-
-#include "../util/log.h"
-
-struct GoSdlPlatform {
- int sdl_initialized;
- SDL_Window* window;
- SDL_Renderer* renderer;
- SDL_AudioDeviceID audio_device;
- GoControllerInput* controller;
- GoHandheldUi* ui;
-};
-
-GoSdlPlatform* go_sdl_platform_create(GoUiStopRequested stop_requested, void* stop_context) {
- GoSdlPlatform* platform = calloc(1, sizeof(*platform));
- if (!platform)
- return NULL;
- if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) !=
- 0) {
- fprintf(stderr, "SDL_Init: %s\n", SDL_GetError());
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- platform->sdl_initialized = 1;
- SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
- platform->window =
- SDL_CreateWindow("GreenOvercast", 0, 0, 640, 480, SDL_WINDOW_FULLSCREEN);
- if (!platform->window) {
- fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError());
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- platform->renderer = SDL_CreateRenderer(platform->window, -1, SDL_RENDERER_ACCELERATED);
- if (!platform->renderer) {
- fprintf(stderr, "Accelerated renderer failed, trying software\n");
- platform->renderer = SDL_CreateRenderer(platform->window, -1, 0);
- }
- if (!platform->renderer) {
- fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError());
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- SDL_RendererInfo renderer_info;
- if (SDL_GetRendererInfo(platform->renderer, &renderer_info) == 0) {
- go_dbg("SDL2 renderer ready: %s%s\n",
- renderer_info.name ? renderer_info.name : "unknown",
- renderer_info.flags & SDL_RENDERER_ACCELERATED ? " (accelerated)" : "");
- }
- SDL_ShowCursor(0);
- SDL_SetRenderDrawColor(platform->renderer, 13, 35, 27, 255);
- SDL_RenderClear(platform->renderer);
- SDL_RenderPresent(platform->renderer);
-
- platform->controller = go_controller_input_create();
- if (!platform->controller) {
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- platform->ui = go_handheld_ui_create(platform->renderer, platform->controller,
- stop_requested, stop_context);
- if (!platform->ui) {
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- SDL_GameControllerEventState(SDL_ENABLE);
- SDL_JoystickEventState(SDL_ENABLE);
-
- SDL_AudioSpec wanted;
- SDL_zero(wanted);
- wanted.freq = 48000;
- wanted.format = AUDIO_S16SYS;
- wanted.channels = 2;
- wanted.samples = 960;
- SDL_AudioSpec obtained;
- platform->audio_device = SDL_OpenAudioDevice(NULL, 0, &wanted, &obtained, 0);
- if (!platform->audio_device) {
- fprintf(stderr, "SDL_OpenAudioDevice: %s\n", SDL_GetError());
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- if (obtained.freq != wanted.freq || obtained.format != wanted.format ||
- obtained.channels != wanted.channels) {
- fprintf(stderr, "Unsupported audio format: %d Hz, format 0x%x, %d channels\n",
- obtained.freq, obtained.format, obtained.channels);
- go_sdl_platform_destroy(platform);
- return NULL;
- }
- go_dbg("Audio: %d Hz stereo s16\n", obtained.freq);
- return platform;
-}
-
-SDL_Renderer* go_sdl_platform_renderer(const GoSdlPlatform* platform) {
- return platform ? platform->renderer : NULL;
-}
-
-SDL_AudioDeviceID go_sdl_platform_audio_device(const GoSdlPlatform* platform) {
- return platform ? platform->audio_device : 0;
-}
-
-GoControllerInput* go_sdl_platform_controller(const GoSdlPlatform* platform) {
- return platform ? platform->controller : NULL;
-}
-
-GoHandheldUi* go_sdl_platform_ui(const GoSdlPlatform* platform) {
- return platform ? platform->ui : NULL;
-}
-
-void go_sdl_platform_destroy(GoSdlPlatform* platform) {
- if (!platform)
- return;
- go_handheld_ui_destroy(platform->ui);
- go_controller_input_destroy(platform->controller);
- if (platform->audio_device)
- SDL_CloseAudioDevice(platform->audio_device);
- if (platform->renderer)
- SDL_DestroyRenderer(platform->renderer);
- if (platform->window)
- SDL_DestroyWindow(platform->window);
- if (platform->sdl_initialized)
- SDL_Quit();
- free(platform);
-}
diff --git a/src/platform/sdl_platform.zig b/src/platform/sdl_platform.zig
new file mode 100644
index 0000000..45454db
--- /dev/null
+++ b/src/platform/sdl_platform.zig
@@ -0,0 +1,142 @@
+const std = @import("std");
+
+const c = @cImport({
+ @cInclude("SDL2/SDL.h");
+ @cInclude("controller.h");
+ @cInclude("handheld_ui.h");
+});
+
+const StopRequested = ?*const fn (?*anyopaque) callconv(.c) c_int;
+
+const Platform = struct {
+ sdl_initialized: bool = false,
+ window: ?*c.SDL_Window = null,
+ renderer: ?*c.SDL_Renderer = null,
+ audio_device: c.SDL_AudioDeviceID = 0,
+ controller: ?*c.GoControllerInput = null,
+ ui: ?*c.GoHandheldUi = null,
+};
+
+fn debugEnabled() bool {
+ return std.posix.getenv("GREENOVERCAST_DEBUG") != null;
+}
+
+fn sdlError(comptime operation: []const u8) void {
+ std.debug.print("{s}: {s}\n", .{ operation, std.mem.span(c.SDL_GetError()) });
+}
+
+pub export fn go_sdl_platform_create(
+ stop_requested: StopRequested,
+ stop_context: ?*anyopaque,
+) ?*Platform {
+ const platform = std.heap.c_allocator.create(Platform) catch return null;
+ platform.* = .{};
+ var created = false;
+ defer if (!created) go_sdl_platform_destroy(platform);
+
+ if (c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO | c.SDL_INIT_JOYSTICK | c.SDL_INIT_GAMECONTROLLER) != 0) {
+ sdlError("SDL_Init");
+ return null;
+ }
+ platform.sdl_initialized = true;
+ _ = c.SDL_SetHint(c.SDL_HINT_RENDER_SCALE_QUALITY, "linear");
+ platform.window = c.SDL_CreateWindow(
+ "GreenOvercast",
+ 0,
+ 0,
+ 640,
+ 480,
+ c.SDL_WINDOW_FULLSCREEN,
+ );
+ if (platform.window == null) {
+ sdlError("SDL_CreateWindow");
+ return null;
+ }
+
+ platform.renderer = c.SDL_CreateRenderer(platform.window, -1, c.SDL_RENDERER_ACCELERATED);
+ if (platform.renderer == null) {
+ std.debug.print("Accelerated renderer failed, trying software\n", .{});
+ platform.renderer = c.SDL_CreateRenderer(platform.window, -1, 0);
+ }
+ if (platform.renderer == null) {
+ sdlError("SDL_CreateRenderer");
+ return null;
+ }
+
+ var renderer_info: c.SDL_RendererInfo = undefined;
+ if (debugEnabled() and c.SDL_GetRendererInfo(platform.renderer, &renderer_info) == 0) {
+ const name = if (renderer_info.name != null) std.mem.span(renderer_info.name) else "unknown";
+ std.debug.print("SDL2 renderer ready: {s}{s}\n", .{
+ name,
+ if (renderer_info.flags & c.SDL_RENDERER_ACCELERATED != 0) " (accelerated)" else "",
+ });
+ }
+ _ = c.SDL_ShowCursor(0);
+ _ = c.SDL_SetRenderDrawColor(platform.renderer, 13, 35, 27, 255);
+ _ = c.SDL_RenderClear(platform.renderer);
+ c.SDL_RenderPresent(platform.renderer);
+
+ platform.controller = c.go_controller_input_create();
+ if (platform.controller == null) return null;
+ platform.ui = c.go_handheld_ui_create(
+ platform.renderer,
+ platform.controller,
+ stop_requested,
+ stop_context,
+ );
+ if (platform.ui == null) return null;
+ _ = c.SDL_GameControllerEventState(c.SDL_ENABLE);
+ _ = c.SDL_JoystickEventState(c.SDL_ENABLE);
+
+ var wanted: c.SDL_AudioSpec = std.mem.zeroes(c.SDL_AudioSpec);
+ wanted.freq = 48000;
+ wanted.format = c.AUDIO_S16SYS;
+ wanted.channels = 2;
+ wanted.samples = 960;
+ var obtained: c.SDL_AudioSpec = undefined;
+ platform.audio_device = c.SDL_OpenAudioDevice(null, 0, &wanted, &obtained, 0);
+ if (platform.audio_device == 0) {
+ sdlError("SDL_OpenAudioDevice");
+ return null;
+ }
+ if (obtained.freq != wanted.freq or obtained.format != wanted.format or
+ obtained.channels != wanted.channels)
+ {
+ std.debug.print("Unsupported audio format: {d} Hz, format 0x{x}, {d} channels\n", .{
+ obtained.freq,
+ obtained.format,
+ obtained.channels,
+ });
+ return null;
+ }
+ if (debugEnabled()) std.debug.print("Audio: {d} Hz stereo s16\n", .{obtained.freq});
+ created = true;
+ return platform;
+}
+
+pub export fn go_sdl_platform_renderer(platform: ?*const Platform) ?*c.SDL_Renderer {
+ return if (platform) |value| value.renderer else null;
+}
+
+pub export fn go_sdl_platform_audio_device(platform: ?*const Platform) c.SDL_AudioDeviceID {
+ return if (platform) |value| value.audio_device else 0;
+}
+
+pub export fn go_sdl_platform_controller(platform: ?*const Platform) ?*c.GoControllerInput {
+ return if (platform) |value| value.controller else null;
+}
+
+pub export fn go_sdl_platform_ui(platform: ?*const Platform) ?*c.GoHandheldUi {
+ return if (platform) |value| value.ui else null;
+}
+
+pub export fn go_sdl_platform_destroy(platform: ?*Platform) void {
+ const value = platform orelse return;
+ c.go_handheld_ui_destroy(value.ui);
+ c.go_controller_input_destroy(value.controller);
+ if (value.audio_device != 0) c.SDL_CloseAudioDevice(value.audio_device);
+ if (value.renderer) |renderer| c.SDL_DestroyRenderer(renderer);
+ if (value.window) |window| c.SDL_DestroyWindow(window);
+ if (value.sdl_initialized) c.SDL_Quit();
+ std.heap.c_allocator.destroy(value);
+}
diff --git a/src/session/cloud_session.zig b/src/session/cloud_session.zig
index c5f5d29..b28f153 100644
--- a/src/session/cloud_session.zig
+++ b/src/session/cloud_session.zig
@@ -1,20 +1,22 @@
const std = @import("std");
+const json = @import("json_reader");
const c = @cImport({
@cInclude("handheld_ui.h");
@cInclude("http_client.h");
- @cInclude("json_reader.h");
@cInclude("xbox_auth.h");
});
const base_url = "https://weu.core.gssv-play-prod.xboxlive.com";
-const device_info_header =
+const device_info_prefix =
"X-MS-Device-Info: {\"appInfo\":{\"env\":{\"clientAppId\":\"www.xbox.com\"," ++
"\"clientAppType\":\"browser\",\"clientAppVersion\":\"26.1.97\"," ++
"\"clientSdkVersion\":\"10.3.7\",\"httpEnvironment\":\"prod\",\"sdkInstallId\":\"\"}}," ++
"\"dev\":{\"hw\":{\"make\":\"Microsoft\",\"model\":\"unknown\",\"sdktype\":\"web\"}," ++
"\"os\":{\"name\":\"android\",\"ver\":\"22631.2715\",\"platform\":\"desktop\"}," ++
- "\"displayInfo\":{\"dimensions\":{\"widthInPixels\":1280,\"heightInPixels\":720}," ++
+ "\"displayInfo\":{\"dimensions\":{\"widthInPixels\":";
+const device_info_between_dimensions = ",\"heightInPixels\":";
+const device_info_suffix = "}," ++
"\"pixelDensity\":{\"dpiX\":1,\"dpiY\":1}},\"browser\":{\"browserName\":\"chrome\"," ++
"\"browserVersion\":\"140.0.3485.54\"}}}";
@@ -49,10 +51,9 @@ fn responseData(response: [*c]c.GoHttpResponse) ?[]const u8 {
return response.*.data[0..response.*.len];
}
-fn jsonString(data: []const u8, key: [*:0]const u8, output: []u8) ![]const u8 {
- const length = c.go_json_copy_string(data.ptr, data.len, key, output.ptr, output.len);
- if (length < 0) return error.MissingField;
- return output[0..@intCast(length)];
+fn jsonString(data: []const u8, key: []const u8, output: []u8) ![]const u8 {
+ const length = try json.parseString(data, key, output);
+ return output[0..length];
}
fn debug(comptime format: []const u8, args: anytype) void {
@@ -76,11 +77,23 @@ fn request(
"Authorization: Bearer {s}",
.{token},
) catch return null;
+ var device_info_buffer: [1024]u8 = undefined;
+ const device_info_header = std.fmt.bufPrintZ(
+ &device_info_buffer,
+ "{s}{d}{s}{d}{s}",
+ .{
+ device_info_prefix,
+ c.go_handheld_ui_stream_width(session.ui),
+ device_info_between_dimensions,
+ c.go_handheld_ui_stream_height(session.ui),
+ device_info_suffix,
+ },
+ ) catch return null;
var headers: [8][*c]const u8 = undefined;
headers[0] = auth_header.ptr;
headers[1] = "Accept: application/json";
headers[2] = "x-gssv-client: XboxComBrowser";
- headers[3] = device_info_header;
+ headers[3] = device_info_header.ptr;
var header_count: usize = 4;
var index: usize = 0;
while (index < @as(usize, @intCast(extra_header_count))) : (index += 1) {
diff --git a/src/session/message_protocol.zig b/src/session/message_protocol.zig
new file mode 100644
index 0000000..06662d1
--- /dev/null
+++ b/src/session/message_protocol.zig
@@ -0,0 +1,57 @@
+const std = @import("std");
+
+const disconnect_target = "/streaming/sessionLifetimeManagement/serverInitiatedDisconnect";
+
+pub fn buildDisconnectAck(message: []const u8, output: []u8) !?usize {
+ const json = std.mem.trimRight(u8, message, "\x00");
+ const parsed = try std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, json, .{});
+ defer parsed.deinit();
+ const object = switch (parsed.value) {
+ .object => |value| value,
+ else => return null,
+ };
+ const target = switch (object.get("target") orelse return null) {
+ .string => |value| value,
+ else => return null,
+ };
+ if (!std.mem.eql(u8, target, disconnect_target)) return null;
+ const id = switch (object.get("id") orelse return error.MissingTransactionId) {
+ .string => |value| value,
+ else => return error.MissingTransactionId,
+ };
+ if (id.len == 0) return error.MissingTransactionId;
+
+ var stream = std.io.fixedBufferStream(output);
+ const writer = stream.writer();
+ try writer.writeAll("{\"type\":\"TransactionComplete\",\"content\":\"\\\"\\\"\",\"id\":");
+ try std.json.encodeJsonString(id, .{}, writer);
+ try writer.writeAll(",\"cv\":\"\"}");
+ if (stream.pos >= output.len) return error.NoSpaceLeft;
+ output[stream.pos] = 0;
+ return stream.pos;
+}
+
+test "builds the disconnect transaction acknowledgement" {
+ const message =
+ "{\"type\":\"TransactionStart\",\"target\":\"/streaming/sessionLifetimeManagement/" ++
+ "serverInitiatedDisconnect\",\"id\":\"quit-\\\"1\",\"content\":\"\"}\x00";
+ var output: [512]u8 = undefined;
+ const length = (try buildDisconnectAck(message, &output)).?;
+ try std.testing.expectEqualStrings(
+ "{\"type\":\"TransactionComplete\",\"content\":\"\\\"\\\"\",\"id\":\"quit-\\\"1\",\"cv\":\"\"}",
+ output[0..length],
+ );
+ try std.testing.expectEqual(@as(u8, 0), output[length]);
+}
+
+test "ignores unrelated messages and rejects missing ids" {
+ var output: [256]u8 = undefined;
+ try std.testing.expect((try buildDisconnectAck("{\"target\":\"other\"}", &output)) == null);
+ try std.testing.expectError(
+ error.MissingTransactionId,
+ buildDisconnectAck(
+ "{\"target\":\"/streaming/sessionLifetimeManagement/serverInitiatedDisconnect\"}",
+ &output,
+ ),
+ );
+}
diff --git a/src/session/webrtc_session.c b/src/session/webrtc_session.c
deleted file mode 100644
index 8ee679b..0000000
--- a/src/session/webrtc_session.c
+++ /dev/null
@@ -1,676 +0,0 @@
-#include "webrtc_session.h"
-
-#include