diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3cf2cfc..247629c5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,19 +43,19 @@ stages: .debian:11-stable: extends: .debian:11 variables: - FDO_DISTRIBUTION_TAG: '$GST_RS_STABLE-${GST_RS_IMG_TAG}_2021-09-09.0' + FDO_DISTRIBUTION_TAG: '$GST_RS_STABLE-${GST_RS_IMG_TAG}_2021-10-12.0' FDO_BASE_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bullseye-slim:$GST_RS_STABLE-$GST_RS_IMG_TAG" .debian:11-msrv: extends: .debian:11 variables: - FDO_DISTRIBUTION_TAG: '$GST_RS_MSRV-${GST_RS_IMG_TAG}_2021-09-09.0' + FDO_DISTRIBUTION_TAG: '$GST_RS_MSRV-${GST_RS_IMG_TAG}_2021-10-12.0' FDO_BASE_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bullseye-slim:$GST_RS_MSRV-$GST_RS_IMG_TAG" .debian:11-nightly: extends: .debian:11 variables: - FDO_DISTRIBUTION_TAG: 'nightly-${GST_RS_IMG_TAG}_2021-09-09.0' + FDO_DISTRIBUTION_TAG: 'nightly-${GST_RS_IMG_TAG}_2021-10-12.0' FDO_BASE_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bullseye-slim:nightly-$GST_RS_IMG_TAG" .build-debian-container: @@ -66,6 +66,7 @@ stages: FDO_DISTRIBUTION_PACKAGES: "libcsound64-dev llvm clang nasm libsodium-dev" FDO_DISTRIBUTION_EXEC: >- bash ci/install-dav1d.sh && + bash ci/install-gtk4.sh && apt clean && bash ./ci/install-rust-ext.sh rules: diff --git a/Cargo.toml b/Cargo.toml index 2a37e6ad..2ff8e0d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "video/ffv1", "video/flavors", "video/gif", + "video/gtk4", "video/rav1e", "video/rspng", "video/hsv", diff --git a/LICENSE-MPL-2.0 b/LICENSE-MPL-2.0 new file mode 100644 index 00000000..14e2f777 --- /dev/null +++ b/LICENSE-MPL-2.0 @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/ci/install-gtk4.sh b/ci/install-gtk4.sh new file mode 100644 index 00000000..0fee20d7 --- /dev/null +++ b/ci/install-gtk4.sh @@ -0,0 +1,10 @@ +set -eux + +BRANCH=gtk-4-4 + +git clone https://gitlab.gnome.org/GNOME/gtk.git --branch $BRANCH --depth=1 +cd gtk +meson build -D prefix=/usr/local +ninja -C build +ninja -C build install +cd .. diff --git a/meson.build b/meson.build index a6e705c4..28f14a6a 100644 --- a/meson.build +++ b/meson.build @@ -108,6 +108,12 @@ else exclude += ['audio/csound'] endif +if dependency('gtk4', required : get_option('gtk4')).found() + plugins_rep += {'video/gtk4' : 'libgstgtk4',} +else + exclude += ['video/gtk4'] +endif + output = [] extensions = [] diff --git a/meson_options.txt b/meson_options.txt index c9c715b3..341e10d7 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,3 +4,4 @@ option('sodium', type : 'combo', choices : ['system', 'built-in', 'disabled'], value : 'built-in', description : 'Weither to use libsodium from the system or the built-in version from the sodiumoxide crate') option('csound', type : 'feature', value : 'auto', description : 'Build csound plugin') +option('gtk4', type : 'feature', value : 'auto', description : 'Build GTK4 plugin') diff --git a/video/gtk4/Cargo.toml b/video/gtk4/Cargo.toml new file mode 100644 index 00000000..f9a03f42 --- /dev/null +++ b/video/gtk4/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "gst-plugin-gtk4" +version = "0.1.0" +authors = ["Bilal Elmoussaoui ", "Jordan Petridis ", "Sebastian Dröge "] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +license = "MPL-2.0" +edition = "2018" +description = "GTK 4 Sink element and Paintable widget" + +[dependencies] +gtk = { package = "gtk4", git = "https://github.com/gtk-rs/gtk4-rs" } + +gst_video = {package="gstreamer-video", git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"} +gst_base = {package="gstreamer-base", git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"} +gst = {package="gstreamer", git="https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"} + +once_cell = "1.0" +fragile = "1.0.0" + +[lib] +name = "gstgtk4" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../version-helper" } + +[features] +# GStreamer 1.14 is required for static linking +static = ["gst/v1_14"] +capi = [] + +[package.metadata.capi] +min_version = "0.7.0" + +[package.metadata.capi.header] +enabled = false + +[package.metadata.capi.library] +install_subdir = "gstreamer-1.0" +versioning = false + +[package.metadata.capi.pkg_config] +requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gtk4, gobject-2.0, glib-2.0, gmodule-2.0" diff --git a/video/gtk4/LICENSE-MPL-2.0 b/video/gtk4/LICENSE-MPL-2.0 new file mode 120000 index 00000000..eb5d24fe --- /dev/null +++ b/video/gtk4/LICENSE-MPL-2.0 @@ -0,0 +1 @@ +../../LICENSE-MPL-2.0 \ No newline at end of file diff --git a/video/gtk4/README.md b/video/gtk4/README.md new file mode 100644 index 00000000..44d2ff5c --- /dev/null +++ b/video/gtk4/README.md @@ -0,0 +1,4 @@ +# Gtk 4 Sink & Paintable + +GTK 4 provides `gtk::Video` & `gtk::Picture` for rendering media such as videos. As the default `gtk::Video` widget doesn't +offer the possibility to use a custom `gst::Pipeline`. The plugin provides a `gst_video::VideoSink` along with a `gdk::Paintable` that's capable of rendering the sink's frames. diff --git a/video/gtk4/build.rs b/video/gtk4/build.rs new file mode 100644 index 00000000..cda12e57 --- /dev/null +++ b/video/gtk4/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/video/gtk4/examples/gtksink.rs b/video/gtk4/examples/gtksink.rs new file mode 100644 index 00000000..0016998a --- /dev/null +++ b/video/gtk4/examples/gtksink.rs @@ -0,0 +1,117 @@ +use gst::prelude::*; + +use gtk::prelude::*; +use gtk::{gdk, gio, glib}; + +use std::cell::RefCell; + +fn create_ui(app: >k::Application) { + let pipeline = gst::Pipeline::new(None); + let src = gst::ElementFactory::make("videotestsrc", None).unwrap(); + + let sink = gst::ElementFactory::make("gtk4paintablesink", None).unwrap(); + let paintable = sink + .property("paintable") + .unwrap() + .get::() + .unwrap(); + + pipeline.add_many(&[&src, &sink]).unwrap(); + src.link(&sink).unwrap(); + + let window = gtk::ApplicationWindow::new(app); + window.set_default_size(640, 480); + + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + let picture = gtk::Picture::new(); + let label = gtk::Label::new(Some("Position: 00:00:00")); + + picture.set_paintable(Some(&paintable)); + vbox.append(&picture); + vbox.append(&label); + + window.set_child(Some(&vbox)); + window.show(); + + app.add_window(&window); + + let pipeline_weak = pipeline.downgrade(); + let timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(500), move || { + let pipeline = match pipeline_weak.upgrade() { + Some(pipeline) => pipeline, + None => return glib::Continue(true), + }; + + let position = pipeline.query_position::(); + label.set_text(&format!("Position: {:.0}", position.display())); + glib::Continue(true) + }); + + let bus = pipeline.bus().unwrap(); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let app_weak = app.downgrade(); + bus.add_watch_local(move |_, msg| { + use gst::MessageView; + + let app = match app_weak.upgrade() { + Some(app) => app, + None => return glib::Continue(false), + }; + + match msg.view() { + MessageView::Eos(..) => app.quit(), + MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + err.src().map(|s| s.path_string()), + err.error(), + err.debug() + ); + app.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .expect("Failed to add bus watch"); + + let timeout_id = RefCell::new(Some(timeout_id)); + let pipeline = RefCell::new(Some(pipeline)); + app.connect_shutdown(move |_| { + window.close(); + + if let Some(pipeline) = pipeline.borrow_mut().take() { + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); + pipeline.bus().unwrap().remove_watch().unwrap(); + } + + if let Some(timeout_id) = timeout_id.borrow_mut().take() { + timeout_id.remove(); + } + }); +} + +fn main() { + gst::init().unwrap(); + gtk::init().unwrap(); + + gstgtk4::plugin_register_static().expect("Failed to register gstgtk4 plugin"); + + { + let app = gtk::Application::new(None, gio::ApplicationFlags::FLAGS_NONE); + + app.connect_activate(create_ui); + app.run(); + } + + unsafe { + gst::deinit(); + } +} diff --git a/video/gtk4/src/lib.rs b/video/gtk4/src/lib.rs new file mode 100644 index 00000000..2133ce92 --- /dev/null +++ b/video/gtk4/src/lib.rs @@ -0,0 +1,31 @@ +// +// Copyright (C) 2021 Bilal Elmoussaoui +// Copyright (C) 2021 Jordan Petridis +// Copyright (C) 2021 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; + +mod sink; +pub use sink::PaintableSink; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + sink::register(plugin) +} + +gst::plugin_define!( + gtk4, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MIT/X11", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/video/gtk4/src/sink/frame.rs b/video/gtk4/src/sink/frame.rs new file mode 100644 index 00000000..777fc6f9 --- /dev/null +++ b/video/gtk4/src/sink/frame.rs @@ -0,0 +1,87 @@ +// +// Copyright (C) 2021 Bilal Elmoussaoui +// Copyright (C) 2021 Jordan Petridis +// Copyright (C) 2021 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gtk::prelude::*; +use gtk::{gdk, glib}; +use std::convert::AsRef; + +#[derive(Debug)] +pub struct Frame(pub gst_video::VideoFrame); + +#[derive(Debug)] +pub struct Paintable { + pub paintable: gdk::Paintable, + pub pixel_aspect_ratio: f64, +} + +impl Paintable { + pub fn width(&self) -> i32 { + f64::round(self.paintable.intrinsic_width() as f64 * self.pixel_aspect_ratio) as i32 + } + + pub fn height(&self) -> i32 { + self.paintable.intrinsic_height() + } +} + +impl AsRef<[u8]> for Frame { + fn as_ref(&self) -> &[u8] { + self.0.plane_data(0).unwrap() + } +} + +impl From for Paintable { + fn from(f: Frame) -> Paintable { + let format = match f.0.format() { + gst_video::VideoFormat::Bgra => gdk::MemoryFormat::B8g8r8a8, + gst_video::VideoFormat::Argb => gdk::MemoryFormat::A8r8g8b8, + gst_video::VideoFormat::Rgba => gdk::MemoryFormat::R8g8b8a8, + gst_video::VideoFormat::Abgr => gdk::MemoryFormat::A8b8g8r8, + gst_video::VideoFormat::Rgb => gdk::MemoryFormat::R8g8b8, + gst_video::VideoFormat::Bgr => gdk::MemoryFormat::B8g8r8, + _ => unreachable!(), + }; + let width = f.0.width() as i32; + let height = f.0.height() as i32; + let rowstride = f.0.plane_stride()[0] as usize; + + let pixel_aspect_ratio = + (*f.0.info().par().numer() as f64) / (*f.0.info().par().denom() as f64); + + Paintable { + paintable: gdk::MemoryTexture::new( + width, + height, + format, + &glib::Bytes::from_owned(f), + rowstride, + ) + .upcast(), + pixel_aspect_ratio, + } + } +} + +impl Frame { + pub fn new(buffer: &gst::Buffer, info: &gst_video::VideoInfo) -> Self { + let video_frame = + gst_video::VideoFrame::from_buffer_readable(buffer.clone(), info).unwrap(); + Self(video_frame) + } + + pub fn width(&self) -> u32 { + self.0.width() + } + + pub fn height(&self) -> u32 { + self.0.height() + } +} diff --git a/video/gtk4/src/sink/imp.rs b/video/gtk4/src/sink/imp.rs new file mode 100644 index 00000000..22afe447 --- /dev/null +++ b/video/gtk4/src/sink/imp.rs @@ -0,0 +1,236 @@ +// +// Copyright (C) 2021 Bilal Elmoussaoui +// Copyright (C) 2021 Jordan Petridis +// Copyright (C) 2021 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use super::SinkEvent; +use crate::sink::frame::Frame; +use crate::sink::paintable::SinkPaintable; + +use glib::prelude::*; +use glib::Sender; + +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_error, gst_trace}; +use gst_base::subclass::prelude::*; +use gst_video::subclass::prelude::*; + +use gtk::glib; + +use once_cell::sync::Lazy; +use std::sync::Mutex; + +use fragile::Fragile; + +pub(super) static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "gtk4paintablesink", + gst::DebugColorFlags::empty(), + Some("GTK4 Paintable sink"), + ) +}); + +#[derive(Default)] +pub struct PaintableSink { + pub(super) paintable: Mutex>>, + info: Mutex>, + pub(super) sender: Mutex>>, + pub(super) pending_frame: Mutex>, +} + +impl Drop for PaintableSink { + fn drop(&mut self) { + let mut paintable = self.paintable.lock().unwrap(); + + // Drop the paintable from the main thread + if let Some(paintable) = paintable.take() { + let context = glib::MainContext::default(); + + context.invoke(move || { + drop(paintable); + }); + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for PaintableSink { + const NAME: &'static str = "Gtk4PaintableSink"; + type Type = super::PaintableSink; + type ParentType = gst_video::VideoSink; +} + +impl ObjectImpl for PaintableSink { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpec::new_object( + "paintable", + "Paintable", + "The Paintable the sink renders to", + gtk::gdk::Paintable::static_type(), + glib::ParamFlags::READABLE, + )] + }); + + PROPERTIES.as_ref() + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "paintable" => { + let mut paintable = self.paintable.lock().unwrap(); + if paintable.is_none() { + obj.initialize_paintable(&mut paintable); + } + + let paintable = match &*paintable { + Some(ref paintable) => paintable, + None => { + gst_error!(CAT, obj: obj, "Failed to create paintable"); + return None::<>k::gdk::Paintable>.to_value(); + } + }; + + // Getter must be called from the main thread + match paintable.try_get() { + Ok(paintable) => paintable.to_value(), + Err(_) => { + gst_error!( + CAT, + obj: obj, + "Can't retrieve Paintable from non-main thread" + ); + None::<>k::gdk::Paintable>.to_value() + } + } + } + _ => unimplemented!(), + } + } +} + +impl ElementImpl for PaintableSink { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: Lazy = Lazy::new(|| { + gst::subclass::ElementMetadata::new( + "GTK 4 Paintable Sink", + "Sink/Video", + "A GTK 4 Paintable sink", + "Bilal Elmoussaoui , Jordan Petridis , Sebastian Dröge ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: Lazy> = Lazy::new(|| { + // Those are the supported formats by a gdk::Texture + let caps = gst_video::video_make_raw_caps(&[ + gst_video::VideoFormat::Bgra, + gst_video::VideoFormat::Argb, + gst_video::VideoFormat::Rgba, + gst_video::VideoFormat::Abgr, + gst_video::VideoFormat::Rgb, + gst_video::VideoFormat::Bgr, + ]) + .any_features() + .build(); + + vec![gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap()] + }); + + PAD_TEMPLATES.as_ref() + } + + #[allow(clippy::single_match)] + fn change_state( + &self, + element: &Self::Type, + transition: gst::StateChange, + ) -> Result { + match transition { + gst::StateChange::NullToReady => { + let mut paintable = self.paintable.lock().unwrap(); + if paintable.is_none() { + element.initialize_paintable(&mut paintable); + } + + if paintable.is_none() { + gst_error!(CAT, obj: element, "Failed to create paintable"); + return Err(gst::StateChangeError); + } + } + _ => (), + } + + let res = self.parent_change_state(element, transition); + + match transition { + gst::StateChange::PausedToReady => { + let _ = self.info.lock().unwrap().take(); + let _ = self.pending_frame.lock().unwrap().take(); + } + _ => (), + } + + res + } +} + +impl BaseSinkImpl for PaintableSink { + fn set_caps(&self, element: &Self::Type, caps: &gst::Caps) -> Result<(), gst::LoggableError> { + gst_debug!(CAT, obj: element, "Setting caps {:?}", caps); + + let video_info = gst_video::VideoInfo::from_caps(caps) + .map_err(|_| gst::loggable_error!(CAT, "Invalid caps"))?; + + self.info.lock().unwrap().replace(video_info); + + Ok(()) + } +} + +impl VideoSinkImpl for PaintableSink { + fn show_frame( + &self, + element: &Self::Type, + buffer: &gst::Buffer, + ) -> Result { + gst_trace!(CAT, obj: element, "Rendering buffer {:?}", buffer); + + let info = self.info.lock().unwrap(); + let info = info.as_ref().ok_or_else(|| { + gst_error!(CAT, obj: element, "Received no caps yet"); + gst::FlowError::NotNegotiated + })?; + + let frame = Frame::new(buffer, info); + self.pending_frame.lock().unwrap().replace(frame); + + let sender = self.sender.lock().unwrap(); + let sender = sender.as_ref().ok_or_else(|| { + gst_error!(CAT, obj: element, "Have no main thread sender"); + gst::FlowError::Error + })?; + + sender.send(SinkEvent::FrameChanged).map_err(|_| { + gst_error!(CAT, obj: element, "Have main thread receiver shut down"); + gst::FlowError::Error + })?; + + Ok(gst::FlowSuccess::Ok) + } +} diff --git a/video/gtk4/src/sink/mod.rs b/video/gtk4/src/sink/mod.rs new file mode 100644 index 00000000..5eca6444 --- /dev/null +++ b/video/gtk4/src/sink/mod.rs @@ -0,0 +1,117 @@ +// +// Copyright (C) 2021 Bilal Elmoussaoui +// Copyright (C) 2021 Jordan Petridis +// Copyright (C) 2021 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gtk::glib; +use gtk::glib::prelude::*; +use gtk::subclass::prelude::*; + +use gst::{gst_debug, gst_trace}; + +use fragile::Fragile; + +use std::sync::{mpsc, MutexGuard}; + +mod frame; +mod imp; +mod paintable; + +use frame::Frame; +use paintable::SinkPaintable; + +enum SinkEvent { + FrameChanged, +} + +glib::wrapper! { + pub struct PaintableSink(ObjectSubclass) + @extends gst_video::VideoSink, gst_base::BaseSink, gst::Element, gst::Object; +} + +// GStreamer elements need to be thread-safe. For the private implementation this is automatically +// enforced but for the public wrapper type we need to specify this manually. +unsafe impl Send for PaintableSink {} +unsafe impl Sync for PaintableSink {} + +impl PaintableSink { + pub fn new(name: Option<&str>) -> Self { + glib::Object::new(&[("name", &name)]).expect("Failed to create a GTK4Sink") + } + + fn pending_frame(&self) -> Option { + let self_ = imp::PaintableSink::from_instance(self); + self_.pending_frame.lock().unwrap().take() + } + + fn initialize_paintable( + &self, + paintable_storage: &mut MutexGuard>>, + ) { + gst_debug!(imp::CAT, obj: self, "Initializing paintable"); + + let context = glib::MainContext::default(); + + // The channel for the SinkEvents + let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); + + // This is an one time channel we send into the closure, so we can block until the paintable has been + // created. + let (send, recv) = mpsc::channel(); + context.invoke(glib::clone!( + @weak self as s => + move || { + let paintable = Fragile::new(SinkPaintable::new()); + send.send(paintable).expect("Somehow we dropped the receiver"); + } + )); + + let paintable = recv.recv().expect("Somehow we dropped the sender"); + + receiver.attach( + None, + glib::clone!( + @weak self as sink => @default-return glib::Continue(false), + move |action| sink.do_action(action) + ), + ); + + **paintable_storage = Some(paintable); + + let self_ = imp::PaintableSink::from_instance(self); + *self_.sender.lock().unwrap() = Some(sender); + } + + fn do_action(&self, action: SinkEvent) -> glib::Continue { + let self_ = imp::PaintableSink::from_instance(self); + let paintable = self_.paintable.lock().unwrap().clone(); + let paintable = match paintable { + Some(paintable) => paintable, + None => return glib::Continue(false), + }; + + match action { + SinkEvent::FrameChanged => { + gst_trace!(imp::CAT, obj: self, "Frame changed"); + paintable.get().handle_frame_changed(self.pending_frame()) + } + } + + glib::Continue(true) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "gtk4paintablesink", + gst::Rank::None, + PaintableSink::static_type(), + ) +} diff --git a/video/gtk4/src/sink/paintable/imp.rs b/video/gtk4/src/sink/paintable/imp.rs new file mode 100644 index 00000000..d226f051 --- /dev/null +++ b/video/gtk4/src/sink/paintable/imp.rs @@ -0,0 +1,101 @@ +// +// Copyright (C) 2021 Bilal Elmoussaoui +// Copyright (C) 2021 Jordan Petridis +// Copyright (C) 2021 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gdk, glib, graphene}; + +use gst::gst_trace; + +use crate::sink::frame::Paintable; + +use std::cell::RefCell; + +use once_cell::sync::Lazy; + +pub(super) static CAT: Lazy = Lazy::new(|| { + gst::DebugCategory::new( + "gtk4paintablesink-paintable", + gst::DebugColorFlags::empty(), + Some("GTK4 Paintable Sink Paintable"), + ) +}); + +#[derive(Default)] +pub struct SinkPaintable { + pub paintable: RefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for SinkPaintable { + const NAME: &'static str = "Gtk4PaintableSinkPaintable"; + type Type = super::SinkPaintable; + type ParentType = glib::Object; + type Interfaces = (gdk::Paintable,); +} + +impl ObjectImpl for SinkPaintable {} + +impl PaintableImpl for SinkPaintable { + fn intrinsic_height(&self, _paintable: &Self::Type) -> i32 { + if let Some(Paintable { ref paintable, .. }) = *self.paintable.borrow() { + paintable.intrinsic_height() + } else { + 0 + } + } + + fn intrinsic_width(&self, _paintable: &Self::Type) -> i32 { + if let Some(Paintable { + ref paintable, + pixel_aspect_ratio, + }) = *self.paintable.borrow() + { + f64::round(paintable.intrinsic_width() as f64 * pixel_aspect_ratio) as i32 + } else { + 0 + } + } + + fn intrinsic_aspect_ratio(&self, _paintable: &Self::Type) -> f64 { + if let Some(Paintable { + ref paintable, + pixel_aspect_ratio, + }) = *self.paintable.borrow() + { + paintable.intrinsic_aspect_ratio() * pixel_aspect_ratio + } else { + 0.0 + } + } + + fn current_image(&self, _paintable: &Self::Type) -> gdk::Paintable { + if let Some(Paintable { ref paintable, .. }) = *self.paintable.borrow() { + paintable.clone() + } else { + gdk::Paintable::new_empty(0, 0).expect("Couldn't create empty paintable") + } + } + + fn snapshot(&self, paintable: &Self::Type, snapshot: &gdk::Snapshot, width: f64, height: f64) { + if let Some(Paintable { ref paintable, .. }) = *self.paintable.borrow() { + gst_trace!(CAT, obj: paintable, "Snapshotting frame"); + paintable.snapshot(snapshot, width, height); + } else { + gst_trace!(CAT, obj: paintable, "Snapshotting black frame"); + let snapshot = snapshot.downcast_ref::().unwrap(); + snapshot.append_color( + &gdk::RGBA::BLACK, + &graphene::Rect::new(0f32, 0f32, width as f32, height as f32), + ); + } + } +} diff --git a/video/gtk4/src/sink/paintable/mod.rs b/video/gtk4/src/sink/paintable/mod.rs new file mode 100644 index 00000000..19914f4a --- /dev/null +++ b/video/gtk4/src/sink/paintable/mod.rs @@ -0,0 +1,65 @@ +// +// Copyright (C) 2021 Bilal Elmoussaoui +// Copyright (C) 2021 Jordan Petridis +// Copyright (C) 2021 Sebastian Dröge +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use crate::sink::frame::{Frame, Paintable}; + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gdk, glib}; + +use gst::{gst_debug, gst_trace}; + +mod imp; + +glib::wrapper! { + pub struct SinkPaintable(ObjectSubclass) + @implements gdk::Paintable; +} + +impl SinkPaintable { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create a SinkPaintable") + } +} + +impl Default for SinkPaintable { + fn default() -> Self { + Self::new() + } +} + +impl SinkPaintable { + pub(crate) fn handle_frame_changed(&self, frame: Option) { + let self_ = imp::SinkPaintable::from_instance(self); + if let Some(frame) = frame { + gst_trace!(imp::CAT, obj: self, "Received new frame"); + + let paintable: Paintable = frame.into(); + let new_size = (paintable.width(), paintable.height()); + + let old_paintable = self_.paintable.replace(Some(paintable)); + let old_size = old_paintable.map(|p| (p.width(), p.height())); + + if Some(new_size) != old_size { + gst_debug!( + imp::CAT, + obj: self, + "Size changed from {:?} to {:?}", + old_size, + new_size, + ); + self.invalidate_size(); + } + + self.invalidate_contents(); + } + } +}