use core::future::Future;
use core::pin::Pin;
use g2g_core::memory::{DomainSet, MemoryDomainKind};
use g2g_core::{
AllocationParams, AsyncElement, Caps, CapsConstraint, CapsSet, ConfigureOutcome, Dim,
ElementMetadata, Frame, G2gError, OutputSink, PadTemplate, PadTemplates, PipelinePacket,
PropError, PropKind, PropValue, PropertySpec, Rate, RawVideoFormat,
};
use crate::format::{format_from_py, format_to_py, frame_bytes};
use crate::props::{fixed_caps, hosted_element_props};
#[derive(Debug)]
pub struct PyTransform {
module: String,
class: String,
accept: Caps,
produce: Option<Caps>,
draw_label: bool,
cuda_frames: bool,
params: Vec<(String, PropValue)>,
configured: bool,
fixed: Option<Caps>,
emitted: u64,
#[cfg(feature = "python")]
worker: Option<crate::host::PyWorker>,
}
impl PyTransform {
pub fn new(module: impl Into<String>, class: impl Into<String>) -> Self {
Self {
module: module.into(),
class: class.into(),
accept: Caps::RawVideo {
format: RawVideoFormat::Rgba8,
width: Dim::Any,
height: Dim::Any,
framerate: Rate::Any,
interlace: g2g_core::Interlace::Any,
},
produce: None,
draw_label: false,
cuda_frames: false,
params: Vec::new(),
configured: false,
fixed: None,
emitted: 0,
#[cfg(feature = "python")]
worker: None,
}
}
pub fn with_accept(mut self, caps: Caps) -> Self {
self.accept = caps;
self
}
pub fn with_produce(mut self, caps: Caps) -> Self {
self.produce = Some(caps);
self
}
fn output_for(&self, input: &Caps) -> Caps {
self.produce.clone().unwrap_or_else(|| input.clone())
}
pub fn with_draw_label(mut self, on: bool) -> Self {
self.draw_label = on;
self
}
pub fn with_cuda_frames(mut self, on: bool) -> Self {
self.cuda_frames = on;
self
}
fn domain(&self) -> MemoryDomainKind {
if self.cuda_frames {
MemoryDomainKind::Cuda
} else {
MemoryDomainKind::System
}
}
pub fn emitted_count(&self) -> u64 {
self.emitted
}
#[cfg(feature = "python")]
async fn run(&self, frame: Frame) -> Result<Vec<Frame>, G2gError> {
let worker = self.worker.as_ref().ok_or(G2gError::NotConfigured)?;
let caps = self.fixed.as_ref().ok_or(G2gError::NotConfigured)?;
worker.run(frame, caps).await
}
#[cfg(not(feature = "python"))]
async fn run(&self, _frame: Frame) -> Result<Vec<Frame>, G2gError> {
Err(G2gError::UnsupportedDomain)
}
}
impl AsyncElement for PyTransform {
type ProcessFuture<'a>
= Pin<Box<dyn Future<Output = Result<(), G2gError>> + 'a>>
where
Self: 'a;
fn caps_constraint_as_transform(&self) -> CapsConstraint<'_> {
if let Some(produce) = self.produce.clone() {
return CapsConstraint::Mapping(vec![(
CapsSet::one(self.accept.clone()),
CapsSet::one(produce),
)]);
}
let accept = self.accept.clone();
CapsConstraint::DerivedOutput(Box::new(move |input: &Caps| {
match input.intersect(&accept) {
Ok(_) => CapsSet::one(input.clone()),
Err(_) => CapsSet::from_alternatives(Vec::new()),
}
}))
}
fn is_format_boundary(&self) -> bool {
self.produce.is_some()
}
fn propose_output_caps(&self, input: &Caps) -> Caps {
self.output_for(input)
}
fn intercept_caps(&self, upstream_caps: &Caps) -> Result<Caps, G2gError> {
upstream_caps.intersect(&self.accept)
}
fn input_domains(&self) -> DomainSet {
DomainSet::only(self.domain())
}
fn output_memory(&self) -> MemoryDomainKind {
self.domain()
}
fn propose_allocation(&self, caps: &Caps) -> Option<AllocationParams> {
let Caps::RawVideo {
format,
width: Dim::Fixed(width),
height: Dim::Fixed(height),
..
} = caps
else {
return None;
};
let size = frame_bytes(*format, *width, *height);
Some(if self.cuda_frames {
AllocationParams::cuda(size, 1, 1)
} else {
AllocationParams::system(size, 1)
})
}
fn configure_pipeline(&mut self, absolute_caps: &Caps) -> Result<ConfigureOutcome, G2gError> {
absolute_caps.intersect(&self.accept)?;
self.fixed = Some(absolute_caps.clone());
#[cfg(feature = "python")]
{
if self.module.is_empty() || self.class.is_empty() {
return Err(G2gError::NotConfigured);
}
if self.worker.is_none() {
self.worker = Some(crate::host::PyWorker::spawn(
&self.module,
&self.class,
self.draw_label,
&self.params,
)?);
}
}
self.configured = true;
Ok(ConfigureOutcome::Accepted)
}
fn process<'a>(
&'a mut self,
packet: PipelinePacket,
out: &'a mut dyn OutputSink,
) -> Self::ProcessFuture<'a> {
Box::pin(async move {
if !self.configured {
return Err(G2gError::NotConfigured);
}
match packet {
PipelinePacket::DataFrame(frame) => {
for output in self.run(frame).await? {
self.emitted += 1;
out.push(PipelinePacket::DataFrame(output)).await?;
}
}
PipelinePacket::CapsChanged(c) => {
c.intersect(&self.accept)?;
let announce = self.output_for(&c);
out.push(PipelinePacket::CapsChanged(announce)).await?;
}
PipelinePacket::Flush => {
out.push(PipelinePacket::Flush).await?;
}
PipelinePacket::Segment(seg) => {
out.push(PipelinePacket::Segment(seg)).await?;
}
PipelinePacket::Eos => {}
other => {
out.push(other).await?;
}
}
Ok(())
})
}
fn metadata(&self) -> ElementMetadata {
ElementMetadata::new(
"Python ML element host",
"Filter/Effect/Video",
"Hosts a gst-python-ml element shell as a g2g transform via embedded CPython.",
"g2g",
)
}
fn properties(&self) -> &'static [PropertySpec] {
PYTRANSFORM_PROPS
}
fn set_property(&mut self, name: &str, value: PropValue) -> Result<(), PropError> {
match name {
"module" => {
self.module = value.as_str().ok_or(PropError::Type)?.to_string();
Ok(())
}
"class" => {
self.class = value.as_str().ok_or(PropError::Type)?.to_string();
Ok(())
}
"draw-label" => {
self.draw_label = value.as_bool().ok_or(PropError::Type)?;
Ok(())
}
"cuda-frames" => {
self.cuda_frames = value.as_bool().ok_or(PropError::Type)?;
Ok(())
}
"format" => {
let parsed = format_from_py(value.as_str().ok_or(PropError::Type)?)
.ok_or(PropError::Value)?;
let Caps::RawVideo { format, .. } = &mut self.accept else {
return Err(PropError::Value);
};
*format = parsed;
Ok(())
}
"input-caps" => {
self.accept = fixed_caps(value.as_str().ok_or(PropError::Type)?)?;
Ok(())
}
"output-caps" => {
self.produce = Some(fixed_caps(value.as_str().ok_or(PropError::Type)?)?);
Ok(())
}
other => {
crate::props::forward(&mut self.params, other, value);
Ok(())
}
}
}
fn get_property(&self, name: &str) -> Option<PropValue> {
match name {
"module" => Some(PropValue::Str(self.module.clone())),
"class" => Some(PropValue::Str(self.class.clone())),
"draw-label" => Some(PropValue::Bool(self.draw_label)),
"cuda-frames" => Some(PropValue::Bool(self.cuda_frames)),
"format" => match &self.accept {
Caps::RawVideo { format, .. } => {
Some(PropValue::Str(format_to_py(*format).to_string()))
}
_ => None,
},
"input-caps" => Some(PropValue::Str(self.accept.to_gst_string())),
"output-caps" => self
.produce
.as_ref()
.map(|c| PropValue::Str(c.to_gst_string())),
other => self
.params
.iter()
.find(|(k, _)| k == other)
.map(|(_, v)| v.clone()),
}
}
}
impl PadTemplates for PyTransform {
fn pad_templates() -> Vec<PadTemplate> {
let rgba = Caps::RawVideo {
format: RawVideoFormat::Rgba8,
width: Dim::Any,
height: Dim::Any,
framerate: Rate::Any,
interlace: g2g_core::Interlace::Any,
};
let set = CapsSet::one(rgba);
Vec::from([PadTemplate::sink(set.clone()), PadTemplate::source(set)])
}
}
static PYTRANSFORM_PROPS: &[PropertySpec] = hosted_element_props![
PropertySpec::new(
"module",
PropKind::Str,
"Python module to import (the element shell)",
),
PropertySpec::new(
"class",
PropKind::Str,
"class within the module to instantiate",
),
PropertySpec::new(
"draw-label",
PropKind::Bool,
"overlay the inferred label on the frame",
)
.with_default("false"),
PropertySpec::new(
"format",
PropKind::Str,
"pixel format the hosted element accepts (RGBA | BGRA | NV12 | I420 | YUY2 | P010_10LE)",
)
.with_default("RGBA"),
PropertySpec::new(
"cuda-frames",
PropKind::Bool,
"host an element that reads GPU-resident CUDA frames (needs g2g_process_cuda, NV12 / P010)",
)
.with_default("false"),
PropertySpec::new(
"input-caps",
PropKind::Str,
"caps accepted on the sink pad, e.g. audio/x-raw,format=S16LE,rate=16000",
)
.with_default("video/x-raw,format=RGBA"),
PropertySpec::new(
"output-caps",
PropKind::Str,
"caps produced downstream when the hosted element changes media type, e.g. text/x-raw,format=utf8",
),
];