feat(gst): Added gst a high level wrapper over gstreamer

chore(example): Added hdr-gstreamer-wgpu example
chore(license): Added MIT license to all crates
This commit is contained in:
uttarayan21
2025-12-16 02:23:30 +05:30
parent 6cc83ba655
commit 7f9152e8fd
26 changed files with 2815 additions and 53 deletions

1
gst/.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

62
gst/.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
checks-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- id: set-matrix
name: Generate Nix Matrix
run: |
set -Eeu
matrix="$(nix eval --json '.#githubActions.matrix')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
checks-build:
needs: checks-matrix
runs-on: ${{ matrix.os }}
strategy:
matrix: ${{fromJSON(needs.checks-matrix.outputs.matrix)}}
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build -L '.#${{ matrix.attr }}'
codecov:
runs-on: ubuntu-latest
permissions:
id-token: "write"
contents: "read"
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Run codecov
run: nix build .#checks.x86_64-linux.hello-llvm-cov
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
flags: unittests
name: codecov-hello
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
files: ./result
verbose: true

38
gst/.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: docs
on:
push:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
docs:
runs-on: ubuntu-latest
permissions:
id-token: "write"
contents: "read"
pages: "write"
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: DeterminateSystems/flake-checker-action@main
- name: Generate docs
run: nix build .#checks.x86_64-linux.hello-docs
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: result/share/doc
- name: Deploy to gh-pages
id: deployment
uses: actions/deploy-pages@v4

3
gst/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/result
/target
.direnv

1040
gst/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
gst/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "gst"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
error-stack = "0.6"
gstreamer = "0.24.4"
gstreamer-app = "0.24.4"
thiserror = "2.0"
tracing = "0.1"
wgpu = { version = "27.0.1", default-features = false }

136
gst/flake.lock generated Normal file
View File

