mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer-rs.git
synced 2024-11-25 11:01:10 +00:00
gstreamer: move parse_* functions to their own module
Better namespacing so the API is more Rust-y. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/merge_requests/1355>
This commit is contained in:
parent
f255b82b55
commit
a649e7dead
24 changed files with 177 additions and 158 deletions
|
@ -48,7 +48,7 @@ fn example_main() {
|
|||
let main_loop = glib::MainLoop::new(None, false);
|
||||
|
||||
// This creates a pipeline by parsing the gst-launch pipeline syntax.
|
||||
let pipeline = gst::parse_launch(
|
||||
let pipeline = gst::parse::launch(
|
||||
"audiotestsrc name=src ! queue max-size-time=2000000000 ! fakesink name=sink sync=true",
|
||||
)
|
||||
.unwrap();
|
||||
|
|
|
@ -27,7 +27,7 @@ fn example_main() {
|
|||
|
||||
let mut context = gst::ParseContext::new();
|
||||
let pipeline =
|
||||
match gst::parse_launch_full(pipeline_str, Some(&mut context), gst::ParseFlags::empty()) {
|
||||
match gst::parse::launch_full(pipeline_str, Some(&mut context), gst::ParseFlags::empty()) {
|
||||
Ok(pipeline) => pipeline,
|
||||
Err(err) => {
|
||||
if let Some(gst::ParseError::NoSuchElement) = err.kind::<gst::ParseError>() {
|
||||
|
|
|
@ -30,7 +30,7 @@ fn example_main() {
|
|||
let main_loop = glib::MainLoop::new(None, false);
|
||||
|
||||
// This creates a pipeline by parsing the gst-launch pipeline syntax.
|
||||
let pipeline = gst::parse_launch("audiotestsrc ! fakesink").unwrap();
|
||||
let pipeline = gst::parse::launch("audiotestsrc ! fakesink").unwrap();
|
||||
let bus = pipeline.bus().unwrap();
|
||||
|
||||
pipeline
|
||||
|
|
|
@ -42,7 +42,7 @@ fn example_main() {
|
|||
gst::init().unwrap();
|
||||
|
||||
// Create a pipeline from the launch-syntax given on the cli.
|
||||
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
|
||||
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
|
||||
let bus = pipeline.bus().unwrap();
|
||||
|
||||
pipeline
|
||||
|
|
|
@ -42,7 +42,7 @@ fn example_main() {
|
|||
gst::init().unwrap();
|
||||
|
||||
// Create a pipeline from the launch-syntax given on the cli.
|
||||
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
|
||||
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
|
||||
let bus = pipeline.bus().unwrap();
|
||||
|
||||
pipeline
|
||||
|
|
|
@ -26,8 +26,11 @@ fn example_main() {
|
|||
// Especially GUIs should probably handle this case, to tell users that they need to
|
||||
// install the corresponding gstreamer plugins.
|
||||
let mut context = gst::ParseContext::new();
|
||||
let pipeline =
|
||||
match gst::parse_launch_full(&pipeline_str, Some(&mut context), gst::ParseFlags::empty()) {
|
||||
let pipeline = match gst::parse::launch_full(
|
||||
&pipeline_str,
|
||||
Some(&mut context),
|
||||
gst::ParseFlags::empty(),
|
||||
) {
|
||||
Ok(pipeline) => pipeline,
|
||||
Err(err) => {
|
||||
if let Some(gst::ParseError::NoSuchElement) = err.kind::<gst::ParseError>() {
|
||||
|
|
|
@ -24,7 +24,7 @@ fn example_main() {
|
|||
let main_loop = glib::MainLoop::new(None, false);
|
||||
|
||||
// Let GStreamer create a pipeline from the parsed launch syntax on the cli.
|
||||
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
|
||||
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
|
||||
let bus = pipeline.bus().unwrap();
|
||||
|
||||
pipeline
|
||||
|
|
|
@ -22,7 +22,7 @@ fn example_main() {
|
|||
// Parse the pipeline we want to probe from a static in-line string.
|
||||
// Here we give our audiotestsrc a name, so we can retrieve that element
|
||||
// from the resulting pipeline.
|
||||
let pipeline = gst::parse_launch(&format!(
|
||||
let pipeline = gst::parse::launch(&format!(
|
||||
"audiotestsrc name=src ! audio/x-raw,format={},channels=1 ! fakesink",
|
||||
gst_audio::AUDIO_FORMAT_S16
|
||||
))
|
||||
|
|
|
@ -28,7 +28,7 @@ fn example_main() {
|
|||
let main_loop = glib::MainLoop::new(None, false);
|
||||
|
||||
// Let GStreamer create a pipeline from the parsed launch syntax on the cli.
|
||||
let pipeline = gst::parse_launch(&pipeline_str).unwrap();
|
||||
let pipeline = gst::parse::launch(&pipeline_str).unwrap();
|
||||
let bus = pipeline.bus().unwrap();
|
||||
|
||||
pipeline
|
||||
|
|
|
@ -42,7 +42,7 @@ fn example_main() -> Result<(), Error> {
|
|||
|
||||
// Parse the pipeline we want to probe from a static in-line string.
|
||||
let mut context = gst::ParseContext::new();
|
||||
let pipeline = match gst::parse_launch_full(
|
||||
let pipeline = match gst::parse::launch_full(
|
||||
"audiotestsrc wave=white-noise num-buffers=100 ! flacenc ! filesink location=test.flac",
|
||||
Some(&mut context),
|
||||
gst::ParseFlags::empty(),
|
||||
|
|
|
@ -27,7 +27,7 @@ fn create_pipeline(uri: String, out_path: std::path::PathBuf) -> Result<gst::Pip
|
|||
gst::init()?;
|
||||
|
||||
// Create our pipeline from a pipeline description string.
|
||||
let pipeline = gst::parse_launch(&format!(
|
||||
let pipeline = gst::parse::launch(&format!(
|
||||
"uridecodebin uri={uri} ! videoconvert ! appsink name=sink"
|
||||
))?
|
||||
.downcast::<gst::Pipeline>()
|
||||
|
|
|
@ -11,7 +11,7 @@ fn example_main() {
|
|||
gst::init().unwrap();
|
||||
|
||||
// This creates a pipeline by parsing the gst-launch pipeline syntax.
|
||||
let pipeline = gst::parse_launch(
|
||||
let pipeline = gst::parse::launch(
|
||||
"videotestsrc name=src ! video/x-raw,width=640,height=480 ! compositor0.sink_0 \
|
||||
compositor ! video/x-raw,width=1280,height=720 ! videoconvert ! autovideosink",
|
||||
)
|
||||
|
|
|
@ -83,7 +83,7 @@ fn example_main() {
|
|||
|
||||
gst::init().unwrap();
|
||||
|
||||
let pipeline = gst::parse_launch(&format!(
|
||||
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::<gst::Pipeline>().unwrap();
|
||||
|
|
|
@ -646,7 +646,7 @@ mod tests {
|
|||
StreamProducer,
|
||||
) {
|
||||
let producer_pipe =
|
||||
gst::parse_launch("appsrc name=producer_src ! appsink name=producer_sink")
|
||||
gst::parse::launch("appsrc name=producer_src ! appsink name=producer_sink")
|
||||
.unwrap()
|
||||
.downcast::<gst::Pipeline>()
|
||||
.unwrap();
|
||||
|
@ -678,7 +678,7 @@ mod tests {
|
|||
|
||||
impl Consumer {
|
||||
fn new(id: &str) -> Self {
|
||||
let pipeline = gst::parse_launch(&format!("appsrc name={id} ! appsink name=sink"))
|
||||
let pipeline = gst::parse::launch(&format!("appsrc name={id} ! appsink name=sink"))
|
||||
.unwrap()
|
||||
.downcast::<gst::Pipeline>()
|
||||
.unwrap();
|
||||
|
|
|
@ -2,125 +2,17 @@
|
|||
|
||||
use std::ptr;
|
||||
|
||||
use glib::{prelude::*, translate::*};
|
||||
use glib::translate::*;
|
||||
|
||||
#[cfg(feature = "v1_18")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "v1_18")))]
|
||||
use crate::Tracer;
|
||||
use crate::{Bin, Element, Object, ParseContext, ParseFlags};
|
||||
|
||||
// import only functions which do not have their own module as namespace
|
||||
pub use crate::auto::functions::{
|
||||
main_executable_path, parse_bin_from_description, parse_launch, parse_launchv, update_registry,
|
||||
util_get_timestamp, version, version_string,
|
||||
main_executable_path, update_registry, util_get_timestamp, version, version_string,
|
||||
};
|
||||
|
||||
pub fn parse_bin_from_description_with_name(
|
||||
bin_description: &str,
|
||||
ghost_unlinked_pads: bool,
|
||||
bin_name: &str,
|
||||
) -> Result<Bin, glib::Error> {
|
||||
skip_assert_initialized!();
|
||||
let bin = parse_bin_from_description(bin_description, ghost_unlinked_pads)?;
|
||||
if !bin_name.is_empty() {
|
||||
let obj = bin.clone().upcast::<Object>();
|
||||
unsafe {
|
||||
ffi::gst_object_set_name(obj.to_glib_none().0, bin_name.to_glib_none().0);
|
||||
}
|
||||
}
|
||||
Ok(bin)
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_bin_from_description_full")]
|
||||
pub fn parse_bin_from_description_full(
|
||||
bin_description: &str,
|
||||
ghost_unlinked_pads: bool,
|
||||
mut context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
skip_assert_initialized!();
|
||||
unsafe {
|
||||
let mut error = ptr::null_mut();
|
||||
let ret = ffi::gst_parse_bin_from_description_full(
|
||||
bin_description.to_glib_none().0,
|
||||
ghost_unlinked_pads.into_glib(),
|
||||
context.to_glib_none_mut().0,
|
||||
flags.into_glib(),
|
||||
&mut error,
|
||||
);
|
||||
if error.is_null() {
|
||||
Ok(from_glib_none(ret))
|
||||
} else {
|
||||
Err(from_glib_full(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_bin_from_description_with_name_full(
|
||||
bin_description: &str,
|
||||
ghost_unlinked_pads: bool,
|
||||
bin_name: &str,
|
||||
context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
skip_assert_initialized!();
|
||||
let bin =
|
||||
parse_bin_from_description_full(bin_description, ghost_unlinked_pads, context, flags)?;
|
||||
if !bin_name.is_empty() {
|
||||
let obj = bin.clone().upcast::<Object>();
|
||||
unsafe {
|
||||
ffi::gst_object_set_name(obj.to_glib_none().0, bin_name.to_glib_none().0);
|
||||
}
|
||||
}
|
||||
Ok(bin)
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_launch_full")]
|
||||
pub fn parse_launch_full(
|
||||
pipeline_description: &str,
|
||||
mut context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
assert_initialized_main_thread!();
|
||||
unsafe {
|
||||
let mut error = ptr::null_mut();
|
||||
let ret = ffi::gst_parse_launch_full(
|
||||
pipeline_description.to_glib_none().0,
|
||||
context.to_glib_none_mut().0,
|
||||
flags.into_glib(),
|
||||
&mut error,
|
||||
);
|
||||
if error.is_null() {
|
||||
Ok(from_glib_none(ret))
|
||||
} else {
|
||||
Err(from_glib_full(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_launchv_full")]
|
||||
pub fn parse_launchv_full(
|
||||
argv: &[&str],
|
||||
mut context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
assert_initialized_main_thread!();
|
||||
unsafe {
|
||||
let mut error = ptr::null_mut();
|
||||
let ret = ffi::gst_parse_launchv_full(
|
||||
argv.to_glib_none().0,
|
||||
context.to_glib_none_mut().0,
|
||||
flags.into_glib(),
|
||||
&mut error,
|
||||
);
|
||||
if error.is_null() {
|
||||
Ok(from_glib_none(ret))
|
||||
} else {
|
||||
Err(from_glib_full(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_calculate_linear_regression")]
|
||||
pub fn calculate_linear_regression(
|
||||
xy: &[(u64, u64)],
|
||||
|
@ -181,7 +73,6 @@ pub fn active_tracers() -> glib::List<Tracer> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_calculate_linear_regression() {
|
||||
|
@ -197,18 +88,4 @@ mod tests {
|
|||
calculate_linear_regression(&values, Some(&mut temp)).unwrap();
|
||||
assert_eq!((m_num, m_denom, b, xbase), (10, 10, 3, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_bin_from_description_with_name() {
|
||||
crate::init().unwrap();
|
||||
|
||||
let bin =
|
||||
parse_bin_from_description_with_name("fakesrc ! fakesink", false, "all_fake").unwrap();
|
||||
let name = bin.name();
|
||||
assert_eq!(name, "all_fake");
|
||||
|
||||
let bin = parse_bin_from_description_with_name("fakesrc ! fakesink", false, "").unwrap();
|
||||
let name = bin.name();
|
||||
assert_ne!(name, "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -259,6 +259,8 @@ pub use crate::functions::*;
|
|||
mod utils;
|
||||
pub use crate::utils::ObjectLockGuard;
|
||||
|
||||
pub mod parse;
|
||||
|
||||
#[cfg(feature = "v1_18")]
|
||||
mod gtype;
|
||||
|
||||
|
|
137
gstreamer/src/parse.rs
Normal file
137
gstreamer/src/parse.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
// Take a look at the license at the top of the repository in the LICENSE file.
|
||||
|
||||
use std::ptr;
|
||||
|
||||
use glib::{prelude::*, translate::*};
|
||||
|
||||
use crate::{Bin, Element, Object, ParseContext, ParseFlags};
|
||||
|
||||
pub use crate::auto::functions::parse_bin_from_description as bin_from_description;
|
||||
pub use crate::auto::functions::parse_launch as launch;
|
||||
pub use crate::auto::functions::parse_launchv as launchv;
|
||||
|
||||
#[doc(alias = "gst_parse_bin_from_description_full")]
|
||||
pub fn bin_from_description_with_name(
|
||||
bin_description: &str,
|
||||
ghost_unlinked_pads: bool,
|
||||
bin_name: &str,
|
||||
) -> Result<Bin, glib::Error> {
|
||||
skip_assert_initialized!();
|
||||
let bin = bin_from_description(bin_description, ghost_unlinked_pads)?;
|
||||
if !bin_name.is_empty() {
|
||||
let obj = bin.clone().upcast::<Object>();
|
||||
unsafe {
|
||||
ffi::gst_object_set_name(obj.to_glib_none().0, bin_name.to_glib_none().0);
|
||||
}
|
||||
}
|
||||
Ok(bin)
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_bin_from_description_full")]
|
||||
pub fn bin_from_description_full(
|
||||
bin_description: &str,
|
||||
ghost_unlinked_pads: bool,
|
||||
mut context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
skip_assert_initialized!();
|
||||
unsafe {
|
||||
let mut error = ptr::null_mut();
|
||||
let ret = ffi::gst_parse_bin_from_description_full(
|
||||
bin_description.to_glib_none().0,
|
||||
ghost_unlinked_pads.into_glib(),
|
||||
context.to_glib_none_mut().0,
|
||||
flags.into_glib(),
|
||||
&mut error,
|
||||
);
|
||||
if error.is_null() {
|
||||
Ok(from_glib_none(ret))
|
||||
} else {
|
||||
Err(from_glib_full(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_bin_from_description_full")]
|
||||
pub fn bin_from_description_with_name_full(
|
||||
bin_description: &str,
|
||||
ghost_unlinked_pads: bool,
|
||||
bin_name: &str,
|
||||
context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
skip_assert_initialized!();
|
||||
let bin = bin_from_description_full(bin_description, ghost_unlinked_pads, context, flags)?;
|
||||
if !bin_name.is_empty() {
|
||||
let obj = bin.clone().upcast::<Object>();
|
||||
unsafe {
|
||||
ffi::gst_object_set_name(obj.to_glib_none().0, bin_name.to_glib_none().0);
|
||||
}
|
||||
}
|
||||
Ok(bin)
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_launch_full")]
|
||||
pub fn launch_full(
|
||||
pipeline_description: &str,
|
||||
mut context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
assert_initialized_main_thread!();
|
||||
unsafe {
|
||||
let mut error = ptr::null_mut();
|
||||
let ret = ffi::gst_parse_launch_full(
|
||||
pipeline_description.to_glib_none().0,
|
||||
context.to_glib_none_mut().0,
|
||||
flags.into_glib(),
|
||||
&mut error,
|
||||
);
|
||||
if error.is_null() {
|
||||
Ok(from_glib_none(ret))
|
||||
} else {
|
||||
Err(from_glib_full(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(alias = "gst_parse_launchv_full")]
|
||||
pub fn launchv_full(
|
||||
argv: &[&str],
|
||||
mut context: Option<&mut ParseContext>,
|
||||
flags: ParseFlags,
|
||||
) -> Result<Element, glib::Error> {
|
||||
assert_initialized_main_thread!();
|
||||
unsafe {
|
||||
let mut error = ptr::null_mut();
|
||||
let ret = ffi::gst_parse_launchv_full(
|
||||
argv.to_glib_none().0,
|
||||
context.to_glib_none_mut().0,
|
||||
flags.into_glib(),
|
||||
&mut error,
|
||||
);
|
||||
if error.is_null() {
|
||||
Ok(from_glib_none(ret))
|
||||
} else {
|
||||
Err(from_glib_full(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_bin_from_description_with_name() {
|
||||
crate::init().unwrap();
|
||||
|
||||
let bin = bin_from_description_with_name("fakesrc ! fakesink", false, "all_fake").unwrap();
|
||||
let name = bin.name();
|
||||
assert_eq!(name, "all_fake");
|
||||
|
||||
let bin = bin_from_description_with_name("fakesrc ! fakesink", false, "").unwrap();
|
||||
let name = bin.name();
|
||||
assert_ne!(name, "");
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ fn tutorial_main() {
|
|||
|
||||
// Build the pipeline
|
||||
let uri = "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm";
|
||||
let pipeline = gst::parse_launch(&format!("playbin uri={uri}")).unwrap();
|
||||
let pipeline = gst::parse::launch(&format!("playbin uri={uri}")).unwrap();
|
||||
|
||||
// Start playing
|
||||
pipeline
|
||||
|
|
|
@ -12,7 +12,7 @@ fn tutorial_main() -> Result<(), Error> {
|
|||
|
||||
// Build the pipeline
|
||||
let uri = "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm";
|
||||
let pipeline = gst::parse_launch(&format!("playbin uri={uri}"))?;
|
||||
let pipeline = gst::parse::launch(&format!("playbin uri={uri}"))?;
|
||||
|
||||
// Start playing
|
||||
let res = pipeline.set_state(gst::State::Playing)?;
|
||||
|
|
|
@ -122,7 +122,7 @@ USAGE: Choose one of the following options, then press enter:
|
|||
|
||||
// Build the pipeline.
|
||||
let uri = "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm";
|
||||
let pipeline = gst::parse_launch(&format!("playbin uri={uri}"))?;
|
||||
let pipeline = gst::parse::launch(&format!("playbin uri={uri}"))?;
|
||||
|
||||
// Start playing.
|
||||
let _ = pipeline.set_state(State::Playing)?;
|
||||
|
|
|
@ -46,7 +46,7 @@ fn tutorial_main() -> Result<(), Error> {
|
|||
gst::init().unwrap();
|
||||
|
||||
// Create the playbin element
|
||||
let pipeline = gst::parse_launch("playbin uri=appsrc://").unwrap();
|
||||
let pipeline = gst::parse::launch("playbin uri=appsrc://").unwrap();
|
||||
|
||||
// This part is called when playbin has created the appsrc element,
|
||||
// so we have a chance to configure it.
|
||||
|
|
|
@ -110,7 +110,7 @@ fn tutorial_main() -> Result<(), Error> {
|
|||
thread::spawn(move || handle_keyboard(ready_tx));
|
||||
|
||||
// Build the pipeline
|
||||
let pipeline = gst::parse_launch(
|
||||
let pipeline = gst::parse::launch(
|
||||
"playbin uri=https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
|
||||
)?;
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ fn tutorial_main() -> Result<(), Error> {
|
|||
let vis_plugin = vis_factory.create().build().unwrap();
|
||||
|
||||
// Build the pipeline
|
||||
let pipeline = gst::parse_launch("playbin uri=http://radio.hbr1.com:19800/ambient.ogg")?;
|
||||
let pipeline = gst::parse::launch("playbin uri=http://radio.hbr1.com:19800/ambient.ogg")?;
|
||||
|
||||
// Set the visualization flag
|
||||
let flags = pipeline.property_value("flags");
|
||||
|
|
|
@ -9,7 +9,7 @@ fn tutorial_main() -> Result<(), Error> {
|
|||
gst::init()?;
|
||||
|
||||
// Build the pipeline
|
||||
let pipeline = gst::parse_launch(
|
||||
let pipeline = gst::parse::launch(
|
||||
"playbin uri=https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
|
||||
)?;
|
||||
|
||||
|
|
Loading…
Reference in a new issue