g2g-python 0.6.0

Host gst-python-ml elements as first-class glass2glass elements (embedded CPython via pyo3).
Documentation
//! M198: PyTransform skeleton, the always-compiled (no `python` feature)
//! surface. Caps negotiation, the bridged property bag, and the lifecycle
//! guard all work without an interpreter; the per-frame Python call is covered
//! separately under `--features python` (needs libpython).

use g2g_core::{
    AsyncElement, Caps, Dim, G2gError, OutputSink, PipelinePacket, PropValue, PushOutcome, Rate,
    RawVideoFormat,
};
// Used only by the configure test, which is gated to the no-interpreter build.
#[cfg(not(feature = "python"))]
use g2g_core::ConfigureOutcome;
use g2g_python::PyTransform;

fn rgba(w: u32, h: u32, fps: u32) -> Caps {
    Caps::RawVideo {
        format: RawVideoFormat::Rgba8,
        width: Dim::Fixed(w),
        height: Dim::Fixed(h),
        framerate: Rate::Fixed(fps),
        interlace: g2g_core::Interlace::Any,
    }
}

/// Collects everything pushed downstream, so a test can assert what flowed.
#[derive(Default)]
struct CollectSink {
    packets: Vec<PipelinePacket>,
}

impl OutputSink for CollectSink {
    fn poll_push(
        &mut self,
        _cx: &mut core::task::Context<'_>,
        packet_slot: &mut Option<PipelinePacket>,
    ) -> core::task::Poll<Result<PushOutcome, G2gError>> {
        let packet = packet_slot.take().expect("poll_push without a packet");

        self.packets.push(packet);
        core::task::Poll::Ready(Ok(PushOutcome::Accepted))
    }
}

#[test]
fn negotiates_concrete_geometry_against_any_accept() {
    let el = PyTransform::new("action", "ActionTransform");
    let upstream = rgba(640, 480, 30);
    // Default accept is RGBA at Any dims/rate; the intersection fixes the
    // concrete upstream geometry (never leaves Any to trip fixate).
    assert_eq!(el.intercept_caps(&upstream).unwrap(), upstream);
}

#[test]
fn rejects_a_format_outside_the_accepted_set() {
    let el = PyTransform::new("action", "ActionTransform");
    let nv12 = Caps::RawVideo {
        format: RawVideoFormat::Nv12,
        width: Dim::Fixed(640),
        height: Dim::Fixed(480),
        framerate: Rate::Fixed(30),
        interlace: g2g_core::Interlace::Any,
    };
    assert!(el.intercept_caps(&nv12).is_err());
}

#[test]
fn with_accept_hosts_a_different_format() {
    let nv12 = Caps::RawVideo {
        format: RawVideoFormat::Nv12,
        width: Dim::Any,
        height: Dim::Any,
        framerate: Rate::Any,
        interlace: g2g_core::Interlace::Any,
    };
    let el = PyTransform::new("action", "ActionTransform").with_accept(nv12);
    let upstream = Caps::RawVideo {
        format: RawVideoFormat::Nv12,
        width: Dim::Fixed(1280),
        height: Dim::Fixed(720),
        framerate: Rate::Fixed(25),
        interlace: g2g_core::Interlace::Any,
    };
    assert_eq!(el.intercept_caps(&upstream).unwrap(), upstream);
}

// Without the `python` feature, configure arms the element without touching an
// interpreter. Under `python` it imports the named module, so the live path is
// covered by m198_python_path.rs against a real fixture instead.
#[cfg(not(feature = "python"))]
#[test]
fn configure_accepts_fixed_caps() {
    let mut el = PyTransform::new("action", "ActionTransform");
    // No `python` feature: configure negotiates and arms the element without
    // instantiating an interpreter.
    let outcome = el.configure_pipeline(&rgba(320, 240, 15)).unwrap();
    assert!(matches!(outcome, ConfigureOutcome::Accepted));
}

#[test]
fn properties_round_trip() {
    let mut el = PyTransform::new("action", "ActionTransform").with_draw_label(true);
    assert_eq!(el.get_property("draw-label"), Some(PropValue::Bool(true)));
    assert_eq!(
        el.get_property("module"),
        Some(PropValue::Str("action".into()))
    );

    el.set_property("class", PropValue::Str("OtherTransform".into()))
        .unwrap();
    assert_eq!(
        el.get_property("class"),
        Some(PropValue::Str("OtherTransform".into()))
    );

    // A name the host does not read itself is kept for the hosted class, which
    // is the only thing that can say whether it is real.
    el.set_property("nope", PropValue::Bool(true)).unwrap();
    assert_eq!(el.get_property("nope"), Some(PropValue::Bool(true)));
}

#[test]
fn process_before_configure_is_rejected() {
    let mut el = PyTransform::new("action", "ActionTransform");
    let mut sink = CollectSink::default();
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();
    let res = rt.block_on(el.process(PipelinePacket::Eos, &mut sink));
    assert_eq!(res, Err(G2gError::NotConfigured));
}

#[cfg(not(feature = "python"))]
#[test]
fn eos_after_configure_drains_to_nothing() {
    let mut el = PyTransform::new("action", "ActionTransform");
    el.configure_pipeline(&rgba(320, 240, 15)).unwrap();
    let mut sink = CollectSink::default();
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();
    rt.block_on(el.process(PipelinePacket::Eos, &mut sink))
        .unwrap();
    // Stateless host: EOS buffers nothing, so nothing is pushed.
    assert!(sink.packets.is_empty());
}