player: generate a graph from a pipeline description

This commit is contained in:
Stéphane Cerveau 2022-02-10 13:28:35 +01:00 committed by Stéphane Cerveau
parent 01df04be60
commit 88ec98bcef
6 changed files with 322 additions and 17 deletions

View file

@ -258,6 +258,9 @@ impl GPSApp {
application.add_action(&gio::SimpleAction::new("open", None)); application.add_action(&gio::SimpleAction::new("open", None));
application.set_accels_for_action("app.open", &["<primary>o"]); application.set_accels_for_action("app.open", &["<primary>o"]);
application.add_action(&gio::SimpleAction::new("open_pipeline", None));
application.set_accels_for_action("app.open_pipeline", &["<primary>p"]);
application.add_action(&gio::SimpleAction::new("save_as", None)); application.add_action(&gio::SimpleAction::new("save_as", None));
application.set_accels_for_action("app.save", &["<primary>s"]); application.set_accels_for_action("app.save", &["<primary>s"]);
@ -488,6 +491,22 @@ impl GPSApp {
}); });
}); });
let app_weak = self.downgrade();
self.connect_app_menu_action("open_pipeline", move |_, _| {
let app = upgrade_weak!(app_weak);
GPSUI::dialog::create_input_dialog(
"Enter pipeline description with gst-launch format",
"description",
&Settings::recent_pipeline_description(),
&app,
move |app, pipeline_desc| {
app.load_pipeline(&pipeline_desc)
.unwrap_or_else(|_| GPS_ERROR!("Unable to open file {}", pipeline_desc));
Settings::set_recent_pipeline_description(&pipeline_desc);
},
);
});
let app_weak = self.downgrade(); let app_weak = self.downgrade();
self.connect_app_menu_action("save_as", move |_, _| { self.connect_app_menu_action("save_as", move |_, _| {
let app = upgrade_weak!(app_weak); let app = upgrade_weak!(app_weak);
@ -631,11 +650,11 @@ impl GPSApp {
app.connect_app_menu_action("graph.check", app.connect_app_menu_action("graph.check",
move |_,_| { move |_,_| {
let app = upgrade_weak!(app_weak); let app = upgrade_weak!(app_weak);
let render_parse_launch = app.player.borrow().render_gst_launch(&app.graphview.borrow()); let pipeline_description = app.player.borrow().pipeline_description_from_graphview(&app.graphview.borrow());
if app.player.borrow().create_pipeline(&render_parse_launch).is_ok() { if app.player.borrow().create_pipeline(&pipeline_description).is_ok() {
GPSUI::message::display_message_dialog(&render_parse_launch,gtk::MessageType::Info, |_| {}); GPSUI::message::display_message_dialog(&pipeline_description,gtk::MessageType::Info, |_| {});
} else { } else {
GPSUI::message::display_error_dialog(false, &format!("Unable to render:\n\n{render_parse_launch}", )); GPSUI::message::display_error_dialog(false, &format!("Unable to render:\n\n{pipeline_description}"));
} }
} }
); );
@ -752,7 +771,7 @@ impl GPSApp {
move |_,_| { move |_,_| {
let app = upgrade_weak!(app_weak); let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.request-pad-input {}", node_id); GPS_DEBUG!("node.request-pad-input {}", node_id);
app.create_port_with_caps(node_id, GM::PortDirection::Input, GM::PortPresence::Sometimes, input.caps().to_string()) app.create_port_with_caps(node_id, GM::PortDirection::Input, GM::PortPresence::Sometimes, input.caps().to_string());
} }
); );
} else { } else {
@ -908,13 +927,13 @@ impl GPSApp {
properties properties
} }
fn create_port_with_caps( pub fn create_port_with_caps(
&self, &self,
node_id: u32, node_id: u32,
direction: GM::PortDirection, direction: GM::PortDirection,
presence: GM::PortPresence, presence: GM::PortPresence,
caps: String, caps: String,
) { ) -> u32 {
let node = self.node(node_id); let node = self.node(node_id);
let ports = node.all_ports(direction); let ports = node.all_ports(direction);
let port_name = match direction { let port_name = match direction {
@ -925,11 +944,27 @@ impl GPSApp {
let graphview = self.graphview.borrow(); let graphview = self.graphview.borrow();
let port_name = format!("{}{}", port_name, ports.len()); let port_name = format!("{}{}", port_name, ports.len());
let port = graphview.create_port(&port_name, direction, presence); let port = graphview.create_port(&port_name, direction, presence);
let id = port.id();
let properties: HashMap<String, String> = HashMap::from([("_caps".to_string(), caps)]); let properties: HashMap<String, String> = HashMap::from([("_caps".to_string(), caps)]);
port.update_properties(&properties); port.update_properties(&properties);
if let Some(mut node) = graphview.node(node_id) { if let Some(mut node) = graphview.node(node_id) {
graphview.add_port_to_node(&mut node, port); graphview.add_port_to_node(&mut node, port);
} }
id
}
pub fn create_link(
&self,
node_from_id: u32,
node_to_id: u32,
port_from_id: u32,
port_to_id: u32,
active: bool,
) {
let graphview = self.graphview.borrow();
let link =
graphview.create_link(node_from_id, node_to_id, port_from_id, port_to_id, active);
graphview.add_link(link);
} }
fn clear_graph(&self) { fn clear_graph(&self) {
@ -954,4 +989,11 @@ impl GPSApp {
graph_view.load_from_xml(buffer)?; graph_view.load_from_xml(buffer)?;
Ok(()) Ok(())
} }
fn load_pipeline(&self, pipeline_desc: &str) -> anyhow::Result<()> {
let player = self.player.borrow();
let graphview = self.graphview.borrow();
player.graphview_from_pipeline_description(&graphview, pipeline_desc);
Ok(())
}
} }

View file

@ -7,8 +7,10 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use crate::app::{AppState, GPSApp, GPSAppWeak}; use crate::app::{AppState, GPSApp, GPSAppWeak};
use crate::graphmanager::{GraphView, Node, NodeType, PortDirection, PropertyExt}; use crate::graphmanager as GM;
use crate::graphmanager::PropertyExt;
use crate::common;
use crate::gps::ElementInfo; use crate::gps::ElementInfo;
use crate::logger; use crate::logger;
use crate::settings; use crate::settings;
@ -165,14 +167,14 @@ impl Player {
pub fn start_pipeline( pub fn start_pipeline(
&self, &self,
graphview: &GraphView, graphview: &GM::GraphView,
new_state: PipelineState, new_state: PipelineState,
) -> anyhow::Result<PipelineState> { ) -> anyhow::Result<PipelineState> {
if self.state() == PipelineState::Stopped || self.state() == PipelineState::Error { if self.state() == PipelineState::Stopped || self.state() == PipelineState::Error {
let pipeline = self let pipeline = self
.create_pipeline(&self.render_gst_launch(graphview)) .create_pipeline(&self.pipeline_description_from_graphview(graphview))
.map_err(|err| { .map_err(|err| {
GPS_ERROR!("Unable to start a pipeline: {}", err); GPS_ERROR!("Unable to create a pipeline: {}", err);
err err
})?; })?;
@ -328,8 +330,8 @@ impl Player {
#[allow(clippy::only_used_in_recursion)] #[allow(clippy::only_used_in_recursion)]
fn process_gst_node( fn process_gst_node(
&self, &self,
graphview: &GraphView, graphview: &GM::GraphView,
node: &Node, node: &GM::Node,
elements: &mut HashMap<String, String>, elements: &mut HashMap<String, String>,
mut description: String, mut description: String,
) -> String { ) -> String {
@ -344,7 +346,7 @@ impl Player {
} }
} }
//Port properties //Port properties
let ports = node.all_ports(PortDirection::All); let ports = node.all_ports(GM::PortDirection::All);
for port in ports { for port in ports {
for (name, value) in port.properties().iter() { for (name, value) in port.properties().iter() {
if !port.hidden_property(name) { if !port.hidden_property(name) {
@ -353,7 +355,7 @@ impl Player {
} }
} }
let ports = node.all_ports(PortDirection::Output); let ports = node.all_ports(GM::PortDirection::Output);
let n_ports = ports.len(); let n_ports = ports.len();
for port in ports { for port in ports {
if let Some((_port_to, node_to)) = graphview.port_connected_to(port.id()) { if let Some((_port_to, node_to)) = graphview.port_connected_to(port.id()) {
@ -375,8 +377,8 @@ impl Player {
description description
} }
pub fn render_gst_launch(&self, graphview: &GraphView) -> String { pub fn pipeline_description_from_graphview(&self, graphview: &GM::GraphView) -> String {
let source_nodes = graphview.all_nodes(NodeType::Source); let source_nodes = graphview.all_nodes(GM::NodeType::Source);
let mut elements: HashMap<String, String> = HashMap::new(); let mut elements: HashMap<String, String> = HashMap::new();
let mut description = String::from(""); let mut description = String::from("");
for source_node in source_nodes { for source_node in source_nodes {
@ -385,6 +387,140 @@ impl Player {
} }
description description
} }
pub fn create_links_for_element(&self, element: &gst::Element, graphview: &GM::GraphView) {
let mut iter = element.iterate_pads();
let node = graphview
.node_by_unique_name(&element.name())
.expect("node should exists");
loop {
match iter.next() {
Ok(Some(pad)) => {
GPS_INFO!("Found pad: {}", pad.name());
if pad.direction() == gst::PadDirection::Src {
let port = node
.port_by_name(&pad.name())
.expect("The port should exist here");
if let Some(peer_pad) = pad.peer() {
if let Some(peer_element) = peer_pad.parent_element() {
let peer_node = graphview
.node_by_unique_name(&peer_element.name())
.expect("The node should exists here");
let peer_port = peer_node
.port_by_name(&peer_pad.name())
.expect("The port should exists here");
self.app.borrow().as_ref().unwrap().create_link(
node.id(),
peer_node.id(),
port.id(),
peer_port.id(),
true,
);
}
}
}
}
Err(gst::IteratorError::Resync) => iter.resync(),
_ => break,
}
}
}
pub fn create_pads_for_element(&self, element: &gst::Element, node: &GM::Node) {
let mut iter = element.iterate_pads();
loop {
match iter.next() {
Ok(Some(pad)) => {
let pad_name = pad.name().to_string();
GPS_INFO!("Found pad: {}", pad_name);
let mut port_direction = GM::PortDirection::Input;
if pad.direction() == gst::PadDirection::Src {
port_direction = GM::PortDirection::Output;
}
let port_id = self.app.borrow().as_ref().unwrap().create_port_with_caps(
node.id(),
port_direction,
GM::PortPresence::Always,
pad.current_caps()
.unwrap_or_else(|| pad.query_caps(None))
.to_string(),
);
if let Some(port) = node.port(port_id) {
port.set_name(&pad_name);
}
}
Err(gst::IteratorError::Resync) => iter.resync(),
_ => break,
}
}
}
pub fn create_properties_for_element(&self, element: &gst::Element, node: &GM::Node) {
let properties = ElementInfo::element_properties(element)
.unwrap_or_else(|_| panic!("Couldn't get properties for {}", node.name()));
for (property_name, property_value) in properties {
if property_name == "name"
|| property_name == "parent"
|| (property_value.flags() & glib::ParamFlags::READABLE)
!= glib::ParamFlags::READABLE
{
continue;
}
if let Ok(value_str) = ElementInfo::element_property(element, &property_name) {
let default_value_str =
common::value_as_str(property_value.default_value()).unwrap_or_default();
GPS_DEBUG!(
"property name {} value_str '{}' default '{}'",
property_name,
value_str,
default_value_str
);
if !value_str.is_empty() && value_str != default_value_str {
node.add_property(&property_name, &value_str);
}
}
}
}
pub fn graphview_from_pipeline_description(
&self,
graphview: &GM::GraphView,
pipeline_desc: &str,
) {
graphview.clear();
if let Ok(pipeline) = self.create_pipeline(pipeline_desc) {
let mut iter = pipeline.iterate_elements();
let mut elements: Vec<gst::Element> = Vec::new();
let elements = loop {
match iter.next() {
Ok(Some(element)) => {
GPS_INFO!("Found element: {}", element.name());
let element_factory_name = element.factory().unwrap().name().to_string();
let node = graphview.create_node(
&element_factory_name,
ElementInfo::element_type(&element_factory_name),
);
node.set_unique_name(&element.name());
graphview.add_node(node.clone());
self.create_pads_for_element(&element, &node);
self.create_properties_for_element(&element, &node);
elements.push(element);
}
Err(gst::IteratorError::Resync) => iter.resync(),
_ => break elements,
}
};
for element in elements {
self.create_links_for_element(&element, graphview);
}
} else {
GPS_ERROR!("Unable to create a pipeline: {}", pipeline_desc);
}
}
} }
impl Drop for PlayerInner { impl Drop for PlayerInner {

58
src/gps/test.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::gps::player::Player;
use crate::graphmanager as GM;
#[cfg(test)]
fn test_synced<F, R>(function: F) -> R
where
F: FnOnce() -> R + Send + std::panic::UnwindSafe + 'static,
R: Send + 'static,
{
/// No-op.
macro_rules! skip_assert_initialized {
() => {};
}
skip_assert_initialized!();
use futures_channel::oneshot;
use std::panic;
let (tx, rx) = oneshot::channel();
TEST_THREAD_WORKER
.push(move || {
tx.send(panic::catch_unwind(function))
.unwrap_or_else(|_| panic!("Failed to return result from thread pool"));
})
.expect("Failed to schedule a test call");
futures_executor::block_on(rx)
.expect("Failed to receive result from thread pool")
.unwrap_or_else(|e| std::panic::resume_unwind(e))
}
#[cfg(test)]
static TEST_THREAD_WORKER: once_cell::sync::Lazy<gtk::glib::ThreadPool> =
once_cell::sync::Lazy::new(|| {
let pool = gtk::glib::ThreadPool::exclusive(1).unwrap();
pool.push(move || {
gtk::init().expect("Tests failed to initialize gtk");
})
.expect("Failed to schedule a test call");
pool
});
#[cfg(test)]
mod player_test {
use super::*;
fn check_pipeline(pipeline_desc: &str) {
let player = Player::new().expect("Not able to create the player");
let graphview = GM::GraphView::new();
player.graphview_from_pipeline_description(&graphview, pipeline_desc);
}
#[test]
fn pipeline_creation() {
test_synced(|| {
println!("coucou");
//check_pipeline("videotestsrc ! autovideosink");
});
}
}

View file

@ -17,10 +17,12 @@ use crate::config;
use crate::logger; use crate::logger;
#[derive(Debug, Serialize, Deserialize, Default)] #[derive(Debug, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Settings { pub struct Settings {
pub app_maximized: bool, pub app_maximized: bool,
pub app_width: i32, pub app_width: i32,
pub app_height: i32, pub app_height: i32,
pub recent_pipeline: String,
// values must be emitted before tables // values must be emitted before tables
pub favorites: Vec<String>, pub favorites: Vec<String>,
@ -67,6 +69,17 @@ impl Settings {
path path
} }
pub fn set_recent_pipeline_description(pipeline: &str) {
let mut settings = Settings::load_settings();
settings.recent_pipeline = pipeline.to_string();
Settings::save_settings(&settings);
}
pub fn recent_pipeline_description() -> String {
let settings = Settings::load_settings();
settings.recent_pipeline
}
pub fn add_favorite(favorite: &str) { pub fn add_favorite(favorite: &str) {
let mut settings = Settings::load_settings(); let mut settings = Settings::load_settings();
settings.favorites.sort(); settings.favorites.sort();

View file

@ -43,3 +43,54 @@ pub fn create_dialog<F: Fn(GPSApp, gtk::Dialog) + 'static>(
dialog dialog
} }
pub fn create_input_dialog<F: Fn(GPSApp, String) + 'static>(
dialog_name: &str,
input_name: &str,
default_value: &str,
app: &GPSApp,
f: F,
) {
let dialog = gtk::Dialog::with_buttons(
Some(dialog_name),
Some(&app.window),
gtk::DialogFlags::MODAL,
&[("Ok", gtk::ResponseType::Apply)],
);
dialog.set_default_size(600, 100);
dialog.set_modal(true);
let label = gtk::Label::builder()
.label(input_name)
.hexpand(true)
.valign(gtk::Align::Center)
.halign(gtk::Align::Start)
.margin_start(4)
.build();
let entry = gtk::Entry::builder()
.width_request(400)
.valign(gtk::Align::Center)
.build();
entry.set_text(default_value);
let content_area = dialog.content_area();
content_area.set_orientation(gtk::Orientation::Horizontal);
content_area.set_vexpand(true);
content_area.set_margin_start(10);
content_area.set_margin_end(10);
content_area.set_margin_top(10);
content_area.set_margin_bottom(10);
content_area.append(&label);
content_area.append(&entry);
let app_weak = app.downgrade();
dialog.connect_response(glib::clone!(@weak entry => move |dialog, response_type| {
let app = upgrade_weak!(app_weak);
if response_type == gtk::ResponseType::Apply {
f(app, entry.text().to_string());
}
dialog.close()
}));
dialog.show();
}

View file

@ -13,6 +13,11 @@
<attribute name="action">app.open</attribute> <attribute name="action">app.open</attribute>
<attribute name="accel">&lt;primary&gt;n</attribute> <attribute name="accel">&lt;primary&gt;n</attribute>
</item> </item>
<item>
<attribute name="label" translatable="yes" comments="Primary menu entry that opens a pipeline">_Open pipeline</attribute>
<attribute name="action">app.open_pipeline</attribute>
<attribute name="accel">&lt;primary&gt;p</attribute>
</item>
<item> <item>
<attribute name="label" translatable="yes" comments="Primary menu entry that saves the graph">_Save As</attribute> <attribute name="label" translatable="yes" comments="Primary menu entry that saves the graph">_Save As</attribute>
<attribute name="action">app.save_as</attribute> <attribute name="action">app.save_as</attribute>