mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-02-16 12:55:13 +00:00
gopbuffer: implement element buffering of an entire GOP
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1349>
This commit is contained in:
parent
bac2e02160
commit
e868f81189
12 changed files with 1573 additions and 0 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -2402,6 +2402,19 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gst-plugin-gopbuffer"
|
||||
version = "0.13.0-alpha.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gst-plugin-version-helper",
|
||||
"gstreamer",
|
||||
"gstreamer-app",
|
||||
"gstreamer-check",
|
||||
"gstreamer-video",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gst-plugin-gtk4"
|
||||
version = "0.13.0-alpha.1"
|
||||
|
|
|
@ -16,6 +16,7 @@ members = [
|
|||
"generic/sodium",
|
||||
"generic/threadshare",
|
||||
"generic/inter",
|
||||
"generic/gopbuffer",
|
||||
|
||||
"mux/flavors",
|
||||
"mux/fmp4",
|
||||
|
@ -69,6 +70,7 @@ default-members = [
|
|||
"generic/originalbuffer",
|
||||
"generic/threadshare",
|
||||
"generic/inter",
|
||||
"generic/gopbuffer",
|
||||
|
||||
"mux/fmp4",
|
||||
"mux/mp4",
|
||||
|
|
|
@ -2342,6 +2342,73 @@
|
|||
"tracers": {},
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
|
||||
},
|
||||
"gopbuffer": {
|
||||
"description": "Store complete groups of pictures at a time",
|
||||
"elements": {
|
||||
"gopbuffer": {
|
||||
"author": "Matthew Waters <matthew@centricular.com>",
|
||||
"description": "GOP Buffer",
|
||||
"hierarchy": [
|
||||
"GstGopBuffer",
|
||||
"GstElement",
|
||||
"GstObject",
|
||||
"GInitiallyUnowned",
|
||||
"GObject"
|
||||
],
|
||||
"klass": "Video",
|
||||
"pad-templates": {
|
||||
"video_sink": {
|
||||
"caps": "video/x-h264:\n stream-format: { (string)avc, (string)avc3 }\n alignment: au\nvideo/x-h265:\n stream-format: { (string)hvc1, (string)hev1 }\n alignment: au\nvideo/x-vp8:\nvideo/x-vp9:\nvideo/x-av1:\n stream-format: obu-stream\n alignment: tu\n",
|
||||
"direction": "sink",
|
||||
"presence": "always"
|
||||
},
|
||||
"video_src": {
|
||||
"caps": "video/x-h264:\n stream-format: { (string)avc, (string)avc3 }\n alignment: au\nvideo/x-h265:\n stream-format: { (string)hvc1, (string)hev1 }\n alignment: au\nvideo/x-vp8:\nvideo/x-vp9:\nvideo/x-av1:\n stream-format: obu-stream\n alignment: tu\n",
|
||||
"direction": "src",
|
||||
"presence": "always"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"max-size-time": {
|
||||
"blurb": "The maximum duration to store (0=disable)",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "0",
|
||||
"max": "18446744073709551615",
|
||||
"min": "0",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "guint64",
|
||||
"writable": true
|
||||
},
|
||||
"minimum-duration": {
|
||||
"blurb": "The minimum duration to store",
|
||||
"conditionally-available": false,
|
||||
"construct": false,
|
||||
"construct-only": false,
|
||||
"controllable": false,
|
||||
"default": "1000000000",
|
||||
"max": "18446744073709551615",
|
||||
"min": "0",
|
||||
"mutable": "ready",
|
||||
"readable": true,
|
||||
"type": "guint64",
|
||||
"writable": true
|
||||
}
|
||||
},
|
||||
"rank": "primary"
|
||||
}
|
||||
},
|
||||
"filename": "gstgopbuffer",
|
||||
"license": "MPL",
|
||||
"other-types": {},
|
||||
"package": "gst-plugin-gopbuffer",
|
||||
"source": "gst-plugin-gopbuffer",
|
||||
"tracers": {},
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
|
||||
},
|
||||
"gtk4": {
|
||||
"description": "GStreamer GTK 4 Sink element and Paintable widget",
|
||||
"elements": {
|
||||
|
|
44
generic/gopbuffer/Cargo.toml
Normal file
44
generic/gopbuffer/Cargo.toml
Normal file
|
@ -0,0 +1,44 @@
|
|||
[package]
|
||||
name = "gst-plugin-gopbuffer"
|
||||
version.workspace = true
|
||||
authors = ["Matthew Waters <matthew@centricular.com>"]
|
||||
license = "MPL-2.0"
|
||||
description = "Store complete groups of pictures at a time"
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
gst = { workspace = true, features = ["v1_18"] }
|
||||
gst-video = { workspace = true, features = ["v1_18"] }
|
||||
once_cell.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstgopbuffer"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
gst-app = { workspace = true, features = ["v1_18"] }
|
||||
gst-check = { workspace = true, features = ["v1_18"] }
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper = { path="../../version-helper" }
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.8.0"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
373
generic/gopbuffer/LICENSE-MPL-2.0
Normal file
373
generic/gopbuffer/LICENSE-MPL-2.0
Normal file
|
@ -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.
|
3
generic/gopbuffer/build.rs
Normal file
3
generic/gopbuffer/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
880
generic/gopbuffer/src/gopbuffer/imp.rs
Normal file
880
generic/gopbuffer/src/gopbuffer/imp.rs
Normal file
|
@ -0,0 +1,880 @@
|
|||
// Copyright (C) 2023 Matthew Waters <matthew@centricular.com>
|
||||
//
|
||||
// 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
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
/**
|
||||
* SECTION:element-gopbuffer
|
||||
*
|
||||
* #gopbuffer is an element that can be used to store a minimum duration of data delimited by
|
||||
* discrete GOPs (Group of Picture). It does this in by differentiation on the DELTA_UNIT
|
||||
* flag on each input buffer.
|
||||
*
|
||||
* One example of the usefulness of #gopbuffer is its ability to store a backlog of data starting
|
||||
* on a key frame boundary if say the previous 10s seconds of a stream would like to be recorded to
|
||||
* disk.
|
||||
*
|
||||
* ## Example pipeline
|
||||
*
|
||||
* |[
|
||||
* gst-launch videotestsrc ! vp8enc ! gopbuffer minimum-duration=10000000000 ! fakesink
|
||||
* ]|
|
||||
*
|
||||
* Since: plugins-rs-0.13.0
|
||||
*/
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"gopbuffer",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("GopBuffer Element"),
|
||||
)
|
||||
});
|
||||
|
||||
const DEFAULT_MIN_TIME: gst::ClockTime = gst::ClockTime::from_seconds(1);
|
||||
const DEFAULT_MAX_TIME: Option<gst::ClockTime> = None;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Settings {
|
||||
min_time: gst::ClockTime,
|
||||
max_time: Option<gst::ClockTime>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
min_time: DEFAULT_MIN_TIME,
|
||||
max_time: DEFAULT_MAX_TIME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) enum DeltaFrames {
|
||||
/// Only single completely decodable frames
|
||||
IntraOnly,
|
||||
/// Frames may depend on past frames
|
||||
PredictiveOnly,
|
||||
/// Frames may depend on past or future frames
|
||||
Bidirectional,
|
||||
}
|
||||
|
||||
impl DeltaFrames {
|
||||
/// Whether dts is required to order buffers differently from presentation order
|
||||
pub(crate) fn requires_dts(&self) -> bool {
|
||||
matches!(self, Self::Bidirectional)
|
||||
}
|
||||
/// Whether this coding structure does not allow delta flags on buffers
|
||||
pub(crate) fn intra_only(&self) -> bool {
|
||||
matches!(self, Self::IntraOnly)
|
||||
}
|
||||
|
||||
pub(crate) fn from_caps(caps: &gst::CapsRef) -> Option<Self> {
|
||||
let s = caps.structure(0)?;
|
||||
Some(match s.name().as_str() {
|
||||
"video/x-h264" | "video/x-h265" => DeltaFrames::Bidirectional,
|
||||
"video/x-vp8" | "video/x-vp9" | "video/x-av1" => DeltaFrames::PredictiveOnly,
|
||||
"image/jpeg" | "image/png" | "video/x-raw" => DeltaFrames::IntraOnly,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add buffer list support
|
||||
#[derive(Debug)]
|
||||
enum GopItem {
|
||||
Buffer(gst::Buffer),
|
||||
Event(gst::Event),
|
||||
}
|
||||
|
||||
struct Gop {
|
||||
// all times are in running time
|
||||
start_pts: gst::ClockTime,
|
||||
start_dts: Option<gst::Signed<gst::ClockTime>>,
|
||||
earliest_pts: gst::ClockTime,
|
||||
final_earliest_pts: bool,
|
||||
end_pts: gst::ClockTime,
|
||||
end_dts: Option<gst::Signed<gst::ClockTime>>,
|
||||
final_end_pts: bool,
|
||||
// Buffer or event
|
||||
data: VecDeque<GopItem>,
|
||||
}
|
||||
|
||||
impl Gop {
|
||||
fn push_on_pad(mut self, pad: &gst::Pad) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut iter = self.data.iter().filter_map(|item| match item {
|
||||
GopItem::Buffer(buffer) => buffer.pts(),
|
||||
_ => None,
|
||||
});
|
||||
let first_pts = iter.next();
|
||||
let last_pts = iter.last();
|
||||
gst::debug!(
|
||||
CAT,
|
||||
"pushing gop with start pts {} end pts {}",
|
||||
first_pts.display(),
|
||||
last_pts.display(),
|
||||
);
|
||||
for item in self.data.drain(..) {
|
||||
match item {
|
||||
GopItem::Buffer(buffer) => {
|
||||
pad.push(buffer)?;
|
||||
}
|
||||
GopItem::Event(event) => {
|
||||
pad.push_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
struct Stream {
|
||||
sinkpad: gst::Pad,
|
||||
srcpad: gst::Pad,
|
||||
|
||||
sink_segment: Option<gst::FormattedSegment<gst::ClockTime>>,
|
||||
|
||||
delta_frames: DeltaFrames,
|
||||
|
||||
queued_gops: VecDeque<Gop>,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
fn queue_buffer(
|
||||
&mut self,
|
||||
buffer: gst::Buffer,
|
||||
segment: &gst::FormattedSegment<gst::ClockTime>,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let pts_position = buffer.pts().unwrap();
|
||||
let end_pts_position = pts_position
|
||||
.opt_add(buffer.duration())
|
||||
.unwrap_or(pts_position);
|
||||
|
||||
let pts = segment
|
||||
.to_running_time_full(pts_position)
|
||||
.ok_or_else(|| {
|
||||
gst::error!(CAT, obj: self.sinkpad, "Couldn't convert PTS to running time");
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
.positive()
|
||||
.unwrap_or_else(|| {
|
||||
gst::warning!(CAT, obj: self.sinkpad, "Negative PTSs are not supported");
|
||||
gst::ClockTime::ZERO
|
||||
});
|
||||
let end_pts = segment
|
||||
.to_running_time_full(end_pts_position)
|
||||
.ok_or_else(|| {
|
||||
gst::error!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Couldn't convert end PTS to running time"
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
.positive()
|
||||
.unwrap_or_else(|| {
|
||||
gst::warning!(CAT, obj: self.sinkpad, "Negative PTSs are not supported");
|
||||
gst::ClockTime::ZERO
|
||||
});
|
||||
|
||||
let (dts, end_dts) = if !self.delta_frames.requires_dts() {
|
||||
(None, None)
|
||||
} else {
|
||||
let dts_position = buffer.dts().expect("No dts");
|
||||
let end_dts_position = buffer
|
||||
.duration()
|
||||
.opt_add(dts_position)
|
||||
.unwrap_or(dts_position);
|
||||
|
||||
let dts = segment.to_running_time_full(dts_position).ok_or_else(|| {
|
||||
gst::error!(CAT, obj: self.sinkpad, "Couldn't convert DTS to running time");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let end_dts = segment
|
||||
.to_running_time_full(end_dts_position)
|
||||
.ok_or_else(|| {
|
||||
gst::error!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Couldn't convert end DTS to running time"
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let end_dts = std::cmp::max(end_dts, dts);
|
||||
|
||||
(Some(dts), Some(end_dts))
|
||||
};
|
||||
|
||||
if !buffer.flags().contains(gst::BufferFlags::DELTA_UNIT) {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
"New GOP detected with buffer pts {} dts {}",
|
||||
buffer.pts().display(),
|
||||
buffer.dts().display()
|
||||
);
|
||||
let gop = Gop {
|
||||
start_pts: pts,
|
||||
start_dts: dts,
|
||||
earliest_pts: pts,
|
||||
final_earliest_pts: false,
|
||||
end_pts: pts,
|
||||
end_dts,
|
||||
final_end_pts: false,
|
||||
data: VecDeque::from([GopItem::Buffer(buffer)]),
|
||||
};
|
||||
self.queued_gops.push_front(gop);
|
||||
if let Some(prev_gop) = self.queued_gops.get_mut(1) {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Updating previous GOP starting at PTS {} to end PTS {}",
|
||||
prev_gop.earliest_pts,
|
||||
pts,
|
||||
);
|
||||
|
||||
prev_gop.end_pts = std::cmp::max(prev_gop.end_pts, pts);
|
||||
prev_gop.end_dts = std::cmp::max(prev_gop.end_dts, dts);
|
||||
|
||||
if !self.delta_frames.requires_dts() {
|
||||
prev_gop.final_end_pts = true;
|
||||
}
|
||||
|
||||
if !prev_gop.final_earliest_pts {
|
||||
// Don't bother logging this for intra-only streams as it would be for every
|
||||
// single buffer.
|
||||
if self.delta_frames.requires_dts() {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Previous GOP has final earliest PTS at {}",
|
||||
prev_gop.earliest_pts
|
||||
);
|
||||
}
|
||||
|
||||
prev_gop.final_earliest_pts = true;
|
||||
if let Some(prev_prev_gop) = self.queued_gops.get_mut(2) {
|
||||
prev_prev_gop.final_end_pts = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(gop) = self.queued_gops.front_mut() {
|
||||
gop.end_pts = std::cmp::max(gop.end_pts, end_pts);
|
||||
gop.end_dts = gop.end_dts.opt_max(end_dts);
|
||||
gop.data.push_back(GopItem::Buffer(buffer));
|
||||
|
||||
if self.delta_frames.requires_dts() {
|
||||
let dts = dts.unwrap();
|
||||
|
||||
if gop.earliest_pts > pts && !gop.final_earliest_pts {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Updating current GOP earliest PTS from {} to {}",
|
||||
gop.earliest_pts,
|
||||
pts
|
||||
);
|
||||
gop.earliest_pts = pts;
|
||||
|
||||
if let Some(prev_gop) = self.queued_gops.get_mut(1) {
|
||||
if prev_gop.end_pts < pts {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Updating previous GOP starting PTS {} end time from {} to {}",
|
||||
pts,
|
||||
prev_gop.end_pts,
|
||||
pts
|
||||
);
|
||||
prev_gop.end_pts = pts;
|
||||
}
|
||||
}
|
||||
}
|
||||
let gop = self.queued_gops.front_mut().unwrap();
|
||||
|
||||
// The earliest PTS is known when the current DTS is bigger or equal to the first
|
||||
// PTS that was observed in this GOP. If there was another frame later that had a
|
||||
// lower PTS then it wouldn't be possible to display it in time anymore, i.e. the
|
||||
// stream would be invalid.
|
||||
if gop.start_pts <= dts && !gop.final_earliest_pts {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"GOP has final earliest PTS at {}",
|
||||
gop.earliest_pts
|
||||
);
|
||||
gop.final_earliest_pts = true;
|
||||
|
||||
if let Some(prev_gop) = self.queued_gops.get_mut(1) {
|
||||
prev_gop.final_end_pts = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
"dropping buffer before first GOP with pts {} dts {}",
|
||||
buffer.pts().display(),
|
||||
buffer.dts().display()
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((prev_gop, first_gop)) = Option::zip(
|
||||
self.queued_gops.iter().find(|gop| gop.final_end_pts),
|
||||
self.queued_gops.back(),
|
||||
) {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Queued full GOPs duration updated to {}",
|
||||
prev_gop.end_pts.saturating_sub(first_gop.earliest_pts),
|
||||
);
|
||||
}
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Queued duration updated to {}",
|
||||
Option::zip(self.queued_gops.front(), self.queued_gops.back())
|
||||
.map(|(end, start)| end.end_pts.saturating_sub(start.start_pts))
|
||||
.unwrap_or(gst::ClockTime::ZERO)
|
||||
);
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn oldest_gop(&mut self) -> Option<Gop> {
|
||||
self.queued_gops.pop_back()
|
||||
}
|
||||
|
||||
fn peek_oldest_gop(&self) -> Option<&Gop> {
|
||||
self.queued_gops.back()
|
||||
}
|
||||
|
||||
fn peek_second_oldest_gop(&self) -> Option<&Gop> {
|
||||
if self.queued_gops.len() <= 1 {
|
||||
return None;
|
||||
}
|
||||
self.queued_gops.get(self.queued_gops.len() - 2)
|
||||
}
|
||||
|
||||
fn drain_all(&mut self) -> impl Iterator<Item = Gop> + '_ {
|
||||
self.queued_gops.drain(..)
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
self.queued_gops.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
streams: Vec<Stream>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn stream_from_sink_pad(&self, pad: &gst::Pad) -> Option<&Stream> {
|
||||
self.streams.iter().find(|stream| &stream.sinkpad == pad)
|
||||
}
|
||||
fn stream_from_sink_pad_mut(&mut self, pad: &gst::Pad) -> Option<&mut Stream> {
|
||||
self.streams
|
||||
.iter_mut()
|
||||
.find(|stream| &stream.sinkpad == pad)
|
||||
}
|
||||
fn stream_from_src_pad(&self, pad: &gst::Pad) -> Option<&Stream> {
|
||||
self.streams.iter().find(|stream| &stream.srcpad == pad)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GopBuffer {
|
||||
state: Mutex<State>,
|
||||
settings: Mutex<Settings>,
|
||||
}
|
||||
|
||||
impl GopBuffer {
|
||||
fn sink_chain(
|
||||
&self,
|
||||
pad: &gst::Pad,
|
||||
buffer: gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let obj = self.obj();
|
||||
if buffer.pts().is_none() {
|
||||
gst::error!(CAT, obj: obj, "Require timestamped buffers!");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
|
||||
let settings = self.settings.lock().unwrap().clone();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let stream = state
|
||||
.stream_from_sink_pad_mut(pad)
|
||||
.expect("pad without an internal Stream");
|
||||
|
||||
let Some(segment) = stream.sink_segment.clone() else {
|
||||
gst::element_imp_error!(self, gst::CoreError::Clock, ["Got buffer before segment"]);
|
||||
return Err(gst::FlowError::Error);
|
||||
};
|
||||
|
||||
if stream.delta_frames.intra_only() && buffer.flags().contains(gst::BufferFlags::DELTA_UNIT)
|
||||
{
|
||||
gst::error!(CAT, obj: pad, "Intra-only stream with delta units");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
|
||||
if stream.delta_frames.requires_dts() && buffer.dts().is_none() {
|
||||
gst::error!(CAT, obj: pad, "Require DTS for video streams");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
|
||||
let srcpad = stream.srcpad.clone();
|
||||
stream.queue_buffer(buffer, &segment)?;
|
||||
let mut gops_to_push = vec![];
|
||||
|
||||
let Some(newest_gop) = stream.queued_gops.front() else {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
};
|
||||
// we are looking for the latest pts value here (which should be the largest)
|
||||
let newest_ts = if stream.delta_frames.requires_dts() {
|
||||
newest_gop.end_dts.unwrap()
|
||||
} else {
|
||||
gst::Signed::Positive(newest_gop.end_pts)
|
||||
};
|
||||
|
||||
loop {
|
||||
// check stored times as though the oldest GOP doesn't exist.
|
||||
let Some(second_oldest_gop) = stream.peek_second_oldest_gop() else {
|
||||
break;
|
||||
};
|
||||
// we are looking for the oldest pts here (with the largest value). This is our potentially
|
||||
// new end time.
|
||||
let oldest_ts = if stream.delta_frames.requires_dts() {
|
||||
second_oldest_gop.start_dts.unwrap()
|
||||
} else {
|
||||
gst::Signed::Positive(second_oldest_gop.start_pts)
|
||||
};
|
||||
|
||||
let stored_duration_without_oldest = newest_ts.saturating_sub(oldest_ts);
|
||||
gst::trace!(
|
||||
CAT,
|
||||
obj: obj,
|
||||
"newest_pts {}, second oldest_pts {}, stored_duration_without_oldest_gop {}, min-time {}",
|
||||
newest_ts.display(),
|
||||
oldest_ts.display(),
|
||||
stored_duration_without_oldest.display(),
|
||||
settings.min_time.display()
|
||||
);
|
||||
if stored_duration_without_oldest < settings.min_time {
|
||||
break;
|
||||
}
|
||||
gops_to_push.push(stream.oldest_gop().unwrap());
|
||||
}
|
||||
|
||||
if let Some(max_time) = settings.max_time {
|
||||
while let Some(oldest_gop) = stream.peek_oldest_gop() {
|
||||
let oldest_ts = oldest_gop.data.iter().rev().find_map(|item| match item {
|
||||
GopItem::Buffer(buffer) => {
|
||||
if stream.delta_frames.requires_dts() {
|
||||
Some(gst::Signed::Positive(buffer.dts().unwrap()))
|
||||
} else {
|
||||
Some(gst::Signed::Positive(buffer.pts().unwrap()))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if newest_ts
|
||||
.opt_saturating_sub(oldest_ts)
|
||||
.map_or(false, |diff| diff > gst::Signed::Positive(max_time))
|
||||
{
|
||||
gst::warning!(CAT, obj: obj, "Stored data has overflowed the maximum allowed stored time {}, pushing oldest GOP", max_time.display());
|
||||
gops_to_push.push(stream.oldest_gop().unwrap());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
for gop in gops_to_push.into_iter() {
|
||||
gop.push_on_pad(&srcpad)?;
|
||||
}
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool {
|
||||
let obj = self.obj();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let stream = state
|
||||
.stream_from_sink_pad_mut(pad)
|
||||
.expect("pad without an internal Stream!");
|
||||
match event.view() {
|
||||
gst::EventView::Caps(caps) => {
|
||||
let Some(delta_frames) = DeltaFrames::from_caps(caps.caps()) else {
|
||||
return false;
|
||||
};
|
||||
stream.delta_frames = delta_frames;
|
||||
}
|
||||
gst::EventView::FlushStop(_flush) => {
|
||||
gst::debug!(CAT, obj: obj, "flushing stored data");
|
||||
stream.flush();
|
||||
}
|
||||
gst::EventView::Eos(_eos) => {
|
||||
gst::debug!(CAT, obj: obj, "draining data at EOS");
|
||||
let gops = stream.drain_all().collect::<Vec<_>>();
|
||||
let srcpad = stream.srcpad.clone();
|
||||
drop(state);
|
||||
for gop in gops.into_iter() {
|
||||
let _ = gop.push_on_pad(&srcpad);
|
||||
}
|
||||
// once we've pushed all the data, we can push the corresponding eos
|
||||
gst::Pad::event_default(pad, Some(&*obj), event);
|
||||
return true;
|
||||
}
|
||||
gst::EventView::Segment(segment) => {
|
||||
let Ok(segment) = segment.segment().clone().downcast::<gst::ClockTime>() else {
|
||||
gst::error!(CAT, "Non TIME segments are not supported");
|
||||
return false;
|
||||
};
|
||||
stream.sink_segment = Some(segment);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
if event.is_serialized() {
|
||||
if stream.peek_oldest_gop().is_none() {
|
||||
// if there is nothing queued, the event can go straight through
|
||||
gst::trace!(CAT, obj: obj, "nothing queued, event {:?} passthrough", event.structure().map(|s| s.name().as_str()));
|
||||
drop(state);
|
||||
return gst::Pad::event_default(pad, Some(&*obj), event);
|
||||
}
|
||||
let gop = stream.queued_gops.front_mut().unwrap();
|
||||
gop.data.push_back(GopItem::Event(event));
|
||||
true
|
||||
} else {
|
||||
// non-serialized events can be pushed directly
|
||||
drop(state);
|
||||
gst::Pad::event_default(pad, Some(&*obj), event)
|
||||
}
|
||||
}
|
||||
|
||||
fn sink_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool {
|
||||
let obj = self.obj();
|
||||
if query.is_serialized() {
|
||||
// TODO: serialized queries somehow?
|
||||
gst::warning!(CAT, obj: pad, "Serialized queries are currently not supported");
|
||||
return false;
|
||||
}
|
||||
gst::Pad::query_default(pad, Some(&*obj), query)
|
||||
}
|
||||
|
||||
fn src_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool {
|
||||
let obj = self.obj();
|
||||
match query.view_mut() {
|
||||
gst::QueryViewMut::Latency(latency) => {
|
||||
let mut upstream_query = gst::query::Latency::new();
|
||||
let otherpad = {
|
||||
let state = self.state.lock().unwrap();
|
||||
let Some(stream) = state.stream_from_src_pad(pad) else {
|
||||
return false;
|
||||
};
|
||||
stream.sinkpad.clone()
|
||||
};
|
||||
let ret = otherpad.peer_query(&mut upstream_query);
|
||||
|
||||
if ret {
|
||||
let (live, mut min, mut max) = upstream_query.result();
|
||||
|
||||
let settings = self.settings.lock().unwrap();
|
||||
min += settings.max_time.unwrap_or(settings.min_time);
|
||||
max = max.opt_max(settings.max_time);
|
||||
|
||||
latency.set(live, min, max);
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Latency query response: live {} min {} max {}",
|
||||
live,
|
||||
min,
|
||||
max.display()
|
||||
);
|
||||
}
|
||||
ret
|
||||
}
|
||||
_ => gst::Pad::query_default(pad, Some(&*obj), query),
|
||||
}
|
||||
}
|
||||
|
||||
fn iterate_internal_links(&self, pad: &gst::Pad) -> gst::Iterator<gst::Pad> {
|
||||
let state = self.state.lock().unwrap();
|
||||
let otherpad = match pad.direction() {
|
||||
gst::PadDirection::Src => state
|
||||
.stream_from_src_pad(pad)
|
||||
.map(|stream| stream.sinkpad.clone()),
|
||||
gst::PadDirection::Sink => state
|
||||
.stream_from_sink_pad(pad)
|
||||
.map(|stream| stream.srcpad.clone()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if let Some(otherpad) = otherpad {
|
||||
gst::Iterator::from_vec(vec![otherpad])
|
||||
} else {
|
||||
gst::Iterator::from_vec(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for GopBuffer {
|
||||
const NAME: &'static str = "GstGopBuffer";
|
||||
type Type = super::GopBuffer;
|
||||
type ParentType = gst::Element;
|
||||
}
|
||||
|
||||
impl ObjectImpl for GopBuffer {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecUInt64::builder("minimum-duration")
|
||||
.nick("Minimum Duration")
|
||||
.blurb("The minimum duration to store")
|
||||
.default_value(DEFAULT_MIN_TIME.nseconds())
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("max-size-time")
|
||||
.nick("Maximum Duration")
|
||||
.blurb("The maximum duration to store (0=disable)")
|
||||
.default_value(0)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
&PROPERTIES
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"minimum-duration" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
let min_time = value.get().expect("type checked upstream");
|
||||
if settings.min_time != min_time {
|
||||
settings.min_time = min_time;
|
||||
drop(settings);
|
||||
self.post_message(gst::message::Latency::builder().src(&*self.obj()).build());
|
||||
}
|
||||
}
|
||||
"max-size-time" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
let max_time = value
|
||||
.get::<Option<gst::ClockTime>>()
|
||||
.expect("type checked upstream");
|
||||
let max_time = if matches!(max_time, Some(gst::ClockTime::ZERO) | None) {
|
||||
None
|
||||
} else {
|
||||
max_time
|
||||
};
|
||||
if settings.max_time != max_time {
|
||||
settings.max_time = max_time;
|
||||
drop(settings);
|
||||
self.post_message(gst::message::Latency::builder().src(&*self.obj()).build());
|
||||
}
|
||||
}
|
||||
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"minimum-duration" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.min_time.to_value()
|
||||
}
|
||||
"max-size-time" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.max_time.unwrap_or(gst::ClockTime::ZERO).to_value()
|
||||
}
|
||||
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
let class = obj.class();
|
||||
let templ = class.pad_template("video_sink").unwrap();
|
||||
let sinkpad = gst::Pad::builder_from_template(&templ)
|
||||
.name("video_sink")
|
||||
.chain_function(|pad, parent, buffer| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| Err(gst::FlowError::Error),
|
||||
|gopbuffer| gopbuffer.sink_chain(pad, buffer),
|
||||
)
|
||||
})
|
||||
.event_function(|pad, parent, event| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|gopbuffer| gopbuffer.sink_event(pad, event),
|
||||
)
|
||||
})
|
||||
.query_function(|pad, parent, query| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|gopbuffer| gopbuffer.sink_query(pad, query),
|
||||
)
|
||||
})
|
||||
.iterate_internal_links_function(|pad, parent| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| gst::Pad::iterate_internal_links_default(pad, parent),
|
||||
|gopbuffer| gopbuffer.iterate_internal_links(pad),
|
||||
)
|
||||
})
|
||||
.flags(gst::PadFlags::PROXY_CAPS)
|
||||
.build();
|
||||
obj.add_pad(&sinkpad).unwrap();
|
||||
|
||||
let templ = class.pad_template("video_src").unwrap();
|
||||
let srcpad = gst::Pad::builder_from_template(&templ)
|
||||
.name("video_src")
|
||||
.query_function(|pad, parent, query| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|gopbuffer| gopbuffer.src_query(pad, query),
|
||||
)
|
||||
})
|
||||
.iterate_internal_links_function(|pad, parent| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| gst::Pad::iterate_internal_links_default(pad, parent),
|
||||
|gopbuffer| gopbuffer.iterate_internal_links(pad),
|
||||
)
|
||||
})
|
||||
.build();
|
||||
obj.add_pad(&srcpad).unwrap();
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.streams.push(Stream {
|
||||
sinkpad,
|
||||
srcpad,
|
||||
sink_segment: None,
|
||||
delta_frames: DeltaFrames::IntraOnly,
|
||||
queued_gops: VecDeque::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for GopBuffer {}
|
||||
|
||||
impl ElementImpl for GopBuffer {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"GopBuffer",
|
||||
"Video",
|
||||
"GOP Buffer",
|
||||
"Matthew Waters <matthew@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
// This element is designed to implement multiple streams but it has not been
|
||||
// implemented.
|
||||
//
|
||||
// The things missing for multiple (audio or video) streams are:
|
||||
// 1. More pad templates
|
||||
// 2. Choosing a main stream to drive the timestamp logic between all input streams
|
||||
// 3. Allowing either the main stream to cause other streams to push data
|
||||
// regardless of it's GOP state, or allow each stream to be individually delimited
|
||||
// by GOP but all still within the minimum duration.
|
||||
let video_caps = [
|
||||
gst::Structure::builder("video/x-h264")
|
||||
.field("stream-format", gst::List::new(["avc", "avc3"]))
|
||||
.field("alignment", "au")
|
||||
.build(),
|
||||
gst::Structure::builder("video/x-h265")
|
||||
.field("stream-format", gst::List::new(["hvc1", "hev1"]))
|
||||
.field("alignment", "au")
|
||||
.build(),
|
||||
gst::Structure::builder("video/x-vp8").build(),
|
||||
gst::Structure::builder("video/x-vp9").build(),
|
||||
gst::Structure::builder("video/x-av1")
|
||||
.field("stream-format", "obu-stream")
|
||||
.field("alignment", "tu")
|
||||
.build(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<gst::Caps>();
|
||||
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"video_src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&video_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"video_sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&video_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
|
||||
fn change_state(
|
||||
&self,
|
||||
transition: gst::StateChange,
|
||||
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
|
||||
#[allow(clippy::single_match)]
|
||||
match transition {
|
||||
gst::StateChange::NullToReady => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if let Some(max_time) = settings.max_time {
|
||||
if max_time < settings.min_time {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::CoreError::StateChange,
|
||||
["Configured maximum time is less than the minimum time"]
|
||||
);
|
||||
return Err(gst::StateChangeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.parent_change_state(transition)?;
|
||||
|
||||
Ok(gst::StateChangeSuccess::Success)
|
||||
}
|
||||
}
|
27
generic/gopbuffer/src/gopbuffer/mod.rs
Normal file
27
generic/gopbuffer/src/gopbuffer/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2022 Matthew Waters <matthew@centricular.com>
|
||||
//
|
||||
// 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
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub(crate) struct GopBuffer(ObjectSubclass<imp::GopBuffer>) @extends gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"gopbuffer",
|
||||
gst::Rank::PRIMARY,
|
||||
GopBuffer::static_type(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
34
generic/gopbuffer/src/lib.rs
Normal file
34
generic/gopbuffer/src/lib.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2022 Matthew Waters <matthew@centricular.com>
|
||||
//
|
||||
// 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
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
/**
|
||||
* plugin-gopbuffer:
|
||||
*
|
||||
* Since: plugins-rs-0.13.0
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod gopbuffer;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gopbuffer::register(plugin)
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
gopbuffer,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
// FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known)
|
||||
"MPL",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
128
generic/gopbuffer/tests/tests.rs
Normal file
128
generic/gopbuffer/tests/tests.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// 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
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstgopbuffer::plugin_register_static().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
macro_rules! check_buffer {
|
||||
($buf1:expr, $buf2:expr) => {
|
||||
assert_eq!($buf1.pts(), $buf2.pts());
|
||||
assert_eq!($buf1.dts(), $buf2.dts());
|
||||
assert_eq!($buf1.flags(), $buf2.flags());
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_one_gop_held() {
|
||||
const OFFSET: gst::ClockTime = gst::ClockTime::from_seconds(10);
|
||||
init();
|
||||
|
||||
let mut h =
|
||||
gst_check::Harness::with_padnames("gopbuffer", Some("video_sink"), Some("video_src"));
|
||||
|
||||
// 200ms min buffer time
|
||||
let element = h.element().unwrap();
|
||||
element.set_property("minimum-duration", gst::ClockTime::from_mseconds(200));
|
||||
|
||||
h.set_src_caps(
|
||||
gst::Caps::builder("video/x-h264")
|
||||
.field("width", 320i32)
|
||||
.field("height", 240i32)
|
||||
.field("framerate", gst::Fraction::new(10, 1))
|
||||
.field("stream-format", "avc")
|
||||
.field("alignment", "au")
|
||||
.field("codec_data", gst::Buffer::with_size(1).unwrap())
|
||||
.build(),
|
||||
);
|
||||
let mut in_segment = gst::Segment::new();
|
||||
in_segment.set_format(gst::Format::Time);
|
||||
in_segment.set_base(10.seconds());
|
||||
assert!(h.push_event(gst::event::Segment::builder(&in_segment).build()));
|
||||
|
||||
h.play();
|
||||
|
||||
// Push 10 buffers of 100ms each, 2nd and 5th buffer without DELTA_UNIT flag
|
||||
let in_buffers: Vec<_> = (0..6)
|
||||
.map(|i| {
|
||||
let mut buffer = gst::Buffer::with_size(1).unwrap();
|
||||
{
|
||||
let buffer = buffer.get_mut().unwrap();
|
||||
buffer.set_pts(OFFSET + gst::ClockTime::from_mseconds(i * 100));
|
||||
buffer.set_dts(OFFSET + gst::ClockTime::from_mseconds(i * 100));
|
||||
buffer.set_duration(gst::ClockTime::from_mseconds(100));
|
||||
if i != 1 && i != 4 {
|
||||
buffer.set_flags(gst::BufferFlags::DELTA_UNIT);
|
||||
}
|
||||
}
|
||||
assert_eq!(h.push(buffer.clone()), Ok(gst::FlowSuccess::Ok));
|
||||
buffer
|
||||
})
|
||||
.collect();
|
||||
|
||||
// pull mandatory events
|
||||
let ev = h.pull_event().unwrap();
|
||||
assert_eq!(ev.type_(), gst::EventType::StreamStart);
|
||||
let ev = h.pull_event().unwrap();
|
||||
assert_eq!(ev.type_(), gst::EventType::Caps);
|
||||
// GstHarness pushes its own segment event that we need to eat
|
||||
let ev = h.pull_event().unwrap();
|
||||
assert_eq!(ev.type_(), gst::EventType::Segment);
|
||||
let ev = h.pull_event().unwrap();
|
||||
let gst::event::EventView::Segment(recv_segment) = ev.view() else {
|
||||
unreachable!()
|
||||
};
|
||||
let recv_segment = recv_segment.segment();
|
||||
assert_eq!(recv_segment, &in_segment);
|
||||
|
||||
// check that at least the first GOP has been output already as it exceeds the minimum-time
|
||||
// value
|
||||
let mut in_iter = in_buffers.iter();
|
||||
|
||||
// the first buffer is dropped because it was not preceded by a keyframe
|
||||
let _buffer = in_iter.next().unwrap();
|
||||
|
||||
// a keyframe
|
||||
let out = h.pull().unwrap();
|
||||
let buffer = in_iter.next().unwrap();
|
||||
check_buffer!(buffer, out);
|
||||
|
||||
// not a keyframe
|
||||
let out = h.pull().unwrap();
|
||||
let buffer = in_iter.next().unwrap();
|
||||
check_buffer!(buffer, out);
|
||||
|
||||
// not a keyframe
|
||||
let out = h.pull().unwrap();
|
||||
let buffer = in_iter.next().unwrap();
|
||||
check_buffer!(buffer, out);
|
||||
|
||||
// no more buffers
|
||||
assert_eq!(h.buffers_in_queue(), 0);
|
||||
|
||||
// push eos to drain out the rest of the data
|
||||
assert!(h.push_event(gst::event::Eos::new()));
|
||||
for buffer in in_iter {
|
||||
let out = h.pull().unwrap();
|
||||
check_buffer!(buffer, out);
|
||||
}
|
||||
|
||||
// no more buffers
|
||||
assert_eq!(h.buffers_in_queue(), 0);
|
||||
|
||||
let ev = h.pull_event().unwrap();
|
||||
assert_eq!(ev.type_(), gst::EventType::Eos);
|
||||
}
|
|
@ -204,6 +204,7 @@ plugins = {
|
|||
'library': 'libgstrsvideofx',
|
||||
'extra-deps': {'cairo-gobject': []},
|
||||
},
|
||||
'gopbuffer': {'library': 'libgstgopbuffer'},
|
||||
}
|
||||
|
||||
if get_option('examples').allowed()
|
||||
|
|
|
@ -10,6 +10,7 @@ option('spotify', type: 'feature', value: 'auto', description: 'Build spotify pl
|
|||
# generic
|
||||
option('file', type: 'feature', value: 'auto', description: 'Build file plugin')
|
||||
option('originalbuffer', type: 'feature', value: 'auto', description: 'Build originalbuffer plugin')
|
||||
option('gopbuffer', type: 'feature', value: 'auto', description: 'Build gopbuffer plugin')
|
||||
option('sodium', type: 'feature', value: 'auto', description: 'Build sodium plugin')
|
||||
option('sodium-source', type: 'combo',
|
||||
choices: ['system', 'built-in'], value: 'built-in',
|
||||
|
|
Loading…
Reference in a new issue