@@ -0,0 +1,136 @@
{
"nodes": {
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1765465865,
"narHash": "sha256-jAyDD6FKEWZafIKN4KjzdQywcS/gR9sHz4zzjxefXcA=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "d0bdb37b2b1dc8a81f47e2042d59227b1f06473f",
"type": "github"
},
"original": {
"owner": "rustsec",
"repo": "advisory-db",
"type": "github"
}
},
"crane": {
"locked": {
"lastModified": 1765739568,
"narHash": "sha256-gQYx35Of4UDKUjAYvmxjUEh/DdszYeTtT6MDin4loGE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "67d2baff0f9f677af35db61b32b5df6863bcc075",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1737420293,
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1765472234,
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"advisory-db": "advisory-db",
"crane": "crane",
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1765766816,
"narHash": "sha256-m2au5a2x9L3ikyBi0g3/NRJSjmHVDvT42mn+O6FlyPs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "4f53a635709d82652567f51ef7af4365fbc0c88b",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

161
gst/flake.nix Normal file
View File

@@ -0,0 +1,161 @@
{
description = "A simple rust flake using rust-overlay and craneLib";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
crane.url = "github:ipetkov/crane";
nix-github-actions = {
url = "github:nix-community/nix-github-actions";
inputs.nixpkgs.follows = "nixpkgs";
};
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
advisory-db = {
url = "github:rustsec/advisory-db";
flake = false;
};
};
outputs = {
self,
crane,
flake-utils,
nixpkgs,
rust-overlay,
advisory-db,
nix-github-actions,
...
}:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlays.default
];
};
inherit (pkgs) lib;
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
name = cargoToml.package.name;
stableToolchain = pkgs.rust-bin.stable.latest.default;
stableToolchainWithLLvmTools = stableToolchain.override {
extensions = ["rust-src" "llvm-tools"];
};
stableToolchainWithRustAnalyzer = stableToolchain.override {
extensions = ["rust-src" "rust-analyzer"];
};
craneLib = (crane.mkLib pkgs).overrideToolchain stableToolchain;
craneLibLLvmTools = (crane.mkLib pkgs).overrideToolchain stableToolchainWithLLvmTools;
src = let
filterBySuffix = path: exts: lib.any (ext: lib.hasSuffix ext path) exts;
sourceFilters = path: type: (craneLib.filterCargoSources path type) || filterBySuffix path [".c" ".h" ".hpp" ".cpp" ".cc"];
in
lib.cleanSourceWith {
filter = sourceFilters;
src = ./.;
};
commonArgs =
{
inherit src;
pname = name;
stdenv = p: p.clangStdenv;
doCheck = false;
# LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
# LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
# SYSTEM_DEPS_LINK = "static";
# PKG_CONFIG_ALL_STATIC = "1";
GIO_EXTRA_MODULES = "${pkgs.glib-networking}/lib/gio/modules";
nativeBuildInputs = with pkgs; [
pkg-config
];
buildInputs = with pkgs;
[
gst_all_1.gst-libav
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-ugly
gst_all_1.gst-plugins-rs
gst_all_1.gstreamer
glib
glib-networking
]
++ (lib.optionals pkgs.stdenv.isDarwin [
libiconv
apple-sdk_26
]);
}
// (lib.optionalAttrs pkgs.stdenv.isLinux {
# BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.llvmPackages.libclang.lib}/lib/clang/18/include";
});
cargoArtifacts = craneLib.buildPackage commonArgs;
in {
checks =
{
"${name}-clippy" = craneLib.cargoClippy (commonArgs
// {
inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
});
"${name}-docs" = craneLib.cargoDoc (commonArgs // {inherit cargoArtifacts;});
"${name}-fmt" = craneLib.cargoFmt {inherit src;};
"${name}-toml-fmt" = craneLib.taploFmt {
src = pkgs.lib.sources.sourceFilesBySuffices src [".toml"];
};
# Audit dependencies
"${name}-audit" = craneLib.cargoAudit {
inherit src advisory-db;
};
# Audit licenses
"${name}-deny" = craneLib.cargoDeny {
inherit src;
};
"${name}-nextest" = craneLib.cargoNextest (commonArgs
// {
inherit cargoArtifacts;
partitions = 1;
partitionType = "count";
});
}
// lib.optionalAttrs (!pkgs.stdenv.isDarwin) {
"${name}-llvm-cov" = craneLibLLvmTools.cargoLlvmCov (commonArgs // {inherit cargoArtifacts;});
};
packages = let
pkg = craneLib.buildPackage (
commonArgs
// {inherit cargoArtifacts;}
);
in {
"${name}" = pkg;
default = pkg;
};
devShells = {
default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} (commonArgs
// {
packages = with pkgs;
[
stableToolchainWithRustAnalyzer
cargo-nextest
cargo-deny
]
++ (lib.optionals pkgs.stdenv.isDarwin [
apple-sdk_26
]);
});
};
}
)
// {
githubActions = nix-github-actions.lib.mkGithubMatrix {
checks = nixpkgs.lib.getAttrs ["x86_64-linux"] self.checks;
};
};
}

7
gst/src/errors.rs Normal file
View File

@@ -0,0 +1,7 @@
pub use error_stack::{Report, ResultExt};
#[derive(Debug, thiserror::Error)]
#[error("An error occurred")]
pub struct Error;
pub type Result<T, E = error_stack::Report<Error>> = core::result::Result<T, E>;

245
gst/src/lib.rs Normal file
View File

