From 0b922b0e89ef9b5a03300d14f2de3cc33d9638fe Mon Sep 17 00:00:00 2001 From: Ruben Gonzalez Date: Sun, 12 Feb 2023 18:11:19 +0100 Subject: [PATCH] examples: zoom effect with compositor and navigations events Use can change the video player zoom using the next keys: * +: Zoom in * -: Zoom out * Up/Down/Right/Left: Move the frame * r: reset the zoom Also mouse navigation events can be used for a better UX. Furthermore, it works with an pipeline using other video compositor filters like glvideomixer. For instance: glvideomixer \ name=mix background=1 \ sink_0::xpos=0 sink_0::ypos=0 sink_0::zorder=0 \ sink_0::width={WIDTH} sink_0::height={HEIGHT} \ ! glimagesinkelement \ gltestsrc pattern=mandelbrot name=src \ ! video/x-raw(memory:GLMemory),framerate=30/1,width={WIDTH},height={HEIGHT},pixel-aspect-ratio=1/1 \ ! queue \ ! mix.sink_0 Probe was added in the sink pad to get direct navigation events w/o transformation done by the mixer. More info about it in the PR [1]. [1] https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1495 Part-of: --- examples/src/bin/zoom.rs | 229 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 examples/src/bin/zoom.rs diff --git a/examples/src/bin/zoom.rs b/examples/src/bin/zoom.rs new file mode 100644 index 000000000..40b0fb99f --- /dev/null +++ b/examples/src/bin/zoom.rs @@ -0,0 +1,229 @@ +// Zoom example using navigation events and a compositor + +// Use can change the video player zoom using the next keys: +// * +: Zoom in +// * -: Zoom out +// * Up/Down/Right/Left: Move the frame +// * r: reset the zoom +// Also mouse navigation events can be used for a better UX. + +use gst::prelude::*; +use gst_video::video_event::NavigationEvent; +use std::sync::Mutex; + +#[path = "../examples-common.rs"] +mod examples_common; + +const WIDTH: i32 = 1280; +const HEIGHT: i32 = 720; + +#[derive(Default)] +struct MouseState { + clicked: bool, + clicked_x: f64, + clicked_y: f64, + clicked_xpos: i32, + clicked_ypos: i32, +} + +fn zoom(mixer_sink_pad: gst::Pad, x: i32, y: i32, zoom_in: bool) { + let xpos = mixer_sink_pad.property::("xpos"); + let ypos = mixer_sink_pad.property::("ypos"); + let width = mixer_sink_pad.property::("width"); + let height = mixer_sink_pad.property::("height"); + + let (width_offset, height_offset) = if zoom_in { + (WIDTH / 10, HEIGHT / 10) + } else { + (-WIDTH / 10, -HEIGHT / 10) + }; + + if width_offset + width <= 0 { + return; + } + + mixer_sink_pad.set_property("width", width + width_offset); + mixer_sink_pad.set_property("height", height + height_offset); + + let xpos_offset = ((x as f32 / WIDTH as f32) * width_offset as f32) as i32; + let new_xpos = xpos - xpos_offset; + let ypos_offset = ((y as f32 / HEIGHT as f32) * height_offset as f32) as i32; + let new_ypos = ypos - ypos_offset; + + if new_xpos != xpos { + mixer_sink_pad.set_property("xpos", new_xpos); + } + if new_ypos != ypos { + mixer_sink_pad.set_property("ypos", new_ypos); + } +} + +fn reset_zoom(mixer_sink_pad: gst::Pad) { + let xpos = mixer_sink_pad.property::("xpos"); + let ypos = mixer_sink_pad.property::("ypos"); + let width = mixer_sink_pad.property::("width"); + let height = mixer_sink_pad.property::("height"); + + if 0 != xpos { + mixer_sink_pad.set_property("xpos", 0); + } + if 0 != ypos { + mixer_sink_pad.set_property("ypos", 0); + } + if WIDTH != width { + mixer_sink_pad.set_property("width", WIDTH); + } + if HEIGHT != height { + mixer_sink_pad.set_property("height", HEIGHT); + } +} + +fn example_main() { + let clicked = Mutex::new(MouseState::default()); + + gst::init().unwrap(); + + let pipeline = gst::parse_launch(&format!( + "compositor name=mix background=1 sink_0::xpos=0 sink_0::ypos=0 sink_0::zorder=0 sink_0::width={WIDTH} sink_0::height={HEIGHT} ! xvimagesink \ + videotestsrc name=src ! video/x-raw,framerate=30/1,width={WIDTH},height={HEIGHT},pixel-aspect-ratio=1/1 ! queue ! mix.sink_0" + )).unwrap().downcast::().unwrap(); + + let mixer = pipeline.by_name("mix").unwrap(); + let mixer_src_pad = mixer.static_pad("src").unwrap(); + let mixer_sink_pad_weak = mixer.static_pad("sink_0").unwrap().downgrade(); + + // Probe added in the sink pad to get direct navigation events w/o transformation done by the mixer + mixer_src_pad.add_probe(gst::PadProbeType::EVENT_UPSTREAM, move |_, probe_info| { + let mixer_sink_pad = mixer_sink_pad_weak.upgrade().unwrap(); + + let Some(gst::PadProbeData::Event(ref ev)) = probe_info.data else { + return gst::PadProbeReturn::Ok; + }; + + if ev.type_() != gst::EventType::Navigation { + return gst::PadProbeReturn::Ok; + }; + + let Ok(nav_event) = NavigationEvent::parse(ev) else { + return gst::PadProbeReturn::Ok; + }; + + match nav_event { + NavigationEvent::KeyPress { key, .. } => match key.as_str() { + "Left" => { + let xpos = mixer_sink_pad.property::("xpos"); + mixer_sink_pad.set_property("xpos", xpos - 10); + } + "Right" => { + let xpos = mixer_sink_pad.property::("xpos"); + mixer_sink_pad.set_property("xpos", xpos + 10); + } + "Up" => { + let ypos = mixer_sink_pad.property::("ypos"); + mixer_sink_pad.set_property("ypos", ypos - 10); + } + "Down" => { + let ypos = mixer_sink_pad.property::("ypos"); + mixer_sink_pad.set_property("ypos", ypos + 10); + } + "plus" => { + zoom(mixer_sink_pad, WIDTH / 2, HEIGHT / 2, true); + } + "minus" => { + zoom(mixer_sink_pad, WIDTH / 2, HEIGHT / 2, false); + } + "r" => { + reset_zoom(mixer_sink_pad); + } + _ => (), + }, + NavigationEvent::MouseMove { x, y, .. } => { + let state = clicked.lock().unwrap(); + if state.clicked { + let xpos = mixer_sink_pad.property::("xpos"); + let ypos = mixer_sink_pad.property::("ypos"); + + let new_xpos = state.clicked_xpos + (x - state.clicked_x) as i32; + let new_ypos = state.clicked_ypos + (y - state.clicked_y) as i32; + + if new_xpos != xpos { + mixer_sink_pad.set_property("xpos", new_xpos); + } + + if new_ypos != ypos { + mixer_sink_pad.set_property("ypos", new_ypos); + } + } + } + NavigationEvent::MouseButtonPress { button, x, y, .. } => { + if button == 1 || button == 272 { + let mut state = clicked.lock().unwrap(); + state.clicked = true; + state.clicked_x = x; + state.clicked_y = y; + state.clicked_xpos = mixer_sink_pad.property("xpos"); + state.clicked_ypos = mixer_sink_pad.property("ypos"); + } else if button == 2 || button == 3 || button == 274 || button == 273 { + reset_zoom(mixer_sink_pad); + } else if button == 4 { + zoom(mixer_sink_pad, x as i32, y as i32, true); + } else if button == 5 { + zoom(mixer_sink_pad, x as i32, y as i32, false); + } + } + NavigationEvent::MouseButtonRelease { button, .. } => { + if button == 1 || button == 272 { + let mut state = clicked.lock().unwrap(); + state.clicked = false; + } + } + #[cfg(feature = "v1_18")] + NavigationEvent::MouseScroll { x, y, delta_y, .. } => { + if delta_y > 0.0 { + zoom(mixer_sink_pad, x as i32, y as i32, true); + } else if delta_y < 0.0 { + zoom(mixer_sink_pad, x as i32, y as i32, false); + } + } + _ => (), + } + + gst::PadProbeReturn::Ok + }); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.bus().unwrap(); + for msg in bus.iter_timed(gst::ClockTime::NONE) { + use gst::MessageView; + + match msg.view() { + MessageView::Eos(..) => { + println!("received eos"); + break; + } + MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + err.src().map(|s| s.path_string()), + err.error(), + err.debug() + ); + break; + } + _ => (), + }; + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); +} + +fn main() { + // tutorials_common::run is only required to set up the application environment on macOS + // (but not necessary in normal Cocoa applications where this is set up automatically) + examples_common::run(example_main); +}