diff --git a/README.md b/README.md index a581aff..db6fdbb 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,111 @@ cargo build --release --no-default-features --features eim cargo cache -a ``` +#### Building Multiple Plugin Variants + +The plugin supports building multiple variants that can coexist in the same GStreamer installation. This is useful when you need to run different models or configurations in the same pipeline. + +**Why PLUGIN_VARIANT?** + +GStreamer identifies plugins by three key attributes: +1. **Library filename**: The shared library file that contains the plugin +2. **Plugin name**: The internal plugin identifier registered with GStreamer +3. **Element names**: The names of individual elements (e.g., `edgeimpulsevideoinfer`) + +To allow multiple plugin builds to coexist, each variant must have unique values for all three. The `PLUGIN_VARIANT` environment variable automatically handles this by: + +- **Library naming**: After building, use the `rename-library.sh` script to rename the output library from `libgstedgeimpulse.{dylib,so,dll}` to `libgstedgeimpulse_{variant}.{dylib,so,dll}` +- **Plugin naming**: The plugin name becomes `gst-plugins-edgeimpulse_{variant}` instead of just `gst-plugins-edgeimpulse` +- **Element naming**: All elements are automatically suffixed with `_{variant}` (e.g., `edgeimpulsevideoinfer_variantX`, `edgeimpulseaudioinfer_variantX`, etc.) + +**Usage:** + +1. **Build with a variant:** + ```bash + # Build variant "variantX" + PLUGIN_VARIANT=variantX cargo build --release + + # After build completes, rename the library + PLUGIN_VARIANT=variantX ./rename-library.sh + ``` + +2. **Build multiple variants:** + ```bash + # Build first variant + PLUGIN_VARIANT=variantX \ + EI_MODEL=~/Downloads/model-a \ + EI_ENGINE=tflite \ + USE_FULL_TFLITE=1 \ + cargo build --release + PLUGIN_VARIANT=variantX ./rename-library.sh + + # Build second variant (with different model or configuration) + PLUGIN_VARIANT=variantY \ + EI_MODEL=~/Downloads/model-b \ + EI_ENGINE=tflite \ + USE_FULL_TFLITE=1 \ + cargo build --release + PLUGIN_VARIANT=variantY ./rename-library.sh + ``` + +3. **Use both variants in the same pipeline:** + ```bash + # Make sure both libraries are in GST_PLUGIN_PATH + export GST_PLUGIN_PATH="$(pwd)/target/release" + + # Use elements from both variants + gst-launch-1.0 \ + videotestsrc ! \ + edgeimpulsevideoinfer_variantX ! \ + edgeimpulseoverlay_variantX ! \ + queue ! \ + edgeimpulsevideoinfer_variantY ! \ + edgeimpulseoverlay_variantY ! \ + autovideosink + ``` + +**Technical Details:** + +- The `PLUGIN_VARIANT` environment variable must be set during both the build and rename steps +- The `rename-library.sh` script renames the output library from `libgstedgeimpulse.{dylib,so,dll}` to `libgstedgeimpulse_{variant}.{dylib,so,dll}` +- Each variant produces a uniquely named library file, allowing GStreamer to load multiple variants simultaneously +- Element names include the variant suffix, preventing naming conflicts when multiple variants are loaded + +**Example Workflow:** + +```bash +# Build variant for model A +PLUGIN_VARIANT=person-detection \ + EI_MODEL=~/Downloads/person-detection-v140 \ + EI_ENGINE=tflite \ + USE_FULL_TFLITE=1 \ + cargo build --release +PLUGIN_VARIANT=person-detection ./rename-library.sh + +# Build variant for model B +PLUGIN_VARIANT=anomaly-detection \ + EI_MODEL=~/Downloads/anomaly-detection-v50 \ + EI_ENGINE=tflite \ + USE_FULL_TFLITE=1 \ + cargo build --release +PLUGIN_VARIANT=anomaly-detection ./rename-library.sh + +# Both libraries will be in target/release: +# - libgstedgeimpulse_person-detection.dylib +# - libgstedgeimpulse_anomaly-detection.dylib + +# Use both in a pipeline +export GST_PLUGIN_PATH="$(pwd)/target/release" +gst-launch-1.0 \ + videotestsrc ! \ + edgeimpulsevideoinfer_person-detection ! \ + edgeimpulseoverlay_person-detection ! \ + queue ! \ + edgeimpulsevideoinfer_anomaly-detection ! \ + edgeimpulseoverlay_anomaly-detection ! \ + autovideosink +``` + #### Environment Variables **Required for FFI Mode:** @@ -272,11 +377,11 @@ cargo build --release --no-default-features --features eim **Platform-Specific Variables:** - `TARGET`: Standard Rust target triple (e.g., `aarch64-unknown-linux-gnu`, `x86_64-apple-darwin`) -- `TARGET_MAC_ARM64=1`: Build for Apple Silicon (M1/M2/M3) - legacy -- `TARGET_MAC_X86_64=1`: Build for Intel Mac - legacy -- `TARGET_LINUX_X86=1`: Build for Linux x86_64 - legacy -- `TARGET_LINUX_AARCH64=1`: Build for Linux ARM64 - legacy -- `TARGET_LINUX_ARMV7=1`: Build for Linux ARMv7 - legacy +- `TARGET_MAC_ARM64=1`: Build for Apple Silicon (M1/M2/M3) +- `TARGET_MAC_X86_64=1`: Build for Intel Mac +- `TARGET_LINUX_X86=1`: Build for Linux x86_64 +- `TARGET_LINUX_AARCH64=1`: Build for Linux ARM64 +- `TARGET_LINUX_ARMV7=1`: Build for Linux ARMv7 **Example:** ```bash diff --git a/build.rs b/build.rs index df1e8da..e8639de 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,5 @@ +use std::env; + fn main() { // Set PKG_CONFIG_PATH to include all necessary directories for macOS if cfg!(target_os = "macos") { @@ -8,7 +10,7 @@ fn main() { format!("{}/share/pkgconfig", homebrew_prefix), ]; - let existing_path = std::env::var("PKG_CONFIG_PATH").unwrap_or_default(); + let existing_path = env::var("PKG_CONFIG_PATH").unwrap_or_default(); let new_path = if existing_path.is_empty() { pkg_config_paths.join(":") } else { @@ -16,9 +18,117 @@ fn main() { }; println!("cargo:warning=Setting PKG_CONFIG_PATH to: {}", new_path); - std::env::set_var("PKG_CONFIG_PATH", &new_path); + env::set_var("PKG_CONFIG_PATH", &new_path); println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH"); } + // Set PLUGIN_VARIANT for plugin/element name suffixing + let plugin_variant = env::var("PLUGIN_VARIANT").unwrap_or_else(|_| "".to_string()); + println!("cargo:rustc-env=PLUGIN_VARIANT={}", plugin_variant); + + // Generate the full plugin name + // GStreamer plugin names should use hyphens, not underscores + let pkg_name = env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME should be set by Cargo"); + let plugin_name = if plugin_variant.is_empty() { + pkg_name + } else { + format!("{}-{}", pkg_name, plugin_variant) + }; + println!("cargo:rustc-env=PLUGIN_NAME={}", plugin_name); + + // Generate the plugin identifier for gst::plugin_define! + // GStreamer derives the registration function name from the library filename, not the plugin name + // For libgstedgeimpulse_banana.dylib, it expects gst_plugin_edgeimpulse_banana_register + // So we strip "libgst" and ".dylib" from the library name to get the identifier + let lib_name = if plugin_variant.is_empty() { + "edgeimpulse".to_string() + } else { + // Convert variant to a valid identifier (replace any non-alphanumeric with underscore) + let variant_ident = plugin_variant + .chars() + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect::(); + format!("edgeimpulse_{}", variant_ident) + }; + let plugin_identifier = lib_name; + + // Generate type names for GObject types to make them unique per variant + // This prevents type registration conflicts when multiple variants are loaded + let type_suffix = if plugin_variant.is_empty() { + "".to_string() + } else { + // Convert variant to a valid identifier for type names (capitalize first letter) + let variant_capitalized = plugin_variant + .chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().collect::() + } else if c.is_alphanumeric() || c == '_' { + c.to_string() + } else { + "_".to_string() + } + }) + .collect::(); + variant_capitalized + }; + + // Generate the plugin definition code with the correct identifier + // This allows the registration function name to match what GStreamer expects + let out_dir = env::var("OUT_DIR").unwrap(); + let plugin_def_path = std::path::Path::new(&out_dir).join("plugin_define.rs"); + let plugin_def_code = format!( + r#"gst::plugin_define!( + {}, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + env!("CARGO_PKG_VERSION"), + "MIT/X11", + env!("PLUGIN_NAME"), + env!("PLUGIN_NAME"), + "https://github.com/edgeimpulse/gst-plugins-edgeimpulse", + env!("BUILD_REL_DATE") +); +"#, + plugin_identifier + ); + std::fs::write(&plugin_def_path, plugin_def_code) + .expect("Failed to write plugin definition file"); + + // Generate type name constants for each element type + // This prevents type registration conflicts when multiple variants are loaded + let type_names_path = std::path::Path::new(&out_dir).join("type_names.rs"); + let type_names_code = format!( + r#"// Auto-generated type names for variant: {} +#[allow(dead_code)] +pub const VIDEO_INFER_TYPE_NAME: &str = "EdgeImpulseVideoInfer{}"; +#[allow(dead_code)] +pub const AUDIO_INFER_TYPE_NAME: &str = "EdgeImpulseAudioInfer{}"; +#[allow(dead_code)] +pub const OVERLAY_TYPE_NAME: &str = "EdgeImpulseOverlay{}"; +#[allow(dead_code)] +pub const SINK_TYPE_NAME: &str = "GstEdgeImpulseSink{}"; +"#, + plugin_variant, type_suffix, type_suffix, type_suffix, type_suffix + ); + std::fs::write(&type_names_path, type_names_code).expect("Failed to write type names file"); + + if !plugin_variant.is_empty() { + println!("cargo:warning=PLUGIN_VARIANT is set to: {}", plugin_variant); + println!("cargo:warning=Plugin name: {}", plugin_name); + println!("cargo:warning=Plugin identifier: {}", plugin_identifier); + println!( + "cargo:warning=After build completes, run: PLUGIN_VARIANT={} ./rename-library.sh", + plugin_variant + ); + } + gst_plugin_version_helper::info() } diff --git a/rename-library.sh b/rename-library.sh new file mode 100755 index 0000000..f7d459f --- /dev/null +++ b/rename-library.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Rename the output library to include PLUGIN_VARIANT suffix +# This allows multiple plugin variants to coexist in the same GStreamer installation +# +# Usage: +# PLUGIN_VARIANT=variantX ./rename-library.sh +# Or after building: cargo build --release && PLUGIN_VARIANT=variantX ./rename-library.sh + +set -e + +if [ -z "$PLUGIN_VARIANT" ]; then + echo "Error: PLUGIN_VARIANT environment variable is not set" + echo "Usage: PLUGIN_VARIANT=variantX ./rename-library.sh" + exit 1 +fi + +# Determine the library extension based on the platform +if [[ "$OSTYPE" == "darwin"* ]]; then + LIB_EXT="dylib" +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + LIB_EXT="so" +elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + LIB_EXT="dll" +else + LIB_EXT="so" # Default fallback +fi + +# Try both debug and release directories +for PROFILE in debug release; do + BUILD_DIR="target/$PROFILE" + ORIGINAL_LIB="libgstedgeimpulse.${LIB_EXT}" + NEW_LIB="libgstedgeimpulse_${PLUGIN_VARIANT}.${LIB_EXT}" + ORIGINAL_D="libgstedgeimpulse.d" + NEW_D="libgstedgeimpulse_${PLUGIN_VARIANT}.d" + + if [ -f "$BUILD_DIR/$ORIGINAL_LIB" ]; then + echo "Renaming $ORIGINAL_LIB to $NEW_LIB in $BUILD_DIR" + mv "$BUILD_DIR/$ORIGINAL_LIB" "$BUILD_DIR/$NEW_LIB" + + if [ -f "$BUILD_DIR/$ORIGINAL_D" ]; then + mv "$BUILD_DIR/$ORIGINAL_D" "$BUILD_DIR/$NEW_D" + fi + echo "Successfully renamed library to $NEW_LIB" + exit 0 + fi +done + +echo "Error: Library libgstedgeimpulse.${LIB_EXT} not found in target/debug or target/release" +echo "Please run 'cargo build' or 'cargo build --release' first" +exit 1 + diff --git a/src/audio/imp.rs b/src/audio/imp.rs index 74d56ca..f1bf108 100644 --- a/src/audio/imp.rs +++ b/src/audio/imp.rs @@ -100,9 +100,18 @@ use once_cell::sync::Lazy; use std::collections::VecDeque; use std::sync::Mutex; +// Include generated type names for variant-specific builds +include!(concat!(env!("OUT_DIR"), "/type_names.rs")); + static CAT: Lazy = Lazy::new(|| { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulseaudioinfer".to_string() + } else { + format!("edgeimpulseaudioinfer_{}", variant) + }; gst::DebugCategory::new( - "edgeimpulseaudioinfer", + &name, gst::DebugColorFlags::empty(), Some("Edge Impulse Audio Inference"), ) @@ -170,7 +179,7 @@ pub struct EdgeImpulseAudioInfer { #[glib::object_subclass] impl ObjectSubclass for EdgeImpulseAudioInfer { - const NAME: &'static str = "EdgeImpulseAudioInfer"; + const NAME: &'static str = AUDIO_INFER_TYPE_NAME; type Type = super::EdgeImpulseAudioInfer; type ParentType = gstreamer_base::BaseTransform; } diff --git a/src/audio/mod.rs b/src/audio/mod.rs index 70b9bf7..962d69c 100644 --- a/src/audio/mod.rs +++ b/src/audio/mod.rs @@ -17,9 +17,15 @@ unsafe impl Sync for EdgeImpulseAudioInfer {} // Register the type for our element pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulseaudioinfer".to_string() + } else { + format!("edgeimpulseaudioinfer_{}", variant) + }; gst::Element::register( Some(plugin), - "edgeimpulseaudioinfer", + &name, gst::Rank::NONE, EdgeImpulseAudioInfer::static_type(), ) diff --git a/src/lib.rs b/src/lib.rs index f27483b..846a80b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,14 +15,6 @@ fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { Ok(()) } -gst::plugin_define!( - edgeimpulse, - env!("CARGO_PKG_DESCRIPTION"), - plugin_init, - concat!(env!("CARGO_PKG_VERSION")), - "MIT/X11", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - "https://github.com/edgeimpulse/gst-plugins-edgeimpulse", - env!("BUILD_REL_DATE") -); +// Include the dynamically generated plugin definition from build.rs +// This ensures the registration function name matches what GStreamer expects +include!(concat!(env!("OUT_DIR"), "/plugin_define.rs")); diff --git a/src/overlay/imp.rs b/src/overlay/imp.rs index 0e1ad6d..b33eeda 100644 --- a/src/overlay/imp.rs +++ b/src/overlay/imp.rs @@ -62,8 +62,14 @@ use std::sync::Mutex; use crate::video::{VideoAnomalyMeta, VideoClassificationMeta}; static CAT: Lazy = Lazy::new(|| { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulseoverlay".to_string() + } else { + format!("edgeimpulseoverlay_{}", variant) + }; gst::DebugCategory::new( - "edgeimpulseoverlay", + &name, gst::DebugColorFlags::empty(), Some("Edge Impulse Overlay Element"), ) @@ -144,9 +150,12 @@ pub struct EdgeImpulseOverlay { label_colors: Mutex>, } +// Include generated type names for variant-specific builds +include!(concat!(env!("OUT_DIR"), "/type_names.rs")); + #[glib::object_subclass] impl ObjectSubclass for EdgeImpulseOverlay { - const NAME: &'static str = "EdgeImpulseOverlay"; + const NAME: &'static str = OVERLAY_TYPE_NAME; type Type = super::EdgeImpulseOverlay; type ParentType = gst_video::VideoFilter; } diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs index 4f7971d..d02862d 100644 --- a/src/overlay/mod.rs +++ b/src/overlay/mod.rs @@ -11,9 +11,15 @@ glib::wrapper! { // Plugin registration pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulseoverlay".to_string() + } else { + format!("edgeimpulseoverlay_{}", variant) + }; gst::Element::register( Some(plugin), - "edgeimpulseoverlay", + &name, gst::Rank::NONE, EdgeImpulseOverlay::static_type(), ) diff --git a/src/sink/imp.rs b/src/sink/imp.rs index 0c4b541..98f3721 100644 --- a/src/sink/imp.rs +++ b/src/sink/imp.rs @@ -15,8 +15,14 @@ use std::time::Instant; use tempfile::NamedTempFile; static CAT: Lazy = Lazy::new(|| { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulsesink".to_string() + } else { + format!("edgeimpulsesink_{}", variant) + }; gst::DebugCategory::new( - "edgeimpulsesink", + &name, gst::DebugColorFlags::empty(), Some("Edge Impulse Sink"), ) @@ -103,9 +109,12 @@ fn post_ingestion_error( let _ = obj.post_message(gst::message::Element::new(s)); } +// Include generated type names for variant-specific builds +include!(concat!(env!("OUT_DIR"), "/type_names.rs")); + #[glib::object_subclass] impl ObjectSubclass for EdgeImpulseSink { - const NAME: &'static str = "GstEdgeImpulseSink"; + const NAME: &'static str = SINK_TYPE_NAME; type Type = super::EdgeImpulseSink; type ParentType = gst_base::BaseSink; } diff --git a/src/sink/mod.rs b/src/sink/mod.rs index 9aa19fb..e31c199 100644 --- a/src/sink/mod.rs +++ b/src/sink/mod.rs @@ -46,9 +46,15 @@ unsafe impl Send for EdgeImpulseSink {} unsafe impl Sync for EdgeImpulseSink {} pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulsesink".to_string() + } else { + format!("edgeimpulsesink_{}", variant) + }; gst::Element::register( Some(plugin), - "edgeimpulsesink", + &name, gst::Rank::NONE, EdgeImpulseSink::static_type(), ) diff --git a/src/video/imp.rs b/src/video/imp.rs index e7af04e..c926131 100644 --- a/src/video/imp.rs +++ b/src/video/imp.rs @@ -196,6 +196,9 @@ //! ``` //! +// Include generated type names for variant-specific builds +include!(concat!(env!("OUT_DIR"), "/type_names.rs")); + use edge_impulse_runner::EdgeImpulseModel; use gstreamer as gst; use gstreamer::glib; @@ -215,8 +218,14 @@ use super::VideoAnomalyMeta; use super::VideoClassificationMeta; static CAT: Lazy = Lazy::new(|| { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulsevideoinfer".to_string() + } else { + format!("edgeimpulsevideoinfer_{}", variant) + }; gst::DebugCategory::new( - "edgeimpulsevideoinfer", + &name, gst::DebugColorFlags::empty(), Some("Edge Impulse Video Inference"), ) @@ -477,7 +486,7 @@ pub struct EdgeImpulseVideoInfer { // This macro implements ObjectSubclassType and other required traits #[glib::object_subclass] impl ObjectSubclass for EdgeImpulseVideoInfer { - const NAME: &'static str = "EdgeImpulseVideoInfer"; + const NAME: &'static str = VIDEO_INFER_TYPE_NAME; type Type = super::EdgeImpulseVideoInfer; type ParentType = gstreamer_base::BaseTransform; } diff --git a/src/video/mod.rs b/src/video/mod.rs index 2c2e45c..639c8cc 100644 --- a/src/video/mod.rs +++ b/src/video/mod.rs @@ -18,9 +18,15 @@ unsafe impl Send for EdgeImpulseVideoInfer {} unsafe impl Sync for EdgeImpulseVideoInfer {} pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + let variant = env!("PLUGIN_VARIANT"); + let name = if variant.is_empty() { + "edgeimpulsevideoinfer".to_string() + } else { + format!("edgeimpulsevideoinfer_{}", variant) + }; gst::Element::register( Some(plugin), - "edgeimpulsevideoinfer", + &name, gst::Rank::NONE, EdgeImpulseVideoInfer::static_type(), )