@@ -0,0 +1,245 @@
pub mod errors;
use errors::*;
use gstreamer::prelude::*;
use std::sync::Arc;
static GST: std::sync::LazyLock<std::sync::Arc<Gst>> = std::sync::LazyLock::new(|| {
gstreamer::init().expect("Failed to initialize GStreamer");
std::sync::Arc::new(Gst {
__private: core::marker::PhantomData,
})
});
/// This should be a global singleton
pub struct Gst {
__private: core::marker::PhantomData<()>,
}
impl Gst {
pub fn new() -> Arc<Self> {
Arc::clone(&GST)
}
pub fn pipeline_from_str(&self, s: &str) -> Result<Pipeline> {
let pipeline = gstreamer::parse::launch(s).change_context(Error)?;
let pipeline = pipeline.downcast::<gstreamer::Pipeline>();
let pipeline = match pipeline {
Err(_e) => return Err(Error).attach("Failed to downcast to Pipeline"),
Ok(p) => p,
};
Ok(Pipeline { pipeline })
}
}
pub struct Sink {
element: gstreamer::Element,
}
pub struct Pipeline {
pipeline: gstreamer::Pipeline,
}
impl core::fmt::Debug for Pipeline {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Pipeline")
.field("pipeline", &self.pipeline)
// .field("state", &self.pipeline.state(gstreamer::ClockTime::NONE))
.finish()
}
}
impl Drop for Pipeline {
fn drop(&mut self) {
let _ = self.pipeline.set_state(gstreamer::State::Null);
}
}
impl Pipeline {
pub fn bus(&self) -> Result<Bus> {
let bus = self
.pipeline
.bus()
.ok_or(Error)
.attach("Failed to get bus from pipeline")?;
Ok(Bus { bus })
}
pub fn set_state(&self, state: gstreamer::State) -> Result<gstreamer::StateChangeSuccess> {
let result = self
.pipeline
.set_state(state)
.change_context(Error)
.attach("Failed to set pipeline state")?;
Ok(result)
}
}
pub struct Bus {
bus: gstreamer::Bus,
}
impl Bus {
pub fn iter_timed(
&self,
timeout: impl Into<Option<gstreamer::ClockTime>>,
) -> gstreamer::bus::Iter<'_> {
self.bus.iter_timed(timeout)
}
}
/// Pads are link points between elements
pub struct Pad {
pad: gstreamer::Pad,
}
pub struct Element {
element: gstreamer::Element,
}
#[test]
fn gst_parse_pipeline() {
let gst = Gst::new();
let pipeline = gst
.pipeline_from_str("videotestsrc ! autovideosink")
.expect("Failed to create pipeline");
println!("{:?}", pipeline);
}
#[test]
fn gst_parse_invalid_pipeline() {
let gst = Gst::new();
let result = gst.pipeline_from_str("invalidpipeline");
assert!(result.is_err());
}
#[test]
fn gst_play_pipeline() {
let gst = Gst::new();
let pipeline = gst
.pipeline_from_str("videotestsrc ! autovideosink")
.expect("Failed to create pipeline");
let bus = pipeline.bus().expect("Failed to get bus from pipeline");
pipeline
.set_state(gstreamer::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
for msg in bus.iter_timed(gstreamer::ClockTime::NONE) {
use gstreamer::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
eprintln!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
}
}
pipeline
.set_state(gstreamer::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
}
#[test]
#[ignore]
fn gstreamer_unwrapped() {
gstreamer::init();
let uri = "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm";
let pipeline = gstreamer::parse::launch(&format!("playbin uri={}", uri)).unwrap();
use gstreamer::prelude::*;
pipeline.set_state(gstreamer::State::Playing).unwrap();
let bus = pipeline.bus().unwrap();
for msg in bus.iter_timed(gstreamer::ClockTime::NONE) {
use gstreamer::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
eprintln!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
}
}
pipeline.set_state(gstreamer::State::Null).unwrap();
}
#[test]
fn test_appsink() {
let gst = Gst::new();
let pipeline = gst
.pipeline_from_str(
"videotestsrc ! videoconvert | capsfilter name=video-filter ! appsink name=video-sink",
)
.expect("Failed to create pipeline");
// let video_sink = pipeline.
let bus = pipeline.bus().expect("Failed to get bus from pipeline");
let sink = pipeline
.pipeline
.by_name("video-sink")
.expect("Sink not found")
.downcast::<gstreamer_app::AppSink>()
.expect("Failed to downcast to AppSink");
let capsfilter = pipeline
.pipeline
.by_name("video-filter")
.expect("Capsfilter not found");
let caps = gstreamer::Caps::builder("video/x-raw")
.field("format", "RGBA")
.build();
capsfilter.set_property("caps", &caps);
sink.set_callbacks(
gstreamer_app::AppSinkCallbacks::builder()
.new_sample(|sink| {
// foo
Ok(gstreamer::FlowSuccess::Ok)
})
.build(),
);
// let appsink = sink
// .dynamic_cast::<gstreamer_app::AppSink>()
// .expect("Failed to cast to AppSink");
pipeline
.set_state(gstreamer::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
for msg in bus.iter_timed(gstreamer::ClockTime::NONE) {
use gstreamer::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
eprintln!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
break;
}
_ => (),
}
}
}

0
gst/src/wgpu.rs Normal file
View File