From 5a4e536d6afb70ff61ee6aa2dea53a263b169964 Mon Sep 17 00:00:00 2001 From: Thibault Saunier Date: Mon, 26 Feb 2024 18:12:34 -0300 Subject: [PATCH] skia: Implement a video compositor using skia Part-of: --- Cargo.lock | 76 ++++- Cargo.toml | 2 + docs/plugins/gst_plugins_cache.json | 342 +++++++++++++++++++ meson.build | 1 + meson_options.txt | 1 + video/skia/Cargo.toml | 45 +++ video/skia/LICENSE-MPL-2.0 | 373 +++++++++++++++++++++ video/skia/build.rs | 3 + video/skia/src/compositor/imp.rs | 496 ++++++++++++++++++++++++++++ video/skia/src/compositor/mod.rs | 30 ++ video/skia/src/compositor/pad.rs | 152 +++++++++ video/skia/src/lib.rs | 38 +++ video/skia/tests/compositor.rs | 56 ++++ 13 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 video/skia/Cargo.toml create mode 100644 video/skia/LICENSE-MPL-2.0 create mode 100644 video/skia/build.rs create mode 100644 video/skia/src/compositor/imp.rs create mode 100644 video/skia/src/compositor/mod.rs create mode 100644 video/skia/src/compositor/pad.rs create mode 100644 video/skia/src/lib.rs create mode 100644 video/skia/tests/compositor.rs diff --git a/Cargo.lock b/Cargo.lock index 934ac998b..9eead8983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2004,6 +2004,18 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -3164,6 +3176,18 @@ dependencies = [ "url", ] +[[package]] +name = "gst-plugin-skia" +version = "0.14.0-alpha.1" +dependencies = [ + "gst-plugin-version-helper", + "gstreamer", + "gstreamer-base", + "gstreamer-check", + "gstreamer-video", + "skia-safe", +] + [[package]] name = "gst-plugin-sodium" version = "0.14.0-alpha.1" @@ -4898,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -4907,6 +4931,17 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", + "redox_syscall", +] + [[package]] name = "librespot-audio" version = "0.6.0" @@ -7297,6 +7332,34 @@ dependencies = [ "quote", ] +[[package]] +name = "skia-bindings" +version = "0.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0974d1fad6818b1c84390a8cd26b48a4f48f1dfd2658e130fb0db5ebbb50aa1c" +dependencies = [ + "bindgen 0.71.1", + "cc", + "flate2", + "heck 0.5.0", + "lazy_static", + "regex", + "serde_json", + "tar", + "toml", +] + +[[package]] +name = "skia-safe" +version = "0.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cade7bc92e092138e3b726ce57162b3e0f4f3099c616b32af740a13ca18cb2a" +dependencies = [ + "bitflags 2.9.0", + "lazy_static", + "skia-bindings", +] + [[package]] name = "slab" version = "0.4.9" @@ -7630,6 +7693,17 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" diff --git a/Cargo.toml b/Cargo.toml index e656bddda..bdc1391fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "video/hsv", "video/png", "video/rav1e", + "video/skia", "video/videofx", "video/vvdec", "video/webp", @@ -116,6 +117,7 @@ default-members = [ "video/hsv", "video/png", "video/rav1e", + "video/skia", ] [profile.release] diff --git a/docs/plugins/gst_plugins_cache.json b/docs/plugins/gst_plugins_cache.json index 2be6e65f2..e3a1aa99f 100644 --- a/docs/plugins/gst_plugins_cache.json +++ b/docs/plugins/gst_plugins_cache.json @@ -14555,6 +14555,348 @@ "tracers": {}, "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" }, + "skia": { + "description": "GStreamer skia plugin", + "elements": { + "skiacompositor": { + "author": "Thibault Saunier , Sebastian Dröge ", + "description": "Skia based compositor", + "hierarchy": [ + "GstSkiaCompositor", + "GstVideoAggregator", + "GstAggregator", + "GstElement", + "GstObject", + "GInitiallyUnowned", + "GObject" + ], + "interfaces": [ + "GstChildProxy" + ], + "klass": "Compositor/Video", + "pad-templates": { + "sink_%%u": { + "caps": "video/x-raw:\n format: { A444_16LE, A444_16BE, Y416_LE, AYUV64, RGBA64_LE, ARGB64, ARGB64_LE, BGRA64_LE, ABGR64_LE, Y416_BE, RGBA64_BE, ARGB64_BE, BGRA64_BE, ABGR64_BE, A422_16LE, A422_16BE, A420_16LE, A420_16BE, A444_12LE, GBRA_12LE, A444_12BE, GBRA_12BE, Y412_LE, Y412_BE, A422_12LE, A422_12BE, A420_12LE, A420_12BE, A444_10LE, GBRA_10LE, A444_10BE, GBRA_10BE, A422_10LE, A422_10BE, A420_10LE, A420_10BE, BGR10A2_LE, RGB10A2_LE, Y410, A444, GBRA, AYUV, VUYA, RGBA, RBGA, ARGB, BGRA, ABGR, A422, A420, AV12, Y444_16LE, GBR_16LE, Y444_16BE, GBR_16BE, Y216_LE, Y216_BE, v216, P016_LE, P016_BE, Y444_12LE, GBR_12LE, Y444_12BE, GBR_12BE, I422_12LE, I422_12BE, Y212_LE, Y212_BE, I420_12LE, I420_12BE, P012_LE, P012_BE, Y444_10LE, GBR_10LE, Y444_10BE, GBR_10BE, r210, I422_10LE, I422_10BE, NV16_10LE32, Y210, UYVP, v210, I420_10LE, I420_10BE, P010_10LE, NV12_10LE40, NV12_10LE32, P010_10BE, MT2110R, MT2110T, NV12_10BE_8L128, NV12_10LE40_4L4, Y444, BGRP, GBR, RGBP, NV24, v308, IYU2, RGBx, xRGB, BGRx, xBGR, RGB, BGR, Y42B, NV16, NV61, YUY2, YVYU, UYVY, VYUY, I420, YV12, NV12, NV21, NV12_16L32S, NV12_32L32, NV12_4L4, NV12_64Z32, NV12_8L128, Y41B, IYU1, YUV9, YVU9, BGR16, RGB16, BGR15, RGB15, RGB8P, GRAY16_LE, GRAY16_BE, GRAY10_LE16, GRAY10_LE32, GRAY8 }\n width: [ 1, 2147483647 ]\n height: [ 1, 2147483647 ]\n framerate: [ 0/1, 2147483647/1 ]\n", + "direction": "sink", + "presence": "request", + "type": "GstSkiaCompositorPad" + }, + "src": { + "caps": "video/x-raw:\n format: { RGBA, BGRA, RGBx, RGB16, GRAY8 }\n width: [ 1, 2147483647 ]\n height: [ 1, 2147483647 ]\n framerate: [ 0/1, 2147483647/1 ]\n", + "direction": "src", + "presence": "always" + } + }, + "properties": { + "background": { + "blurb": "NULL", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "checker (0)", + "mutable": "null", + "readable": true, + "type": "GstSkiaCompositorBackground", + "writable": true + } + }, + "rank": "secondary" + } + }, + "filename": "gstskia", + "license": "MIT/X11", + "other-types": { + "GstSkiaCompositorBackground": { + "kind": "enum", + "values": [ + { + "desc": "Checker", + "name": "checker", + "value": "0" + }, + { + "desc": "Black", + "name": "black", + "value": "1" + }, + { + "desc": "White", + "name": "white", + "value": "2" + }, + { + "desc": "Transparent", + "name": "transparent", + "value": "3" + } + ] + }, + "GstSkiaCompositorPad": { + "hierarchy": [ + "GstSkiaCompositorPad", + "GstVideoAggregatorConvertPad", + "GstVideoAggregatorPad", + "GstAggregatorPad", + "GstPad", + "GstObject", + "GInitiallyUnowned", + "GObject" + ], + "kind": "object", + "properties": { + "alpha": { + "blurb": "Alpha value of the input", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "1", + "max": "1", + "min": "0", + "mutable": "null", + "readable": true, + "type": "gdouble", + "writable": true + }, + "anti-alias": { + "blurb": "Whether to use anti-aliasing", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "true", + "mutable": "null", + "readable": true, + "type": "gboolean", + "writable": true + }, + "height": { + "blurb": "Height of the picture", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "-1", + "max": "3.40282e+38", + "min": "-1", + "mutable": "null", + "readable": true, + "type": "gfloat", + "writable": true + }, + "operator": { + "blurb": "Blending operator to use for blending this pad over the previous ones", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "over (1)", + "mutable": "null", + "readable": true, + "type": "GstSkiaCompositorPadOperator", + "writable": true + }, + "width": { + "blurb": "Width of the picture", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "-1", + "max": "3.40282e+38", + "min": "-1", + "mutable": "null", + "readable": true, + "type": "gfloat", + "writable": true + }, + "xpos": { + "blurb": "Horizontal position of the input", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "0", + "max": "3.40282e+38", + "min": "-3.40282e+38", + "mutable": "null", + "readable": true, + "type": "gfloat", + "writable": true + }, + "ypos": { + "blurb": "Vertical position of the input", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "0", + "max": "3.40282e+38", + "min": "-3.40282e+38", + "mutable": "null", + "readable": true, + "type": "gfloat", + "writable": true + } + } + }, + "GstSkiaCompositorPadOperator": { + "kind": "enum", + "values": [ + { + "desc": "Source", + "name": "source", + "value": "0" + }, + { + "desc": "Over", + "name": "over", + "value": "1" + }, + { + "desc": "Add", + "name": "add", + "value": "2" + }, + { + "desc": "Dest", + "name": "dest", + "value": "3" + }, + { + "desc": "Clear", + "name": "clear", + "value": "4" + }, + { + "desc": "DestOver", + "name": "dest-over", + "value": "5" + }, + { + "desc": "SourceIn", + "name": "source-in", + "value": "6" + }, + { + "desc": "DestIn", + "name": "dest-in", + "value": "7" + }, + { + "desc": "SourceOut", + "name": "source-out", + "value": "8" + }, + { + "desc": "DestOut", + "name": "dest-out", + "value": "9" + }, + { + "desc": "SourceATop", + "name": "source-a-top", + "value": "10" + }, + { + "desc": "DestATop", + "name": "dest-a-top", + "value": "11" + }, + { + "desc": "Xor", + "name": "xor", + "value": "12" + }, + { + "desc": "Modulate", + "name": "modulate", + "value": "13" + }, + { + "desc": "Screen", + "name": "screen", + "value": "14" + }, + { + "desc": "Overlay", + "name": "overlay", + "value": "15" + }, + { + "desc": "Darken", + "name": "darken", + "value": "16" + }, + { + "desc": "Lighten", + "name": "lighten", + "value": "17" + }, + { + "desc": "ColorDodge", + "name": "color-dodge", + "value": "18" + }, + { + "desc": "ColorBurn", + "name": "color-burn", + "value": "19" + }, + { + "desc": "HardLight", + "name": "hard-light", + "value": "20" + }, + { + "desc": "SoftLight", + "name": "soft-light", + "value": "21" + }, + { + "desc": "Difference", + "name": "difference", + "value": "22" + }, + { + "desc": "Exclusion", + "name": "exclusion", + "value": "23" + }, + { + "desc": "Multiply", + "name": "multiply", + "value": "24" + }, + { + "desc": "Hue", + "name": "hue", + "value": "25" + }, + { + "desc": "Saturation", + "name": "saturation", + "value": "26" + }, + { + "desc": "Color", + "name": "color", + "value": "27" + }, + { + "desc": "Luminosity", + "name": "luminosity", + "value": "28" + } + ] + } + }, + "package": "gst-plugin-skia", + "source": "gst-plugin-skia", + "tracers": {}, + "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" + }, "sodium": { "description": "GStreamer plugin for libsodium-based file encryption and decryption", "elements": { diff --git a/meson.build b/meson.build index 4fffddb0e..c43e0270e 100644 --- a/meson.build +++ b/meson.build @@ -222,6 +222,7 @@ plugins = { 'quic_roq', ], }, + 'skia': {'library': 'libgstskia'}, 'speechmatics': {'library': 'libgstspeechmatics'}, 'vvdec': { 'library': 'libgstvvdec', diff --git a/meson_options.txt b/meson_options.txt index d75770ee4..8e09d6e34 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -66,6 +66,7 @@ option('gtk4', type: 'feature', value: 'auto', description: 'Build GTK4 plugin') option('hsv', type: 'feature', value: 'auto', description: 'Build hsv plugin') option('png', type: 'feature', value: 'auto', description: 'Build png plugin') option('rav1e', type: 'feature', value: 'auto', description: 'Build rav1e plugin') +option('skia', type: 'feature', value: 'auto', description: 'Build skia plugin') option('videofx', type: 'feature', value: 'auto', description: 'Build videofx plugin') option('vvdec', type: 'feature', value: 'auto', description: 'Build vvdec plugin') option('webp', type: 'feature', value: 'auto', description: 'Build webp plugin') diff --git a/video/skia/Cargo.toml b/video/skia/Cargo.toml new file mode 100644 index 000000000..043d7c018 --- /dev/null +++ b/video/skia/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "gst-plugin-skia" +version.workspace = true +authors = ["Thibault Saunier "] +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license = "MPL-2.0" +description = "GStreamer skia plugin" + +[dependencies] +skia = { package = "skia-safe", version = "0.81" } +gst.workspace = true +gst-base.workspace = true +gst-video = { workspace = true, features = ["v1_20"] } + +[lib] +name = "gstskia" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper.workspace = true + +[features] +static = [] +capi = [] +doc = ["gst/v1_18"] + +[package.metadata.capi] +min_version = "0.9.21" + +[package.metadata.capi.header] +enabled = false + +[package.metadata.capi.library] +install_subdir = "gstreamer-1.0" +versioning = false +import_library = false + +[package.metadata.capi.pkg_config] +requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0" + +[dev-dependencies] +gst-check.workspace = true diff --git a/video/skia/LICENSE-MPL-2.0 b/video/skia/LICENSE-MPL-2.0 new file mode 100644 index 000000000..14e2f777f --- /dev/null +++ b/video/skia/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/video/skia/build.rs b/video/skia/build.rs new file mode 100644 index 000000000..cda12e57e --- /dev/null +++ b/video/skia/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/video/skia/src/compositor/imp.rs b/video/skia/src/compositor/imp.rs new file mode 100644 index 000000000..cfca51a25 --- /dev/null +++ b/video/skia/src/compositor/imp.rs @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: MPL-2.0 +use gst::glib::Properties; +use gst_base::subclass::prelude::*; +use gst_video::{prelude::*, subclass::prelude::*}; +use std::{ + ops::ControlFlow, + sync::{LazyLock, Mutex}, +}; + +use super::*; + +static CAT: LazyLock = LazyLock::new(|| { + gst::DebugCategory::new( + "skiacompositor", + gst::DebugColorFlags::FG_BLUE, + Some("Skia compositor"), + ) +}); + +mod video_format { + static MAPPINGS: &[(skia::ColorType, gst_video::VideoFormat)] = &[ + (skia::ColorType::RGBA8888, gst_video::VideoFormat::Rgba), + (skia::ColorType::BGRA8888, gst_video::VideoFormat::Bgra), + (skia::ColorType::RGB888x, gst_video::VideoFormat::Rgbx), + (skia::ColorType::RGB565, gst_video::VideoFormat::Rgb16), + (skia::ColorType::Gray8, gst_video::VideoFormat::Gray8), + ]; + + pub fn gst_to_skia(video_format: gst_video::VideoFormat) -> Option { + MAPPINGS + .iter() + .find_map(|&(ct, vf)| (vf == video_format).then_some(ct)) + } + + pub fn gst_formats() -> Vec { + MAPPINGS.iter().map(|&(_, vf)| vf).collect() + } +} + +#[derive(glib::Enum, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)] +#[enum_type(name = "GstSkiaCompositorBackground")] +#[repr(u32)] +pub enum Background { + #[default] + Checker = 0, + Black = 1, + White = 2, + Transparent = 3, +} + +#[derive(Default, Properties, Debug)] +#[properties(wrapper_type = super::SkiaCompositor)] +pub struct SkiaCompositor { + #[property(name = "background", get, set, builder(Background::Checker))] + background: Mutex, +} + +impl SkiaCompositor { + fn should_draw_background(&self, token: &gst_video::subclass::AggregateFramesToken) -> bool { + let obj = self.obj(); + let info = match obj.video_info() { + Some(info) => info, + None => return false, + }; + + let bg_rect = gst_video::VideoRectangle { + x: 0, + y: 0, + w: info.width() as i32, + h: info.height() as i32, + }; + + for pad in obj.sink_pads() { + let pad = pad.downcast_ref::().unwrap(); + if pad.is_inactive() || pad.prepared_frame(token).is_none() { + continue; + } + + if self.pad_obscures_rectangle(pad, &bg_rect, token) { + return false; + } + } + + true + } + + fn pad_obscures_rectangle( + &self, + pad: &SkiaCompositorPad, + rect: &gst_video::VideoRectangle, + token: &gst_video::subclass::AggregateFramesToken, + ) -> bool { + let mut fill_border = true; + let mut border_argb = 0xff000000; + + if !pad.has_current_buffer(token) { + return false; + } + + if pad.alpha() != 1.0 + || pad + .video_info() + .expect("Pad has a buffer, it must have VideoInfo data") + .has_alpha() + { + gst::trace!( + CAT, + imp = self, + "Pad {} has alpha or alpha channel", + pad.name() + ); + return false; + } + + if let Some(config) = pad.property::>("converter-config") { + border_argb = config.get::("border-argb").unwrap_or(border_argb); + fill_border = config.get::("fill-border").unwrap_or(fill_border); + } + + if !fill_border || (border_argb & 0xff000000) != 0xff000000 { + gst::trace!(CAT, imp = self, "Pad {} has border", pad.name()); + return false; + } + + let mut pad_rect = gst_video::VideoRectangle { + x: pad.xpos() as i32, + y: pad.ypos() as i32, + w: 0, + h: 0, + }; + + let out_info = self.obj().video_info().unwrap(); + let (output_width, output_height) = self.mixer_pad_get_output_size(pad, out_info.par()); + pad_rect.w = output_width as i32; + pad_rect.h = output_height as i32; + + if !self.is_rectangle_contained(rect, &pad_rect) { + return false; + } + + true + } + + fn is_rectangle_contained( + &self, + rect1: &gst_video::VideoRectangle, + rect2: &gst_video::VideoRectangle, + ) -> bool { + rect2.x <= rect1.x + && rect2.y <= rect1.y + && rect2.x + rect2.w >= rect1.x + rect1.w + && rect2.y + rect2.h >= rect1.y + rect1.h + } + + fn mixer_pad_get_output_size( + &self, + pad: &SkiaCompositorPad, + out_par: gst::Fraction, + ) -> (f32, f32) { + let mut pad_width; + let mut pad_height; + + let obj = self.obj(); + let video_info = obj.video_info().unwrap(); + pad_width = if pad.width() <= 0. { + video_info.width() as f32 + } else { + pad.width() + }; + pad_height = if pad.height() <= 0. { + video_info.height() as f32 + } else { + pad.height() + }; + + if pad_width == 0. || pad_height == 0. { + return (0., 0.); + } + + let dar = match gst_video::calculate_display_ratio( + pad_width as u32, + pad_height as u32, + video_info.par(), + out_par, + ) { + None => return (0., 0.), + Some(dar) => dar, + }; + + if pad_height % dar.numer() as f32 == 0. { + pad_width = pad_height * dar.numer() as f32 / dar.denom() as f32; + } else if pad_width % dar.denom() as f32 == 0. { + pad_height = pad_width * dar.denom() as f32 / dar.numer() as f32; + } else { + pad_width = pad_height * dar.numer() as f32 / dar.denom() as f32; + } + + (pad_width, pad_height) + } + + fn draw_background(&self, canvas: &skia::Canvas, info: &gst_video::VideoInfo) { + let mut paint = skia::Paint::default(); + match *self.background.lock().unwrap() { + Background::Black => paint.set_color(skia::Color::BLACK), + Background::White => paint.set_color(skia::Color::WHITE), + Background::Transparent => paint.set_color(skia::Color::TRANSPARENT), + Background::Checker => { + let square_size: f32 = 10.; + let size = canvas.base_layer_size(); + + for i in 0..(size.width / square_size as i32) { + for j in 0..(size.height / square_size as i32) { + let is_even = (i + j) % 2 == 0; + paint.set_color(if is_even { + skia::Color::DARK_GRAY + } else { + skia::Color::GRAY + }); + + let x = i as f32 * square_size; + let y = j as f32 * square_size; + + let rect = skia::Rect::from_xywh(x, y, square_size, square_size); + canvas.draw_rect(rect, &paint); + } + } + + return; + } + }; + paint.set_style(skia::paint::Style::Fill); + paint.set_anti_alias(true); + + canvas.draw_rect( + skia::Rect::from_xywh(0., 0., info.width() as f32, info.height() as f32), + &paint, + ); + } +} + +#[glib::object_subclass] +impl ObjectSubclass for SkiaCompositor { + const NAME: &'static str = "GstSkiaCompositor"; + type Type = super::SkiaCompositor; + type ParentType = gst_video::VideoAggregator; + type Interfaces = (gst::ChildProxy,); +} + +#[glib::derived_properties] +impl ObjectImpl for SkiaCompositor {} +impl GstObjectImpl for SkiaCompositor {} + +impl ElementImpl for SkiaCompositor { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: std::sync::OnceLock = + std::sync::OnceLock::new(); + + Some(ELEMENT_METADATA.get_or_init(|| { + gst::subclass::ElementMetadata::new( + "Skia Compositor", + "Compositor/Video", + "Skia based compositor", + "Thibault Saunier , Sebastian Dröge ", + ) + })) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: std::sync::OnceLock> = + std::sync::OnceLock::new(); + + PAD_TEMPLATES.get_or_init(|| { + vec![ + gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + // Support formats supported by Skia and GStreamer on the src side + &gst_video::VideoCapsBuilder::new() + .format_list(video_format::gst_formats()) + .build(), + ) + .unwrap(), + gst::PadTemplate::with_gtype( + "sink_%u", + gst::PadDirection::Sink, + gst::PadPresence::Request, + // Support all formats as inputs will be converted to the output format + // automatically by the VideoAggregatorConvertPad base class + &gst_video::VideoCapsBuilder::new().build(), + SkiaCompositorPad::static_type(), + ) + .unwrap(), + ] + }) + } + + // Notify via the child proxy interface whenever a new pad is added or removed. + fn request_new_pad( + &self, + templ: &gst::PadTemplate, + name: Option<&str>, + caps: Option<&gst::Caps>, + ) -> Option { + let element = self.obj(); + let pad = self.parent_request_new_pad(templ, name, caps)?; + element.child_added(&pad, &pad.name()); + Some(pad) + } + + fn release_pad(&self, pad: &gst::Pad) { + let element = self.obj(); + element.child_removed(pad, &pad.name()); + self.parent_release_pad(pad); + } +} + +// Implementation of gst_base::Aggregator virtual methods. +impl AggregatorImpl for SkiaCompositor { + fn sink_query( + &self, + aggregator_pad: &gst_base::AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryViewMut; + + match query.view_mut() { + QueryViewMut::Caps(q) => { + let caps = aggregator_pad.pad_template_caps(); + let filter = q.filter(); + + let caps = if let Some(filter) = filter { + filter.intersect_with_mode(&caps, gst::CapsIntersectMode::First) + } else { + caps + }; + + q.set_result(&caps); + + true + } + QueryViewMut::AcceptCaps(q) => { + let caps = q.caps(); + let template_caps = aggregator_pad.pad_template_caps(); + let res = caps.is_subset(&template_caps); + q.set_result(res); + + true + } + _ => self.parent_sink_query(aggregator_pad, query), + } + } +} + +impl VideoAggregatorImpl for SkiaCompositor { + fn aggregate_frames( + &self, + token: &gst_video::subclass::AggregateFramesToken, + outbuf: &mut gst::BufferRef, + ) -> Result { + let obj = self.obj(); + + // Map the output frame writable. + let out_info = obj.video_info().unwrap(); + + let mut mapped_mem = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?; + + let width = out_info.width() as i32; + let height = out_info.height() as i32; + let out_img_info = skia::ImageInfo::new( + skia::ISize { width, height }, + video_format::gst_to_skia(out_info.format()).unwrap(), + skia::AlphaType::Unpremul, + None, + ); + let draw_background = self.should_draw_background(token); + let mut pads_to_draw = Vec::with_capacity(obj.num_sink_pads() as usize); + obj.foreach_sink_pad(|_obj, pad| { + let pad = pad.downcast_ref::().unwrap(); + let frame = match pad.prepared_frame(token) { + Some(frame) => frame, + None => return ControlFlow::Continue(()), + }; + + if pad.alpha() == 0. { + return ControlFlow::Continue(()); + } + + if pads_to_draw.is_empty() + && !draw_background + && out_info.width() == frame.width() + && out_info.height() == frame.height() + && out_info.format() == frame.info().format() + { + gst::trace!(CAT, imp = self, "Copying frame directly to output buffer"); + mapped_mem.copy_from_slice(frame.plane_data(0).unwrap()); + + return ControlFlow::Continue(()); + } + + pads_to_draw.push((pad.clone(), frame)); + + ControlFlow::Continue(()) + }); + + let mut surface = + skia::surface::surfaces::wrap_pixels(&out_img_info, &mut mapped_mem, None, None) + .ok_or(gst::FlowError::Error)?; + + let canvas = surface.canvas(); + if draw_background { + self.draw_background(canvas, &out_info); + } + + for (pad, frame) in pads_to_draw { + let mut paint = skia::Paint::default(); + paint.set_anti_alias(pad.anti_alias()); + paint.set_blend_mode(pad.operator().into()); + paint.set_alpha_f(pad.alpha() as f32); + let img_info = skia::ImageInfo::new( + skia::ISize { + width: frame.width() as i32, + height: frame.height() as i32, + }, + video_format::gst_to_skia(frame.info().format()).unwrap(), + skia::AlphaType::Unpremul, + None, + ); + + // SAFETY: We own the data throughout all the drawing process as we own a readable + // reference on the underlying GStreamer buffer + let image = unsafe { + skia::image::images::raster_from_data( + &img_info, + skia::Data::new_bytes(frame.plane_data(0).unwrap()), + frame.info().stride()[0] as usize, + ) + } + .expect("Wrong image parameters to raster from data."); + + let mut desired_width = pad.width(); + if desired_width <= 0. { + desired_width = frame.width() as f32; + } + let mut desired_height = pad.height(); + if desired_height <= 0. { + desired_height = frame.height() as f32; + } + let src_rect = skia::Rect::from_wh(frame.width() as f32, frame.height() as f32); // Source rectangle + let dst_rect = + skia::Rect::from_xywh(pad.xpos(), pad.ypos(), desired_width, desired_height); + gst::log!( + CAT, + imp = self, + "Drawing frame from pad {} at {:?} to {:?}", + pad.name(), + src_rect, + dst_rect + ); + canvas.draw_image_rect( + image, + Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), + dst_rect, + &paint, + ); + } + drop(surface); + + Ok(gst::FlowSuccess::Ok) + } +} + +impl ChildProxyImpl for SkiaCompositor { + fn children_count(&self) -> u32 { + let object = self.obj(); + object.num_sink_pads() as u32 + } + + fn child_by_name(&self, name: &str) -> Option { + let object = self.obj(); + object + .sink_pads() + .into_iter() + .find(|p| p.name() == name) + .map(|p| p.upcast()) + } + + fn child_by_index(&self, index: u32) -> Option { + let object = self.obj(); + object + .pads() + .into_iter() + .nth(index as usize) + .map(|p| p.upcast()) + } +} diff --git a/video/skia/src/compositor/mod.rs b/video/skia/src/compositor/mod.rs new file mode 100644 index 000000000..7e0687b25 --- /dev/null +++ b/video/skia/src/compositor/mod.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; +use gst::prelude::*; + +mod imp; +mod pad; + +#[cfg(feature = "doc")] +pub use imp::Background; + +#[cfg(feature = "doc")] +pub use pad::Operator; + +glib::wrapper! { + pub struct SkiaCompositor(ObjectSubclass) @extends gst_video::VideoAggregator, gst_base::Aggregator, gst::Element, gst::Object, @implements gst::ChildProxy; +} + +glib::wrapper! { + pub struct SkiaCompositorPad(ObjectSubclass) @extends gst_video::VideoAggregatorConvertPad, gst_video::VideoAggregatorPad, gst_base::AggregatorPad, gst::Pad, gst::Object; +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "skiacompositor", + gst::Rank::SECONDARY, + SkiaCompositor::static_type(), + ) +} diff --git a/video/skia/src/compositor/pad.rs b/video/skia/src/compositor/pad.rs new file mode 100644 index 000000000..9cd6c9cbb --- /dev/null +++ b/video/skia/src/compositor/pad.rs @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MPL-2.0 +use gst::glib::Properties; +use gst_base::subclass::prelude::*; +use gst_video::{prelude::*, subclass::prelude::*}; +use std::sync::Mutex; + +use super::*; + +#[derive(Clone, Debug)] +pub struct Settings { + alpha: f64, + xpos: f32, + ypos: f32, + width: f32, + height: f32, + anti_alias: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + alpha: 1.0, + xpos: 0.0, + ypos: 0.0, + width: -1.0, + height: -1.0, + anti_alias: true, + } + } +} + +#[derive(glib::Enum, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)] +#[enum_type(name = "GstSkiaCompositorPadOperator")] +#[repr(u32)] +// Trying to exactly match names and value of compositor:operator +pub enum Operator { + Source, + #[default] + Over, + Add, + Dest, + Clear, + DestOver, + SourceIn, + DestIn, + SourceOut, + DestOut, + SourceATop, + DestATop, + Xor, + Modulate, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Multiply, + Hue, + Saturation, + Color, + Luminosity, +} + +impl From for skia::BlendMode { + fn from(val: Operator) -> Self { + match val { + Operator::Clear => skia::BlendMode::Clear, + Operator::Source => skia::BlendMode::Src, + Operator::Dest => skia::BlendMode::Dst, + Operator::Over => skia::BlendMode::SrcOver, + Operator::DestOver => skia::BlendMode::DstOver, + Operator::SourceIn => skia::BlendMode::SrcIn, + Operator::DestIn => skia::BlendMode::DstIn, + Operator::SourceOut => skia::BlendMode::SrcOut, + Operator::DestOut => skia::BlendMode::DstOut, + Operator::SourceATop => skia::BlendMode::SrcATop, + Operator::DestATop => skia::BlendMode::DstATop, + Operator::Xor => skia::BlendMode::Xor, + Operator::Add => skia::BlendMode::Plus, + Operator::Modulate => skia::BlendMode::Modulate, + Operator::Screen => skia::BlendMode::Screen, + Operator::Overlay => skia::BlendMode::Overlay, + Operator::Darken => skia::BlendMode::Darken, + Operator::Lighten => skia::BlendMode::Lighten, + Operator::ColorDodge => skia::BlendMode::ColorDodge, + Operator::ColorBurn => skia::BlendMode::ColorBurn, + Operator::HardLight => skia::BlendMode::HardLight, + Operator::SoftLight => skia::BlendMode::SoftLight, + Operator::Difference => skia::BlendMode::Difference, + Operator::Exclusion => skia::BlendMode::Exclusion, + Operator::Multiply => skia::BlendMode::Multiply, + Operator::Hue => skia::BlendMode::Hue, + Operator::Saturation => skia::BlendMode::Saturation, + Operator::Color => skia::BlendMode::Color, + Operator::Luminosity => skia::BlendMode::Luminosity, + } + } +} + +#[derive(Properties, Debug, Default)] +#[properties(wrapper_type = super::SkiaCompositorPad)] +pub struct SkiaCompositorPad { + #[property( + get, + set, + name = "alpha", + nick = "Alpha", + blurb = "Alpha value of the input", + minimum = 0.0, + maximum = 1.0, + type = f64, + member = alpha, + )] + #[property(get, set, type = f32, name= "xpos", nick = "X Position", blurb = "Horizontal position of the input", minimum = f32::MIN, maximum = f32::MAX, member = xpos)] + #[property(get, set, type = f32, name = "ypos", nick = "Y Position", blurb = "Vertical position of the input", minimum = f32::MIN, maximum = f32::MAX, member = ypos)] + #[property(get, set, type = f32, name = "width", nick = "Width", blurb = "Width of the picture", minimum = -1.0, maximum = f32::MAX, member = width, default = -1.0)] + #[property(get, set, type = f32, name = "height", nick = "Height", blurb = "Height of the picture", minimum = -1.0, maximum = f32::MAX, member = height, default = -1.0)] + #[property(get, set, type = bool, name = "anti-alias", nick = "Anti-alias", blurb = "Whether to use anti-aliasing", member = anti_alias, default = true)] + pub settings: Mutex, + #[property( + get, + set, + name = "operator", + nick = "Operator", + blurb = "Blending operator to use for blending this pad over the previous ones", + builder(Operator::Over) + )] + operator: Mutex, +} + +// This trait registers our type with the GObject object system and +// provides the entry points for creating a new instance and setting +// up the class data. +#[glib::object_subclass] +impl ObjectSubclass for SkiaCompositorPad { + const NAME: &'static str = "GstSkiaCompositorPad"; + type Type = super::SkiaCompositorPad; + type ParentType = gst_video::VideoAggregatorConvertPad; +} + +#[glib::derived_properties] +impl ObjectImpl for SkiaCompositorPad {} +impl GstObjectImpl for SkiaCompositorPad {} +impl PadImpl for SkiaCompositorPad {} +impl AggregatorPadImpl for SkiaCompositorPad {} +impl VideoAggregatorPadImpl for SkiaCompositorPad {} +impl VideoAggregatorConvertPadImpl for SkiaCompositorPad {} diff --git a/video/skia/src/lib.rs b/video/skia/src/lib.rs new file mode 100644 index 000000000..b50b3aa2c --- /dev/null +++ b/video/skia/src/lib.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MPL-2.0 + +#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)] + +/** + * plugin-skia: + * + * Since: plugins-rs-0.14.0 + */ +use gst::glib; + +mod compositor; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + compositor::register(plugin)?; + #[cfg(feature = "doc")] + { + use gst::prelude::*; + + compositor::Background::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); + compositor::SkiaCompositorPad::static_type() + .mark_as_plugin_api(gst::PluginAPIFlags::empty()); + compositor::Operator::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); + } + Ok(()) +} + +gst::plugin_define!( + skia, + 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/skia/tests/compositor.rs b/video/skia/tests/compositor.rs new file mode 100644 index 000000000..8a7813042 --- /dev/null +++ b/video/skia/tests/compositor.rs @@ -0,0 +1,56 @@ +// Copyright (C) 2024 Thibault Saunier +#![allow(clippy::single_match)] + +use gst::prelude::*; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstskia::plugin_register_static().expect("skia test"); + }); +} + +#[test] +fn test_simple() { + init(); + + let mut h = gst_check::Harness::with_padnames("skiacompositor", Some("sink_0"), Some("src")); + + let video_info = gst_video::VideoInfo::builder(gst_video::VideoFormat::Rgba, 160, 120) + .fps((10, 1)) + .build() + .unwrap(); + let caps = video_info.to_caps().unwrap(); + + h.set_sink_caps(caps.clone()); + h.set_src_caps(caps); + + for pts in 0..5 { + let buffer = { + let mut buffer = gst::Buffer::with_size(video_info.size()).unwrap(); + { + let buffer = buffer.get_mut().unwrap(); + buffer.set_pts(pts.seconds()); + } + let mut vframe = + gst_video::VideoFrame::from_buffer_writable(buffer, &video_info).unwrap(); + for v in vframe.plane_data_mut(0).unwrap() { + *v = 128; + } + vframe.into_buffer() + }; + h.push(buffer.clone()).unwrap(); + } + h.push_event(gst::event::Eos::new()); + + for i in 0..6 { + let buffer = h.pull().unwrap(); + assert_eq!( + buffer.pts(), + Some(gst::ClockTime::from_seconds_f64(1. / 10. * i as f64)) + ) + } +}