diff --git a/.gitignore b/.gitignore index 81a6064..ab5ef6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # ---> Zig -zig-cache/ -zig-out/ +.zig-cache/* +zig-out/* build/ build-*/ docgen_tmp/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..1a8273a --- /dev/null +++ b/build.zig @@ -0,0 +1,81 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + + const exe = b.addExecutable(.{ + .name = "zig-gst", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // These environment vars are set with the `nix develop` command + const gst_dev_path = b.graph.env_map.get("GST_DEV_PATH"); + const glib_dev_path = b.graph.env_map.get("GLIB_DEV_PATH"); + const glib_path = b.graph.env_map.get("GLIB_PATH"); + + if (gst_dev_path == null) { + std.debug.panic("GST_DEV_PATH environment variable was not set. Has the nix flake been updated? This should point at the gstreamer.XXX-dev folder", .{}); + } + + if (glib_dev_path == null) { + std.debug.panic("GLIB_DEV_PATH environment variable was not set. Has the nix flake been updated? This should point at the glib.XXX-dev folder", .{}); + } + + if (glib_path == null) { + std.debug.panic("GLIB_PATH environment variable was not set. Has the nix flake been updated? This should point at the glib.XXX folder", .{}); + } + + exe.linkSystemLibrary("c"); + exe.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include/gstreamer-1.0", .{gst_dev_path.?}) }); // not sure why both are needed, but + exe.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include/gstreamer-1.0/gst", .{gst_dev_path.?}) }); // it won't compile without them + exe.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include/glib-2.0", .{glib_dev_path.?}) }); + exe.addIncludePath(.{ .cwd_relative = b.fmt("{s}/lib/glib-2.0/include", .{glib_path.?}) }); + + + exe.linkSystemLibrary("gstreamer-1.0"); + exe.linkSystemLibrary("glib-2.0"); + exe.linkSystemLibrary("gobject-2.0"); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f736302 --- /dev/null +++ b/flake.lock @@ -0,0 +1,147 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1727802920, + "narHash": "sha256-HP89HZOT0ReIbI7IJZJQoJgxvB2Tn28V6XS3MNKnfLs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "27e30d177e57d912d614c88c622dcfdb2e6e6515", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1708161998, + "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "84d981bae8b5e783b3b548de505b22880559515f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "zig": "zig" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1727656237, + "narHash": "sha256-qfZ7nYqYmzPc3Ei9hvWRrFu3Mn3WJ2akyYvaoPOOHcM=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "5bdd678ad99cd0069dfa6a5822c1d82d45d34ad2", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4279eb1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + zig.url = "github:mitchellh/zig-overlay"; + flake-utils = { url = "github:numtide/flake-utils"; }; + }; + + outputs = { self, nixpkgs, zig, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem(system: + let + pkgs = import nixpkgs { + inherit system; + }; + zig_exe = zig.packages.${system}.master; + in + { + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + zig_exe + glib + glib.dev + gst_all_1.gstreamer + gst_all_1.gstreamer.dev + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + ]; + + GST_DEV_PATH = pkgs.gst_all_1.gstreamer.dev; + GLIB_DEV_PATH = pkgs.glib.dev; + GLIB_PATH = pkgs.glib.out; + }; + } + ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..1f562a3 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const gst = @cImport({ // glib-object for g_object_* functions + @cInclude("glib-object.h"); + @cInclude("gst.h"); + @cInclude("glib.h"); // and glib for other g_* functions +}); + +pub fn main() void { + // This allows me to utilize the same command line args and gstreamer + gst.gst_init(@ptrCast(&std.os.argv.len), @ptrCast(&std.os.argv.ptr)); + + const source: ?*gst.GstElement = gst.gst_element_factory_make("videotestsrc", "source"); + const sink: ?*gst.GstElement = gst.gst_element_factory_make("autovideosink", "sink"); + + const pipeline: ?*gst.GstElement = gst.gst_pipeline_new("test-pipeline"); + + if (source == null or sink == null or pipeline == null) { + std.debug.panic("Not all elements could be created!", .{}); + } + + // When you look into the GST_BIN macro that zig can't compile, + // it really is just this pointer cast with extra steps of verification + const bin: *gst.GstBin = @ptrCast(pipeline); + + + // Gstreamer gives a critical warning when using gst.gst_bin_add_many, but doesn't + // when calling each individually + _ = gst.gst_bin_add(bin, source); + _ = gst.gst_bin_add(bin, sink); + + // the failure return code is -1 I believe + if (gst.gst_element_link(source, sink) < 0) { + gst.gst_object_unref(pipeline); + std.debug.panic("Elements could not be linked\n", .{}); + } + + // g_int is just i32. You can + gst.g_object_set(source, "pattern", @as(i16, 0)); + + const ret = gst.gst_element_set_state(pipeline, gst.GST_STATE_PLAYING); + if (ret == gst.GST_STATE_CHANGE_FAILURE) { + gst.gst_object_unref(pipeline); + std.debug.panic("Could not start pipeline", .{}); + } + + const bus: *gst.GstBus = gst.gst_element_get_bus(pipeline); + const msg: *gst.GstMessage = gst.gst_bus_timed_pop_filtered( // This call holds until there is a valid message + bus, + gst.GST_CLOCK_TIME_NONE, + gst.GST_MESSAGE_ERROR | gst.GST_MESSAGE_EOS, + ); + + if (gst.GST_MESSAGE_TYPE(msg) == gst.GST_MESSAGE_ERROR) { + const err: [*c][*c]gst.GError = null; + var debug_info: ?*gst.gchar = null; + + switch (gst.GST_MESSAGE_TYPE(msg)) { + gst.GST_MESSAGE_ERROR => { + gst.gst_message_parse_error(msg, err, &debug_info); + std.debug.print("Error received from element {s}: {s}", .{ gst.GST_OBJECT_NAME(msg.src), err.*.*.message }); + if (debug_info != null) { // I couldn't figure out how to do a orelse statement for this unwrap. + std.debug.print("Debugging information: {s}", .{debug_info.?}); + } + gst.g_clear_error(err); + gst.g_free(debug_info); + }, + else => {}, + } + gst.gst_message_unref(msg); + } + + gst.gst_object_unref(bus); + _ = gst.gst_element_set_state(pipeline, gst.GST_STATE_NULL); + gst.gst_object_unref(pipeline); +}