From 6ba49c4dcecfe6cf0b2224706713f51786eb46cd Mon Sep 17 00:00:00 2001 From: Carlos Bentzen Date: Wed, 5 Mar 2025 10:14:24 +0100 Subject: [PATCH] vvdec: add VVdeC VVC/H.266 decoder element Part-of: --- Cargo.lock | 55 +- Cargo.toml | 1 + docs/plugins/gst_plugins_cache.json | 68 +++ meson.build | 4 + meson_options.txt | 1 + video/vvdec/Cargo.toml | 43 ++ video/vvdec/LICENSE-MPL-2.0 | 373 ++++++++++++++ video/vvdec/build.rs | 3 + video/vvdec/src/dec/imp.rs | 694 ++++++++++++++++++++++++++ video/vvdec/src/dec/mod.rs | 25 + video/vvdec/src/lib.rs | 33 ++ video/vvdec/tests/vvc_yuv420p10le.mkv | Bin 0 -> 1935 bytes video/vvdec/tests/vvc_yuv420p10le.ref | Bin 0 -> 57600 bytes video/vvdec/tests/vvdec.rs | 58 +++ 14 files changed, 1357 insertions(+), 1 deletion(-) create mode 100644 video/vvdec/Cargo.toml create mode 100644 video/vvdec/LICENSE-MPL-2.0 create mode 100644 video/vvdec/build.rs create mode 100644 video/vvdec/src/dec/imp.rs create mode 100644 video/vvdec/src/dec/mod.rs create mode 100644 video/vvdec/src/lib.rs create mode 100644 video/vvdec/tests/vvc_yuv420p10le.mkv create mode 100644 video/vvdec/tests/vvc_yuv420p10le.ref create mode 100644 video/vvdec/tests/vvdec.rs diff --git a/Cargo.lock b/Cargo.lock index 05f482dff..a87a171c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,7 +341,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9dd2e03ee80ca2822dd6ea431163d2ef259f2066a4d6ccaca6d9dcb386aa43" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -904,6 +904,26 @@ dependencies = [ "which", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.99", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -3294,6 +3314,18 @@ dependencies = [ "rgb", ] +[[package]] +name = "gst-plugin-vvdec" +version = "0.14.0-alpha.1" +dependencies = [ + "gst-plugin-version-helper", + "gstreamer", + "gstreamer-audio", + "gstreamer-check", + "gstreamer-video", + "vvdec", +] + [[package]] name = "gst-plugin-webp" version = "0.14.0-alpha.1" @@ -8155,6 +8187,27 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vvdec" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4dd0b089dddc930150dbce1f3222020d3c8f6d3c6d81ffea3492d913a1a195" +dependencies = [ + "thiserror 2.0.12", + "vvdec-sys", +] + +[[package]] +name = "vvdec-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f825818f7e4770c5807f26b6230d2ec369f8e8a35f00c0d081de519e7917db" +dependencies = [ + "bindgen 0.70.1", + "cmake", + "system-deps 7.0.3", +] + [[package]] name = "waker-fn" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index e0034a90d..8928bfa24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ members = [ "video/png", "video/rav1e", "video/videofx", + "video/vvdec", "video/webp", ] diff --git a/docs/plugins/gst_plugins_cache.json b/docs/plugins/gst_plugins_cache.json index 7d37daed6..84e01d182 100644 --- a/docs/plugins/gst_plugins_cache.json +++ b/docs/plugins/gst_plugins_cache.json @@ -16751,6 +16751,74 @@ "tracers": {}, "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" }, + "vvdec": { + "description": "GStreamer VVdeC VVC/H.266 decoder plugin", + "elements": { + "vvdec": { + "author": "Carlos Bentzen ", + "description": "Decode VVC/H.266 video streams with VVdeC", + "hierarchy": [ + "GstVVdeC", + "GstVideoDecoder", + "GstElement", + "GstObject", + "GInitiallyUnowned", + "GObject" + ], + "klass": "Codec/Decoder/Video", + "pad-templates": { + "sink": { + "caps": "video/x-h266:\n stream-format: byte-stream\n alignment: au\n", + "direction": "sink", + "presence": "always" + }, + "src": { + "caps": "video/x-raw:\n format: { GRAY8, I420, Y42B, Y444, GRAY10_LE16, I420_10LE, I422_10LE, Y444_10LE }\n width: [ 1, 2147483647 ]\n height: [ 1, 2147483647 ]\n framerate: [ 0/1, 2147483647/1 ]\n", + "direction": "src", + "presence": "always" + } + }, + "properties": { + "n-parser-threads": { + "blurb": "Number of parser threads to use while decoding (set to -1 to let libvvdec choose the number of parser threads, set to 0 to parse on the element streaming thread)", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "-1", + "max": "2147483647", + "min": "-1", + "mutable": "null", + "readable": true, + "type": "gint", + "writable": true + }, + "n-threads": { + "blurb": "Number of threads to use while decoding (set to -1 to use the number of logical cores, set to 0 to decode in a single thread)", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "-1", + "max": "2147483647", + "min": "-1", + "mutable": "null", + "readable": true, + "type": "gint", + "writable": true + } + }, + "rank": "secondary" + } + }, + "filename": "gstvvdec", + "license": "MPL-2.0", + "other-types": {}, + "package": "gst-plugin-vvdec", + "source": "gst-plugin-vvdec", + "tracers": {}, + "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" + }, "webrtchttp": { "description": "GStreamer WebRTC Plugin for WebRTC HTTP protocols (WHIP/WHEP)", "elements": { diff --git a/meson.build b/meson.build index 83e92ab7d..f4e69ca1d 100644 --- a/meson.build +++ b/meson.build @@ -222,6 +222,10 @@ plugins = { ], }, 'speechmatics': {'library': 'libgstspeechmatics'}, + 'vvdec': { + 'library': 'libgstvvdec', + 'extra-deps': {'libvvdec': ['>= 3.0']} + } } # Won't build on platforms where it bundles the sources because of: diff --git a/meson_options.txt b/meson_options.txt index 448ad1dec..d75770ee4 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -67,6 +67,7 @@ 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('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') # Common options diff --git a/video/vvdec/Cargo.toml b/video/vvdec/Cargo.toml new file mode 100644 index 000000000..88397d562 --- /dev/null +++ b/video/vvdec/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "gst-plugin-vvdec" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors = ["Carlos Bentzen "] +repository.workspace = true +license = "MPL-2.0" +description = "GStreamer VVdeC VVC/H.266 decoder plugin" + +[dependencies] +gst.workspace = true +gst-audio.workspace = true +gst-video = { workspace = true, features = ["v1_26"] } +vvdec = { version = "0.6" } + +[dev-dependencies] +gst-check.workspace = true + +[lib] +name = "gstvvdec" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper.workspace = true + +[features] +capi = [] + +[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, libvvdec >= 3.0" diff --git a/video/vvdec/LICENSE-MPL-2.0 b/video/vvdec/LICENSE-MPL-2.0 new file mode 100644 index 000000000..14e2f777f --- /dev/null +++ b/video/vvdec/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/vvdec/build.rs b/video/vvdec/build.rs new file mode 100644 index 000000000..cda12e57e --- /dev/null +++ b/video/vvdec/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/video/vvdec/src/dec/imp.rs b/video/vvdec/src/dec/imp.rs new file mode 100644 index 000000000..551cb17d8 --- /dev/null +++ b/video/vvdec/src/dec/imp.rs @@ -0,0 +1,694 @@ +// Copyright (C) 2025 Carlos Bentzen +// +// 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 + +/** + * SECTION:element-vvdec + * + * #vvdec is a decoder element for VVC/H.266 video streams using the VVdeC decoder. + * + * ## Example pipeline + * + * |[ + * gst-launch filesrc location=vvc.mp4 ! qtdemux ! h266parse ! vvdec ! videoconvert ! autovideosink + * ]| + * + * Since: plugins-rs-0.14.0 + */ +use std::sync::{Mutex, MutexGuard}; + +use gst::glib::{self, Properties}; +use gst::subclass::prelude::*; +use gst_video::prelude::*; +use gst_video::subclass::prelude::*; +use std::sync::LazyLock; +use vvdec::AccessUnit; + +const DEFAULT_N_THREADS: i32 = -1; +const DEFAULT_N_PARSER_THREADS: i32 = -1; + +static CAT: LazyLock = LazyLock::new(|| { + gst::DebugCategory::new( + "vvdec", + gst::DebugColorFlags::empty(), + Some("VVdeC VVC/H.266 decoder"), + ) +}); + +struct State { + decoder: vvdec::Decoder, + video_meta_supported: bool, + output_info: Option, + input_state: gst_video::VideoCodecState<'static, gst_video::video_codec_state::Readable>, +} + +#[derive(Debug)] +struct Settings { + n_threads: i32, + parse_delay: i32, +} + +impl Default for Settings { + fn default() -> Self { + Self { + n_threads: DEFAULT_N_THREADS, + parse_delay: DEFAULT_N_PARSER_THREADS, + } + } +} + +impl Settings { + fn create_decoder(&self) -> Result { + vvdec::Decoder::builder() + .num_threads(self.n_threads) + .parse_delay(self.parse_delay) + .build() + } +} + +#[derive(Default, Properties)] +#[properties(wrapper_type = super::VVdeC)] +pub struct VVdeC { + state: Mutex>, + #[property(name = "n-threads", get, set, type = i32, member = n_threads, minimum = -1, default = DEFAULT_N_THREADS, blurb = "Number of threads to use while decoding (set to -1 to use the number of logical cores, set to 0 to decode in a single thread)")] + #[property(name = "n-parser-threads", get, set, type = i32, member = parse_delay, minimum = -1, default = DEFAULT_N_PARSER_THREADS, blurb = "Number of parser threads to use while decoding (set to -1 to let libvvdec choose the number of parser threads, set to 0 to parse on the element streaming thread)")] + settings: Mutex, +} + +type StateGuard<'s> = MutexGuard<'s, Option>; + +impl VVdeC { + fn create_decoder(&self) -> Result { + self.settings.lock().unwrap().create_decoder() + } + + fn decode<'s>( + &'s self, + mut state_guard: StateGuard<'s>, + input_buffer: gst::Buffer, + frame: gst_video::VideoCodecFrame, + ) -> Result<(), gst::FlowError> { + let state = state_guard.as_mut().ok_or(gst::FlowError::Flushing)?; + + // VVdeC doesn't have a mechanism for passing opaque data per-frame. + // But we can store system_frame_number via the cts field, since it's not + // used in the decoder but the value is returned in the matching decoded frames. + let cts = frame.system_frame_number() as u64; + let dts = input_buffer.dts().map(|ts| *ts); + let is_random_access_point = frame + .flags() + .contains(gst_video::VideoCodecFrameFlags::SYNC_POINT); + let payload = input_buffer + .into_mapped_buffer_readable() + .map_err(|_| gst::FlowError::Error)?; + + let access_unit = AccessUnit { + payload, + cts: Some(cts), + dts, + is_random_access_point, + }; + + match state.decoder.decode(access_unit) { + Ok(Some(frame)) => { + drop(self.handle_decoded_frame(state_guard, &frame)?); + } + Ok(None) => (), + Err(vvdec::Error::TryAgain) => { + gst::trace!(CAT, imp = self, "Decoder returned TryAgain"); + } + Err(vvdec::Error::DecInput) => { + gst_video::video_decoder_error!( + &*self.obj(), + 1, + gst::StreamError::Decode, + ["Discarding frame {} because it's undecodable", cts] + )?; + drop(state_guard); + self.obj().release_frame(frame); + } + Err(vvdec::Error::RestartRequired) => { + gst::warning!(CAT, imp = self, "decoder requires restart"); + state_guard = self.forward_pending_frames(state_guard)?; + let state = state_guard.as_mut().ok_or(gst::FlowError::Flushing)?; + state.decoder = self.create_decoder().map_err(|_| { + gst::error!(CAT, imp = self, "Failed to create new decoder instance"); + gst::FlowError::Error + })?; + } + Err(err) => { + gst::error!(CAT, imp = self, "decoder returned error: {err}"); + return Err(gst::FlowError::Error); + } + } + + Ok(()) + } + + fn handle_decoded_frame<'s>( + &'s self, + state_guard: StateGuard<'s>, + decoded_frame: &vvdec::Frame, + ) -> Result, gst::FlowError> { + let system_frame_number = decoded_frame.cts().expect("frames were pushed with cts"); + gst::trace!( + CAT, + imp = self, + "Handling decoded frame {system_frame_number}" + ); + + let state_guard = self.handle_resolution_changes(state_guard, decoded_frame)?; + let instance = self.obj(); + + let frame = instance.frame(system_frame_number.try_into().unwrap()); + if let Some(mut frame) = frame { + self.set_decoded_frame_as_output_buffer(state_guard, decoded_frame, &mut frame)?; + instance.finish_frame(frame)?; + gst::trace!(CAT, imp = self, "Finished frame {system_frame_number}"); + Ok(self.state.lock().unwrap()) + } else { + gst::warning!( + CAT, + imp = self, + "No frame found for system frame number {system_frame_number}" + ); + Ok(state_guard) + } + } + + fn handle_resolution_changes<'s>( + &'s self, + mut state_guard: StateGuard<'s>, + frame: &vvdec::Frame, + ) -> Result, gst::FlowError> { + let format = self.gst_video_format_from_vvdec_frame(frame); + if format == gst_video::VideoFormat::Unknown { + return Err(gst::FlowError::NotNegotiated); + } + + let state = state_guard.as_mut().ok_or(gst::FlowError::Flushing)?; + let need_negotiate = { + match state.output_info { + Some(ref i) => { + (i.width() != frame.width()) + || (i.height() != frame.height() || (i.format() != format)) + } + None => true, + } + }; + if !need_negotiate { + return Ok(state_guard); + } + + gst::info!( + CAT, + imp = self, + "Negotiating format {} frame dimensions {}x{}", + format, + frame.width(), + frame.height() + ); + + let input_state = state.input_state.clone(); + drop(state_guard); + + let instance = self.obj(); + + let interlace_mode = match frame.frame_format() { + vvdec::FrameFormat::TopBottom | vvdec::FrameFormat::BottomTop => { + gst_video::VideoInterlaceMode::Interleaved + } + vvdec::FrameFormat::TopField | vvdec::FrameFormat::BottomField => { + gst_video::VideoInterlaceMode::Alternate + } + vvdec::FrameFormat::Progressive => gst_video::VideoInterlaceMode::Progressive, + _ => { + gst::error!( + CAT, + imp = self, + "Unsupported VVdeC frame format {:?}", + frame.frame_format() + ); + return Err(gst::FlowError::NotNegotiated); + } + }; + + let mut output_state_height = frame.height(); + if interlace_mode == gst_video::VideoInterlaceMode::Alternate { + output_state_height *= 2; + } + + let output_state = instance.set_interlaced_output_state( + format, + interlace_mode, + frame.width(), + output_state_height, + Some(&input_state), + )?; + instance.negotiate(output_state)?; + let out_state = instance.output_state().unwrap(); + + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().ok_or(gst::FlowError::Flushing)?; + state.output_info = Some(out_state.info()); + + gst::trace!(CAT, imp = self, "Negotiated format"); + + Ok(state_guard) + } + + fn gst_video_format_from_vvdec_frame(&self, frame: &vvdec::Frame) -> gst_video::VideoFormat { + let color_format = frame.color_format(); + let bit_depth = frame.bit_depth(); + + match (&color_format, bit_depth) { + (vvdec::ColorFormat::Yuv400Planar, 8) => gst_video::VideoFormat::Gray8, + (vvdec::ColorFormat::Yuv420Planar, 8) => gst_video::VideoFormat::I420, + (vvdec::ColorFormat::Yuv422Planar, 8) => gst_video::VideoFormat::Y42b, + (vvdec::ColorFormat::Yuv444Planar, 8) => gst_video::VideoFormat::Y444, + #[cfg(target_endian = "little")] + (vvdec::ColorFormat::Yuv400Planar, 10) => gst_video::VideoFormat::Gray10Le16, + #[cfg(target_endian = "little")] + (vvdec::ColorFormat::Yuv420Planar, 10) => gst_video::VideoFormat::I42010le, + #[cfg(target_endian = "little")] + (vvdec::ColorFormat::Yuv422Planar, 10) => gst_video::VideoFormat::I42210le, + #[cfg(target_endian = "little")] + (vvdec::ColorFormat::Yuv444Planar, 10) => gst_video::VideoFormat::Y44410le, + #[cfg(target_endian = "big")] + (vvdec::ColorFormat::Yuv420Planar, 10) => gst_video::VideoFormat::I42010be, + #[cfg(target_endian = "big")] + (vvdec::ColorFormat::Yuv422Planar, 10) => gst_video::VideoFormat::I42210be, + #[cfg(target_endian = "big")] + (vvdec::ColorFormat::Yuv444Planar, 10) => gst_video::VideoFormat::Y44410be, + _ => { + gst::warning!( + CAT, + imp = self, + "Unsupported VVdeC format {:?}/{:?}", + color_format, + bit_depth + ); + gst_video::VideoFormat::Unknown + } + } + } + + fn forward_pending_frames<'s>( + &'s self, + mut state_guard: StateGuard<'s>, + ) -> Result, gst::FlowError> { + loop { + let state = state_guard.as_mut().ok_or(gst::FlowError::Flushing)?; + match state.decoder.flush() { + Ok(Some(frame)) => { + gst::trace!(CAT, imp = self, "Forwarding pending frame."); + state_guard = self.handle_decoded_frame(state_guard, &frame)?; + } + Ok(None) | Err(vvdec::Error::RestartRequired) => break, + Err(err) => { + gst::error!( + CAT, + imp = self, + "Decoder returned error while flushing: {err}" + ); + return Err(gst::FlowError::Error); + } + }; + } + Ok(state_guard) + } + + fn flush_decoder(&self, state: &mut State) -> bool { + loop { + match state.decoder.flush() { + Ok(Some(_)) => continue, + Ok(None) | Err(vvdec::Error::RestartRequired) | Err(vvdec::Error::Eof) => break, + Err(err) => { + gst::error!( + CAT, + imp = self, + "Decoder returned error while flushing: {err}" + ); + return false; + } + } + } + true + } + + fn set_decoded_frame_as_output_buffer<'s>( + &'s self, + mut state_guard: StateGuard<'s>, + frame: &vvdec::Frame, + codec_frame: &mut gst_video::VideoCodecFrame, + ) -> Result<(), gst::FlowError> { + let state = state_guard.as_mut().expect("state is set"); + let video_meta_supported = state.video_meta_supported; + drop(state_guard); + + let mut out_buffer = video_meta_supported.then(gst::Buffer::new); + let mut_buffer = if video_meta_supported { + out_buffer.as_mut().unwrap().get_mut().unwrap() + } else { + self.obj().allocate_output_frame(codec_frame, None)?; + codec_frame + .output_buffer_mut() + .expect("output_buffer is set") + }; + + state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().expect("state is set"); + let info = state.output_info.as_ref().expect("output_info is set"); + + let color_format = frame.color_format(); + if color_format == vvdec::ColorFormat::Invalid { + gst::error!(CAT, imp = self, "Invalid color format"); + return Err(gst::FlowError::Error); + } + + let frame_format = frame.frame_format(); + let video_flags = match frame_format { + vvdec::FrameFormat::Progressive => None, + vvdec::FrameFormat::TopField => Some( + gst_video::VideoBufferFlags::INTERLACED | gst_video::VideoBufferFlags::TOP_FIELD, + ), + vvdec::FrameFormat::BottomField => Some( + gst_video::VideoBufferFlags::INTERLACED | gst_video::VideoBufferFlags::BOTTOM_FIELD, + ), + vvdec::FrameFormat::TopBottom => { + Some(gst_video::VideoBufferFlags::INTERLACED | gst_video::VideoBufferFlags::TFF) + } + vvdec::FrameFormat::BottomTop => Some(gst_video::VideoBufferFlags::INTERLACED), + _ => { + gst::error!( + CAT, + imp = self, + "Unsupported VVdeC frame format {:?}", + frame_format + ); + return Err(gst::FlowError::Error); + } + }; + if let Some(video_flags) = video_flags { + mut_buffer.set_video_flags(video_flags); + } + + let components = if color_format == vvdec::ColorFormat::Yuv400Planar { + const GRAY_COMPONENTS: [vvdec::PlaneComponent; 1] = [vvdec::PlaneComponent::Y]; + &GRAY_COMPONENTS[..] + } else { + const YUV_COMPONENTS: [vvdec::PlaneComponent; 3] = [ + vvdec::PlaneComponent::Y, + vvdec::PlaneComponent::U, + vvdec::PlaneComponent::V, + ]; + &YUV_COMPONENTS[..] + }; + + if video_meta_supported { + let mut offsets = vec![]; + let mut strides = vec![]; + let mut acc_offset: usize = 0; + + assert!(mut_buffer.size() == 0); + + for component in components { + let plane = frame + .plane(*component) + .expect("frame contains the requested plane"); + let src_stride = plane.stride(); + let mem = gst::Memory::from_slice(plane); + let mem_size = mem.size(); + mut_buffer.append_memory(mem); + + strides.push(src_stride as i32); + offsets.push(acc_offset); + acc_offset += mem_size; + } + + let frame_flags = match frame_format { + vvdec::FrameFormat::Progressive => gst_video::VideoFrameFlags::empty(), + vvdec::FrameFormat::TopField => { + gst_video::VideoFrameFlags::INTERLACED | gst_video::VideoFrameFlags::TOP_FIELD + } + vvdec::FrameFormat::BottomField => { + gst_video::VideoFrameFlags::INTERLACED + | gst_video::VideoFrameFlags::BOTTOM_FIELD + } + vvdec::FrameFormat::TopBottom => { + gst_video::VideoFrameFlags::INTERLACED | gst_video::VideoFrameFlags::TFF + } + vvdec::FrameFormat::BottomTop => gst_video::VideoFrameFlags::INTERLACED, + _ => unreachable!("frame_format is checked above"), + }; + gst_video::VideoMeta::add_full( + mut_buffer, + frame_flags, + info.format(), + info.width(), + info.height(), + &offsets, + &strides[..], + ) + .unwrap(); + + assert!(codec_frame.output_buffer().is_none()); + codec_frame.set_output_buffer(out_buffer.expect("out_buffer is set")); + } else { + assert!(mut_buffer.size() > 0); + let mut vframe = gst_video::VideoFrameRef::from_buffer_ref_writable(mut_buffer, info) + .expect("can map writable frame"); + for component in components { + let dest_stride: u32 = vframe.plane_stride()[*component as usize] as u32; + let dest_height: u32 = vframe.plane_height(*component as u32); + let dest_plane_data = vframe + .plane_data_mut(*component as u32) + .expect("can get plane data"); + let plane = frame + .plane(*component) + .expect("frame contains the requested plane"); + let src_stride = plane.stride(); + let src_slice = plane.as_ref(); + let src_height = plane.height(); + let chunk_len = std::cmp::min(src_stride, dest_stride) as usize; + + if src_stride == dest_stride && src_height == dest_height { + dest_plane_data.copy_from_slice(src_slice); + } else { + for (out_line, in_line) in dest_plane_data + .chunks_exact_mut(dest_stride.try_into().unwrap()) + .zip(src_slice.chunks_exact(src_stride.try_into().unwrap())) + { + out_line.copy_from_slice(&in_line[..chunk_len]); + } + } + } + } + + Ok(()) + } +} + +fn video_output_formats() -> impl IntoIterator { + [ + gst_video::VideoFormat::Gray8, + gst_video::VideoFormat::I420, + gst_video::VideoFormat::Y42b, + gst_video::VideoFormat::Y444, + #[cfg(target_endian = "little")] + gst_video::VideoFormat::Gray10Le16, + #[cfg(target_endian = "little")] + gst_video::VideoFormat::I42010le, + #[cfg(target_endian = "little")] + gst_video::VideoFormat::I42210le, + #[cfg(target_endian = "little")] + gst_video::VideoFormat::Y44410le, + #[cfg(target_endian = "big")] + gst_video::VideoFormat::I42010be, + #[cfg(target_endian = "big")] + gst_video::VideoFormat::I42210be, + #[cfg(target_endian = "big")] + gst_video::VideoFormat::Y44410be, + ] +} + +#[glib::object_subclass] +impl ObjectSubclass for VVdeC { + const NAME: &'static str = "GstVVdeC"; + type Type = super::VVdeC; + type ParentType = gst_video::VideoDecoder; +} + +#[glib::derived_properties] +impl ObjectImpl for VVdeC {} + +impl GstObjectImpl for VVdeC {} + +impl ElementImpl for VVdeC { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: LazyLock = LazyLock::new(|| { + gst::subclass::ElementMetadata::new( + "VVdeC VVC/H.266 Decoder", + "Codec/Decoder/Video", + "Decode VVC/H.266 video streams with VVdeC", + "Carlos Bentzen ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| { + let sink_caps = gst::Caps::builder("video/x-h266") + .field("stream-format", "byte-stream") + .field("alignment", "au") + .build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + + let src_caps = gst_video::VideoCapsBuilder::new() + .format_list(video_output_formats()) + .build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + + vec![src_pad_template, sink_pad_template] + }); + + PAD_TEMPLATES.as_ref() + } +} + +impl VideoDecoderImpl for VVdeC { + fn set_format( + &self, + input_state: &gst_video::VideoCodecState<'static, gst_video::video_codec_state::Readable>, + ) -> Result<(), gst::LoggableError> { + let mut state_guard = self.state.lock().unwrap(); + let settings = self.settings.lock().unwrap(); + + if state_guard.is_some() { + state_guard = self + .forward_pending_frames(state_guard) + .map_err(|_| gst::loggable_error!(CAT, "Failed to forward pending frames"))?; + } + + gst::trace!(CAT, imp = self, "Creating decoder with {settings:?}"); + let decoder = settings + .create_decoder() + .map_err(|_| gst::loggable_error!(CAT, "Failed to create decoder instance"))?; + + *state_guard = Some(State { + decoder, + video_meta_supported: false, + output_info: None, + input_state: input_state.clone(), + }); + + self.parent_set_format(input_state) + } + + fn handle_frame( + &self, + frame: gst_video::VideoCodecFrame, + ) -> Result { + let system_frame_number = frame.system_frame_number(); + gst::trace!(CAT, imp = self, "Decoding frame {}", system_frame_number); + + let input_buffer = frame.input_buffer_owned().expect("frame has input buffer"); + { + let state_guard = self.state.lock().unwrap(); + self.decode(state_guard, input_buffer, frame)?; + } + + Ok(gst::FlowSuccess::Ok) + } + + fn stop(&self) -> Result<(), gst::ErrorMessage> { + gst::info!(CAT, imp = self, "Stopping"); + + { + let mut state_guard = self.state.lock().unwrap(); + *state_guard = None; + } + + self.parent_stop() + } + + fn drain(&self) -> Result { + gst::info!(CAT, imp = self, "Draining"); + + { + let mut state_guard = self.state.lock().unwrap(); + if state_guard.as_mut().is_some() { + drop(self.forward_pending_frames(state_guard)?); + } + } + + self.parent_drain() + } + + fn finish(&self) -> Result { + gst::info!(CAT, imp = self, "Finishing"); + + { + let mut state_guard = self.state.lock().unwrap(); + if state_guard.as_mut().is_some() { + drop(self.forward_pending_frames(state_guard)?); + } + } + + self.parent_finish() + } + + fn flush(&self) -> bool { + gst::info!(CAT, imp = self, "Flushing"); + + { + let mut state_guard = self.state.lock().unwrap(); + if let Some(state) = state_guard.as_mut() { + return self.flush_decoder(state); + } + } + + true + } + + fn decide_allocation( + &self, + query: &mut gst::query::Allocation, + ) -> Result<(), gst::LoggableError> { + gst::trace!(CAT, imp = self, "Deciding allocation"); + self.parent_decide_allocation(query)?; + + let mut state_guard = self.state.lock().unwrap(); + if let Some(state) = state_guard.as_mut() { + state.video_meta_supported = query + .find_allocation_meta::() + .is_some(); + gst::info!( + CAT, + imp = self, + "Video meta support: {}", + state.video_meta_supported + ); + }; + + Ok(()) + } +} diff --git a/video/vvdec/src/dec/mod.rs b/video/vvdec/src/dec/mod.rs new file mode 100644 index 000000000..27deb637a --- /dev/null +++ b/video/vvdec/src/dec/mod.rs @@ -0,0 +1,25 @@ +// Copyright (C) 2025 Carlos Bentzen +// +// 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; +use gst::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct VVdeC(ObjectSubclass) @extends gst_video::VideoDecoder, gst::Element, gst::Object; +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "vvdec", + gst::Rank::SECONDARY, + VVdeC::static_type(), + ) +} diff --git a/video/vvdec/src/lib.rs b/video/vvdec/src/lib.rs new file mode 100644 index 000000000..fc133f1b7 --- /dev/null +++ b/video/vvdec/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright (C) 2025 Carlos Bentzen +// +// 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 + +/** + * plugin-vvdec: + * + * Since: plugins-rs-0.14.0 + */ +use gst::glib; + +mod dec; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + dec::register(plugin)?; + Ok(()) +} + +gst::plugin_define!( + vvdec, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MPL-2.0", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/video/vvdec/tests/vvc_yuv420p10le.mkv b/video/vvdec/tests/vvc_yuv420p10le.mkv new file mode 100644 index 0000000000000000000000000000000000000000..0297f6f76794cb3789622f5969e41c10c1b9abda GIT binary patch literal 1935 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@&d2})?9qb0 zvr7)_Z_#pa%hJpY^(^%a4a~wFQMkoR zCY)ioyzSlVQ%A&kul!x>IK9uMv%|p!;wq4T998zWxGOvETnBMAqv-WU#^R$L3x9OR z8)m<`IH!?8Y3rQk(!7+8MuxWjuz249S9ec+&tQN3urTN5Mn>hw%`HqxQ49|{H#9ET z)mSmBkty_DBLgfDm>3w67dagG&yX6=$RyAJw08kR1p@;UKLaDfEQU&kwP3-9fW{|T z48Z%)p_kf?2~bkt5-V6PrhKkX(P*l0B2Myj5IRbRY3t(J3-fD5Ya^ z;4}}XD~c6PU$eYVR<#8r9bpzzP;g>l;cDUFkOI=&0tyN)0s=ZsOe`%dl1wa4EZmAr zEFA(F3Z5)73MT|J990yLn5dq3TK}>v{<#VR12Y2)h)78i06LGGp^!{L*XM+ z17pnC7#L*+3OSicWo4;($>}E+x-RZ;2@P@#@$~m|X`gIhUK8w4j?s%!xZQ`IR-@?3Uc$qakRv@cnGLdh%P-39 zJ^I9G<~d`1qmHBpG5IaucfI!3DsV9}5NNCV>wVX*SzY~gAj6&`D|1Vi7%(V^MW4ER z`TleBOW1I*2pxcY~t*zvUCcm)ja%i7bmKTfAasW*b-e?bKBqx1-|Z ze=r(0r0==^x*~evwb_kR*wfxs{qK~T6(BIfSMl@mB(d^qjrt0=^h=Aa74|key088I zS9V3vuJ2qXmt5}1%KvWTN^DR#(G}GHv(rNETSwfvuO5kWKI~kk@Op9drs^Y8rnmQf zUzPoqDZ-#=v7YNw>m2d2U4mK09Y*iu?ZX)UZ@swhvS{L#3dL=b^Lz837|2W#nt$zJ z$_JL%wX7E++kG$eROiNA6THR$&3!>Mzq63jm&O1GPMN|5dxLYI@G}Q0ZV=$VV3_)4 zdqsK6&-hzwjh`^+x7P;GT*>gHvL%9{>H5ii-dwDeZeQ+v+ZtmhEH4v1$>87zr?$h! zPHIMpiF@xTxAeI62TXY;xoKC*9hsdstYbIt}{{@i}pV(R-dJ7=v*KFng7e(S8?r|t8@*Roc)o0;EVegBEL zW|gB)&p-JmYP=;ATh=~g>GCaV^{6)Ilzh2C+;IB@&-B-|JPsAly_Hs+_%b=*D2-Ncs)KJ&-rl0TYiCX#$Nl?P z_HP##X%Arb(u}NU+ASVqC-lL6hPsi%jm~o)vd!XU<#+P3M(}TJyHCOE1 yc;vh9bRecAB`LEa<+j3BCY99a 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 S5p8+|V~H*a^mAIaHvbQ7gW7xm literal 0 HcmV?d00001 diff --git a/video/vvdec/tests/vvdec.rs b/video/vvdec/tests/vvdec.rs new file mode 100644 index 000000000..146182851 --- /dev/null +++ b/video/vvdec/tests/vvdec.rs @@ -0,0 +1,58 @@ +// Copyright (C) 2025 Carlos Bentzen +// +// 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; +use gst::prelude::*; + +use std::fs; +use std::path::PathBuf; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstvvdec::plugin_register_static().expect("vvdec test"); + }); +} + +#[test] +fn test_decode_yuv420p10le() { + init(); + test_decode("yuv420p10le"); +} + +fn test_decode(name: &str) { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push(format!("tests/vvc_{name}.mkv")); + + let bin = gst::parse::bin_from_description( + &format!("filesrc location={path:?} ! matroskademux ! h266parse ! vvdec name=vvdec"), + false, + ) + .unwrap(); + + let srcpad = bin.by_name("vvdec").unwrap().static_pad("src").unwrap(); + let _ = bin.add_pad(&gst::GhostPad::with_target(&srcpad).unwrap()); + + let mut h = gst_check::Harness::with_element(&bin, None, Some("src")); + + h.play(); + + let buf = h.pull().unwrap(); + let frame = buf.into_mapped_buffer_readable().unwrap(); + + let mut refpath = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + refpath.push(format!("tests/vvc_{name}.ref")); + + let ref_frame = fs::read(refpath).unwrap(); + + assert_eq!(frame.len(), ref_frame.len()); + assert_eq!(frame.as_slice(), glib::Bytes::from_owned(ref_frame)); +}