mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-02-02 06:02:20 +00:00
Merge branch 'master' of https://github.com/teltek/gst-plugin-ndi
This commit is contained in:
commit
7a90500fe7
20 changed files with 6520 additions and 0 deletions
33
net/ndi/Cargo.toml
Normal file
33
net/ndi/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
[package]
|
||||||
|
name = "gst-plugin-ndi"
|
||||||
|
version = "1.0.0"
|
||||||
|
authors = ["Ruben Gonzalez <rubenrua@teltek.es>", "Daniel Vilar <daniel.peiteado@teltek.es>", "Sebastian Dröge <sebastian@centricular.com>"]
|
||||||
|
repository = "https://github.com/teltek/gst-plugin-ndi"
|
||||||
|
license = "LGPL"
|
||||||
|
description = "NewTek NDI Plugin"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
glib = "0.15"
|
||||||
|
gst = { package = "gstreamer", version = "0.18", features = ["v1_12"] }
|
||||||
|
gst-base = { package = "gstreamer-base", version = "0.18" }
|
||||||
|
gst-audio = { package = "gstreamer-audio", version = "0.18" }
|
||||||
|
gst-video = { package = "gstreamer-video", version = "0.18", features = ["v1_12"] }
|
||||||
|
byte-slice-cast = "1"
|
||||||
|
once_cell = "1.0"
|
||||||
|
byteorder = "1.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
gst-plugin-version-helper = "0.7"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["interlaced-fields", "reference-timestamps", "sink"]
|
||||||
|
interlaced-fields = ["gst/v1_16", "gst-video/v1_16"]
|
||||||
|
reference-timestamps = ["gst/v1_14"]
|
||||||
|
sink = ["gst/v1_18", "gst-base/v1_18"]
|
||||||
|
advanced-sdk = []
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "gstndi"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
path = "src/lib.rs"
|
504
net/ndi/LICENSE
Normal file
504
net/ndi/LICENSE
Normal file
|
@ -0,0 +1,504 @@
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
|
If you develop a new library, and you want it to be of the greatest
|
||||||
|
possible use to the public, we recommend making it free software that
|
||||||
|
everyone can redistribute and change. You can do so by permitting
|
||||||
|
redistribution under these terms (or, alternatively, under the terms of the
|
||||||
|
ordinary General Public License).
|
||||||
|
|
||||||
|
To apply these terms, attach the following notices to the library. It is
|
||||||
|
safest to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||||
|
USA
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||||
|
library `Frob' (a library for tweaking knobs) written by James Random
|
||||||
|
Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
81
net/ndi/README.md
Normal file
81
net/ndi/README.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
GStreamer NDI Plugin for Linux
|
||||||
|
====================
|
||||||
|
|
||||||
|
*Compiled and tested with NDI SDK 4.0, 4.1 and 5.0*
|
||||||
|
|
||||||
|
This is a plugin for the [GStreamer](https://gstreamer.freedesktop.org/) multimedia framework that allows GStreamer to receive a stream from a [NDI](https://www.newtek.com/ndi/) source. This plugin has been developed by [Teltek](http://teltek.es/) and was funded by the [University of the Arts London](https://www.arts.ac.uk/) and [The University of Manchester](https://www.manchester.ac.uk/).
|
||||||
|
|
||||||
|
Currently the plugin has a source element for receiving from NDI sources, a sink element to provide an NDI source and a device provider for discovering NDI sources on the network.
|
||||||
|
|
||||||
|
Some examples of how to use these elements from the command line:
|
||||||
|
|
||||||
|
```console
|
||||||
|
# Information about the elements
|
||||||
|
$ gst-inspect-1.0 ndi
|
||||||
|
$ gst-inspect-1.0 ndisrc
|
||||||
|
$ gst-inspect-1.0 ndisink
|
||||||
|
|
||||||
|
# Discover all NDI sources on the network
|
||||||
|
$ gst-device-monitor-1.0 -f Source/Network:application/x-ndi
|
||||||
|
|
||||||
|
# Audio/Video source pipeline
|
||||||
|
$ gst-launch-1.0 ndisrc ndi-name="GC-DEV2 (OBS)" ! ndisrcdemux name=demux demux.video ! queue ! videoconvert ! autovideosink demux.audio ! queue ! audioconvert ! autoaudiosink
|
||||||
|
|
||||||
|
# Audio/Video sink pipeline
|
||||||
|
$ gst-launch-1.0 videotestsrc is-live=true ! video/x-raw,format=UYVY ! ndisinkcombiner name=combiner ! ndisink ndi-name="My NDI source" audiotestsrc is-live=true ! combiner.audio
|
||||||
|
```
|
||||||
|
|
||||||
|
Feel free to contribute to this project. Some ways you can contribute are:
|
||||||
|
* Testing with more hardware and software and reporting bugs
|
||||||
|
* Doing pull requests.
|
||||||
|
|
||||||
|
Compilation of the NDI element
|
||||||
|
-------
|
||||||
|
To compile the NDI element it's necessary to install Rust, the NDI SDK and the following packages for gstreamer:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||||||
|
gstreamer1.0-plugins-base
|
||||||
|
|
||||||
|
```
|
||||||
|
To install the required NDI library there are two options:
|
||||||
|
1. Download NDI SDK from NDI website and move the library to the correct location.
|
||||||
|
2. Use a [deb package](https://github.com/Palakis/obs-ndi/releases/download/4.5.2/libndi3_3.5.1-1_amd64.deb) made by the community. Thanks to [NDI plugin for OBS](https://github.com/Palakis/obs-ndi).
|
||||||
|
|
||||||
|
To install Rust, you can follow their documentation: https://www.rust-lang.org/en-US/install.html
|
||||||
|
|
||||||
|
Once all requirements are met, you can build the plugin by executing the following command from the project root folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo build
|
||||||
|
export GST_PLUGIN_PATH=`pwd`/target/debug
|
||||||
|
gst-inspect-1.0 ndi
|
||||||
|
```
|
||||||
|
|
||||||
|
By default GStreamer 1.18 is required, to use an older version. You can build with `$ cargo build --no-default-features --features whatever_you_want_to_enable_of_the_above_features`
|
||||||
|
|
||||||
|
|
||||||
|
If all went ok, you should see info related to the NDI element. To make the plugin available without using `GST_PLUGIN_PATH` it's necessary to copy the plugin to the gstreamer plugins folder.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ cargo build --release
|
||||||
|
$ sudo install -o root -g root -m 644 target/release/libgstndi.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/
|
||||||
|
$ sudo ldconfig
|
||||||
|
$ gst-inspect-1.0 ndi
|
||||||
|
```
|
||||||
|
|
||||||
|
More info about GStreamer plugins written in Rust:
|
||||||
|
----------------------------------
|
||||||
|
https://gitlab.freedesktop.org/gstreamer/gstreamer-rs
|
||||||
|
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
This plugin is licensed under the LGPL - see the [LICENSE](LICENSE) file for details
|
||||||
|
|
||||||
|
|
||||||
|
Acknowledgments
|
||||||
|
-------
|
||||||
|
* University of the Arts London and The University of Manchester.
|
||||||
|
* Sebastian Dröge (@sdroege).
|
3
net/ndi/build.rs
Normal file
3
net/ndi/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
gst_plugin_version_helper::info()
|
||||||
|
}
|
269
net/ndi/src/device_provider/imp.rs
Normal file
269
net/ndi/src/device_provider/imp.rs
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
use gst::prelude::*;
|
||||||
|
use gst::subclass::prelude::*;
|
||||||
|
use gst::{gst_error, gst_log, gst_trace};
|
||||||
|
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use std::sync::atomic;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::ndi;
|
||||||
|
|
||||||
|
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new(
|
||||||
|
"ndideviceprovider",
|
||||||
|
gst::DebugColorFlags::empty(),
|
||||||
|
Some("NewTek NDI Device Provider"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DeviceProvider {
|
||||||
|
thread: Mutex<Option<thread::JoinHandle<()>>>,
|
||||||
|
current_devices: Mutex<Vec<super::Device>>,
|
||||||
|
find: Mutex<Option<ndi::FindInstance>>,
|
||||||
|
is_running: atomic::AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DeviceProvider {
|
||||||
|
const NAME: &'static str = "NdiDeviceProvider";
|
||||||
|
type Type = super::DeviceProvider;
|
||||||
|
type ParentType = gst::DeviceProvider;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
thread: Mutex::new(None),
|
||||||
|
current_devices: Mutex::new(vec![]),
|
||||||
|
find: Mutex::new(None),
|
||||||
|
is_running: atomic::AtomicBool::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DeviceProvider {}
|
||||||
|
|
||||||
|
impl GstObjectImpl for DeviceProvider {}
|
||||||
|
|
||||||
|
impl DeviceProviderImpl for DeviceProvider {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::DeviceProviderMetadata> {
|
||||||
|
static METADATA: Lazy<gst::subclass::DeviceProviderMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::DeviceProviderMetadata::new("NewTek NDI Device Provider",
|
||||||
|
"Source/Audio/Video/Network",
|
||||||
|
"NewTek NDI Device Provider",
|
||||||
|
"Ruben Gonzalez <rubenrua@teltek.es>, Daniel Vilar <daniel.peiteado@teltek.es>, Sebastian Dröge <sebastian@centricular.com>")
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn probe(&self, _device_provider: &Self::Type) -> Vec<gst::Device> {
|
||||||
|
self.current_devices
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|d| d.clone().upcast())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self, device_provider: &Self::Type) -> Result<(), gst::LoggableError> {
|
||||||
|
let mut thread_guard = self.thread.lock().unwrap();
|
||||||
|
if thread_guard.is_some() {
|
||||||
|
gst_log!(CAT, obj: device_provider, "Device provider already started");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_running.store(true, atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
|
let device_provider_weak = device_provider.downgrade();
|
||||||
|
let mut first = true;
|
||||||
|
*thread_guard = Some(thread::spawn(move || {
|
||||||
|
let device_provider = match device_provider_weak.upgrade() {
|
||||||
|
None => return,
|
||||||
|
Some(device_provider) => device_provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
let imp = DeviceProvider::from_instance(&device_provider);
|
||||||
|
{
|
||||||
|
let mut find_guard = imp.find.lock().unwrap();
|
||||||
|
if find_guard.is_some() {
|
||||||
|
gst_log!(CAT, obj: &device_provider, "Already started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let find = match ndi::FindInstance::builder().build() {
|
||||||
|
None => {
|
||||||
|
gst_error!(CAT, obj: &device_provider, "Failed to create Find instance");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(find) => find,
|
||||||
|
};
|
||||||
|
*find_guard = Some(find);
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let device_provider = match device_provider_weak.upgrade() {
|
||||||
|
None => break,
|
||||||
|
Some(device_provider) => device_provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
let imp = DeviceProvider::from_instance(&device_provider);
|
||||||
|
if !imp.is_running.load(atomic::Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
imp.poll(&device_provider, first);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, _device_provider: &Self::Type) {
|
||||||
|
if let Some(_thread) = self.thread.lock().unwrap().take() {
|
||||||
|
self.is_running.store(false, atomic::Ordering::SeqCst);
|
||||||
|
// Don't actually join because that might take a while
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceProvider {
|
||||||
|
fn poll(&self, device_provider: &super::DeviceProvider, first: bool) {
|
||||||
|
let mut find_guard = self.find.lock().unwrap();
|
||||||
|
let find = match *find_guard {
|
||||||
|
None => return,
|
||||||
|
Some(ref mut find) => find,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !find.wait_for_sources(if first { 1000 } else { 5000 }) {
|
||||||
|
gst_trace!(CAT, obj: device_provider, "No new sources found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sources = find.get_current_sources();
|
||||||
|
let mut sources = sources.iter().map(|s| s.to_owned()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut current_devices_guard = self.current_devices.lock().unwrap();
|
||||||
|
let mut expired_devices = vec![];
|
||||||
|
let mut remaining_sources = vec![];
|
||||||
|
|
||||||
|
// First check for each device we previously knew if it's still available
|
||||||
|
for old_device in &*current_devices_guard {
|
||||||
|
let old_device_imp = Device::from_instance(old_device);
|
||||||
|
let old_source = old_device_imp.source.get().unwrap();
|
||||||
|
|
||||||
|
if !sources.contains(&*old_source) {
|
||||||
|
gst_log!(
|
||||||
|
CAT,
|
||||||
|
obj: device_provider,
|
||||||
|
"Source {:?} disappeared",
|
||||||
|
old_source
|
||||||
|
);
|
||||||
|
expired_devices.push(old_device.clone());
|
||||||
|
} else {
|
||||||
|
// Otherwise remember that we had it before already and don't have to announce it
|
||||||
|
// again. After the loop we're going to remove these all from the sources vec.
|
||||||
|
remaining_sources.push(old_source.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for remaining_source in remaining_sources {
|
||||||
|
sources.retain(|s| s != &remaining_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all expired devices from the list of cached devices
|
||||||
|
current_devices_guard.retain(|d| !expired_devices.contains(d));
|
||||||
|
// And also notify the device provider of them having disappeared
|
||||||
|
for old_device in expired_devices {
|
||||||
|
device_provider.device_remove(&old_device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now go through all new devices and announce them
|
||||||
|
for source in sources {
|
||||||
|
gst_log!(CAT, obj: device_provider, "Source {:?} appeared", source);
|
||||||
|
let device = super::Device::new(&source);
|
||||||
|
device_provider.device_add(&device);
|
||||||
|
current_devices_guard.push(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Device {
|
||||||
|
source: OnceCell<ndi::Source<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for Device {
|
||||||
|
const NAME: &'static str = "NdiDevice";
|
||||||
|
type Type = super::Device;
|
||||||
|
type ParentType = gst::Device;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
source: OnceCell::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for Device {}
|
||||||
|
|
||||||
|
impl GstObjectImpl for Device {}
|
||||||
|
|
||||||
|
impl DeviceImpl for Device {
|
||||||
|
fn create_element(
|
||||||
|
&self,
|
||||||
|
_device: &Self::Type,
|
||||||
|
name: Option<&str>,
|
||||||
|
) -> Result<gst::Element, gst::LoggableError> {
|
||||||
|
let source_info = self.source.get().unwrap();
|
||||||
|
let element = glib::Object::with_type(
|
||||||
|
crate::ndisrc::NdiSrc::static_type(),
|
||||||
|
&[
|
||||||
|
("name", &name),
|
||||||
|
("ndi-name", &source_info.ndi_name()),
|
||||||
|
("url-address", &source_info.url_address()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.dynamic_cast::<gst::Element>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Device {
|
||||||
|
fn new(source: &ndi::Source<'_>) -> super::Device {
|
||||||
|
let display_name = source.ndi_name();
|
||||||
|
let device_class = "Source/Audio/Video/Network";
|
||||||
|
|
||||||
|
let element_class =
|
||||||
|
glib::Class::<gst::Element>::from_type(crate::ndisrc::NdiSrc::static_type()).unwrap();
|
||||||
|
let templ = element_class.pad_template("src").unwrap();
|
||||||
|
let caps = templ.caps();
|
||||||
|
|
||||||
|
// Put the url-address into the extra properties
|
||||||
|
let extra_properties = gst::Structure::builder("properties")
|
||||||
|
.field("ndi-name", &source.ndi_name())
|
||||||
|
.field("url-address", &source.url_address())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let device = glib::Object::new::<super::Device>(&[
|
||||||
|
("caps", &caps),
|
||||||
|
("display-name", &display_name),
|
||||||
|
("device-class", &device_class),
|
||||||
|
("properties", &extra_properties),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let device_impl = Device::from_instance(&device);
|
||||||
|
|
||||||
|
device_impl.source.set(source.to_owned()).unwrap();
|
||||||
|
|
||||||
|
device
|
||||||
|
}
|
||||||
|
}
|
26
net/ndi/src/device_provider/mod.rs
Normal file
26
net/ndi/src/device_provider/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use glib::prelude::*;
|
||||||
|
|
||||||
|
mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DeviceProvider(ObjectSubclass<imp::DeviceProvider>) @extends gst::DeviceProvider, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for DeviceProvider {}
|
||||||
|
unsafe impl Sync for DeviceProvider {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct Device(ObjectSubclass<imp::Device>) @extends gst::Device, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for Device {}
|
||||||
|
unsafe impl Sync for Device {}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
gst::DeviceProvider::register(
|
||||||
|
Some(plugin),
|
||||||
|
"ndideviceprovider",
|
||||||
|
gst::Rank::Primary,
|
||||||
|
DeviceProvider::static_type(),
|
||||||
|
)
|
||||||
|
}
|
159
net/ndi/src/lib.rs
Normal file
159
net/ndi/src/lib.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
mod device_provider;
|
||||||
|
pub mod ndi;
|
||||||
|
#[cfg(feature = "sink")]
|
||||||
|
mod ndisink;
|
||||||
|
#[cfg(feature = "sink")]
|
||||||
|
mod ndisinkcombiner;
|
||||||
|
#[cfg(feature = "sink")]
|
||||||
|
pub mod ndisinkmeta;
|
||||||
|
mod ndisrc;
|
||||||
|
mod ndisrcdemux;
|
||||||
|
pub mod ndisrcmeta;
|
||||||
|
pub mod ndisys;
|
||||||
|
pub mod receiver;
|
||||||
|
|
||||||
|
use crate::ndi::*;
|
||||||
|
use crate::ndisys::*;
|
||||||
|
use crate::receiver::*;
|
||||||
|
|
||||||
|
use std::time;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||||
|
#[repr(u32)]
|
||||||
|
#[enum_type(name = "GstNdiTimestampMode")]
|
||||||
|
pub enum TimestampMode {
|
||||||
|
#[enum_value(name = "Receive Time / Timecode", nick = "receive-time-vs-timecode")]
|
||||||
|
ReceiveTimeTimecode = 0,
|
||||||
|
#[enum_value(name = "Receive Time / Timestamp", nick = "receive-time-vs-timestamp")]
|
||||||
|
ReceiveTimeTimestamp = 1,
|
||||||
|
#[enum_value(name = "NDI Timecode", nick = "timecode")]
|
||||||
|
Timecode = 2,
|
||||||
|
#[enum_value(name = "NDI Timestamp", nick = "timestamp")]
|
||||||
|
Timestamp = 3,
|
||||||
|
#[enum_value(name = "Receive Time", nick = "receive-time")]
|
||||||
|
ReceiveTime = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||||
|
#[repr(u32)]
|
||||||
|
#[enum_type(name = "GstNdiRecvColorFormat")]
|
||||||
|
pub enum RecvColorFormat {
|
||||||
|
#[enum_value(name = "BGRX or BGRA", nick = "bgrx-bgra")]
|
||||||
|
BgrxBgra = 0,
|
||||||
|
#[enum_value(name = "UYVY or BGRA", nick = "uyvy-bgra")]
|
||||||
|
UyvyBgra = 1,
|
||||||
|
#[enum_value(name = "RGBX or RGBA", nick = "rgbx-rgba")]
|
||||||
|
RgbxRgba = 2,
|
||||||
|
#[enum_value(name = "UYVY or RGBA", nick = "uyvy-rgba")]
|
||||||
|
UyvyRgba = 3,
|
||||||
|
#[enum_value(name = "Fastest", nick = "fastest")]
|
||||||
|
Fastest = 4,
|
||||||
|
#[enum_value(name = "Best", nick = "best")]
|
||||||
|
Best = 5,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v1", nick = "compressed-v1")]
|
||||||
|
CompressedV1 = 6,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v2", nick = "compressed-v2")]
|
||||||
|
CompressedV2 = 7,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v3", nick = "compressed-v3")]
|
||||||
|
CompressedV3 = 8,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v3 with audio", nick = "compressed-v3-with-audio")]
|
||||||
|
CompressedV3WithAudio = 9,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v4", nick = "compressed-v4")]
|
||||||
|
CompressedV4 = 10,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v4 with audio", nick = "compressed-v4-with-audio")]
|
||||||
|
CompressedV4WithAudio = 11,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v5", nick = "compressed-v5")]
|
||||||
|
CompressedV5 = 12,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[enum_value(name = "Compressed v5 with audio", nick = "compressed-v5-with-audio")]
|
||||||
|
CompressedV5WithAudio = 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RecvColorFormat> for NDIlib_recv_color_format_e {
|
||||||
|
fn from(v: RecvColorFormat) -> Self {
|
||||||
|
match v {
|
||||||
|
RecvColorFormat::BgrxBgra => NDIlib_recv_color_format_BGRX_BGRA,
|
||||||
|
RecvColorFormat::UyvyBgra => NDIlib_recv_color_format_UYVY_BGRA,
|
||||||
|
RecvColorFormat::RgbxRgba => NDIlib_recv_color_format_RGBX_RGBA,
|
||||||
|
RecvColorFormat::UyvyRgba => NDIlib_recv_color_format_UYVY_RGBA,
|
||||||
|
RecvColorFormat::Fastest => NDIlib_recv_color_format_fastest,
|
||||||
|
RecvColorFormat::Best => NDIlib_recv_color_format_best,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV1 => NDIlib_recv_color_format_ex_compressed,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV2 => NDIlib_recv_color_format_ex_compressed_v2,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV3 => NDIlib_recv_color_format_ex_compressed_v3,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV3WithAudio => {
|
||||||
|
NDIlib_recv_color_format_ex_compressed_v3_with_audio
|
||||||
|
}
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV4 => NDIlib_recv_color_format_ex_compressed_v4,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV4WithAudio => {
|
||||||
|
NDIlib_recv_color_format_ex_compressed_v4_with_audio
|
||||||
|
}
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV5 => NDIlib_recv_color_format_ex_compressed_v5,
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
RecvColorFormat::CompressedV5WithAudio => {
|
||||||
|
NDIlib_recv_color_format_ex_compressed_v5_with_audio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
if !ndi::initialize() {
|
||||||
|
return Err(glib::bool_error!("Cannot initialize NDI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
device_provider::register(plugin)?;
|
||||||
|
|
||||||
|
ndisrc::register(plugin)?;
|
||||||
|
ndisrcdemux::register(plugin)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "sink")]
|
||||||
|
{
|
||||||
|
ndisinkcombiner::register(plugin)?;
|
||||||
|
ndisink::register(plugin)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_RECEIVER_NDI_NAME: Lazy<String> = Lazy::new(|| {
|
||||||
|
format!(
|
||||||
|
"GStreamer NDI Source {}-{}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
env!("COMMIT_ID")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "reference-timestamps")]
|
||||||
|
static TIMECODE_CAPS: Lazy<gst::Caps> =
|
||||||
|
Lazy::new(|| gst::Caps::new_simple("timestamp/x-ndi-timecode", &[]));
|
||||||
|
#[cfg(feature = "reference-timestamps")]
|
||||||
|
static TIMESTAMP_CAPS: Lazy<gst::Caps> =
|
||||||
|
Lazy::new(|| gst::Caps::new_simple("timestamp/x-ndi-timestamp", &[]));
|
||||||
|
|
||||||
|
gst::plugin_define!(
|
||||||
|
ndi,
|
||||||
|
env!("CARGO_PKG_DESCRIPTION"),
|
||||||
|
plugin_init,
|
||||||
|
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||||
|
"LGPL",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
env!("CARGO_PKG_REPOSITORY"),
|
||||||
|
env!("BUILD_REL_DATE")
|
||||||
|
);
|
1184
net/ndi/src/ndi.rs
Normal file
1184
net/ndi/src/ndi.rs
Normal file
File diff suppressed because it is too large
Load diff
363
net/ndi/src/ndisink/imp.rs
Normal file
363
net/ndi/src/ndisink/imp.rs
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
use glib::subclass::prelude::*;
|
||||||
|
use gst::prelude::*;
|
||||||
|
use gst::subclass::prelude::*;
|
||||||
|
use gst::{gst_debug, gst_error, gst_info, gst_trace};
|
||||||
|
use gst_base::prelude::*;
|
||||||
|
use gst_base::subclass::prelude::*;
|
||||||
|
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::ndi::SendInstance;
|
||||||
|
|
||||||
|
static DEFAULT_SENDER_NDI_NAME: Lazy<String> = Lazy::new(|| {
|
||||||
|
format!(
|
||||||
|
"GStreamer NDI Sink {}-{}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
env!("COMMIT_ID")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Settings {
|
||||||
|
ndi_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Settings {
|
||||||
|
ndi_name: DEFAULT_SENDER_NDI_NAME.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
send: SendInstance,
|
||||||
|
video_info: Option<gst_video::VideoInfo>,
|
||||||
|
audio_info: Option<gst_audio::AudioInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NdiSink {
|
||||||
|
settings: Mutex<Settings>,
|
||||||
|
state: Mutex<Option<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new("ndisink", gst::DebugColorFlags::empty(), Some("NDI Sink"))
|
||||||
|
});
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for NdiSink {
|
||||||
|
const NAME: &'static str = "NdiSink";
|
||||||
|
type Type = super::NdiSink;
|
||||||
|
type ParentType = gst_base::BaseSink;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
settings: Mutex::new(Default::default()),
|
||||||
|
state: Mutex::new(Default::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for NdiSink {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![glib::ParamSpecString::new(
|
||||||
|
"ndi-name",
|
||||||
|
"NDI Name",
|
||||||
|
"NDI Name to use",
|
||||||
|
Some(DEFAULT_SENDER_NDI_NAME.as_ref()),
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
_obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"ndi-name" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
settings.ndi_name = value
|
||||||
|
.get::<String>()
|
||||||
|
.unwrap_or_else(|_| DEFAULT_SENDER_NDI_NAME.clone());
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"ndi-name" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.ndi_name.to_value()
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GstObjectImpl for NdiSink {}
|
||||||
|
|
||||||
|
impl ElementImpl for NdiSink {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||||
|
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"NDI Sink",
|
||||||
|
"Sink/Audio/Video",
|
||||||
|
"Render as an NDI stream",
|
||||||
|
"Sebastian Dröge <sebastian@centricular.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||||
|
let caps = gst::Caps::builder_full()
|
||||||
|
.structure(
|
||||||
|
gst::Structure::builder("video/x-raw")
|
||||||
|
.field(
|
||||||
|
"format",
|
||||||
|
&gst::List::new(&[
|
||||||
|
&gst_video::VideoFormat::Uyvy.to_str(),
|
||||||
|
&gst_video::VideoFormat::I420.to_str(),
|
||||||
|
&gst_video::VideoFormat::Nv12.to_str(),
|
||||||
|
&gst_video::VideoFormat::Nv21.to_str(),
|
||||||
|
&gst_video::VideoFormat::Yv12.to_str(),
|
||||||
|
&gst_video::VideoFormat::Bgra.to_str(),
|
||||||
|
&gst_video::VideoFormat::Bgrx.to_str(),
|
||||||
|
&gst_video::VideoFormat::Rgba.to_str(),
|
||||||
|
&gst_video::VideoFormat::Rgbx.to_str(),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.field("width", &gst::IntRange::<i32>::new(1, std::i32::MAX))
|
||||||
|
.field("height", &gst::IntRange::<i32>::new(1, std::i32::MAX))
|
||||||
|
.field(
|
||||||
|
"framerate",
|
||||||
|
&gst::FractionRange::new(
|
||||||
|
gst::Fraction::new(0, 1),
|
||||||
|
gst::Fraction::new(std::i32::MAX, 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.structure(
|
||||||
|
gst::Structure::builder("audio/x-raw")
|
||||||
|
.field("format", &gst_audio::AUDIO_FORMAT_F32.to_str())
|
||||||
|
.field("rate", &gst::IntRange::<i32>::new(1, i32::MAX))
|
||||||
|
.field("channels", &gst::IntRange::<i32>::new(1, i32::MAX))
|
||||||
|
.field("layout", &"interleaved")
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let sink_pad_template = gst::PadTemplate::new(
|
||||||
|
"sink",
|
||||||
|
gst::PadDirection::Sink,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&caps,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
vec![sink_pad_template]
|
||||||
|
});
|
||||||
|
|
||||||
|
PAD_TEMPLATES.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseSinkImpl for NdiSink {
|
||||||
|
fn start(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
|
||||||
|
let send = SendInstance::builder(&settings.ndi_name)
|
||||||
|
.build()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
gst::error_msg!(
|
||||||
|
gst::ResourceError::OpenWrite,
|
||||||
|
["Could not create send instance"]
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let state = State {
|
||||||
|
send,
|
||||||
|
video_info: None,
|
||||||
|
audio_info: None,
|
||||||
|
};
|
||||||
|
*state_storage = Some(state);
|
||||||
|
gst_info!(CAT, obj: element, "Started");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
|
||||||
|
*state_storage = None;
|
||||||
|
gst_info!(CAT, obj: element, "Stopped");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock(&self, _element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_stop(&self, _element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_caps(&self, element: &Self::Type, caps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||||
|
gst_debug!(CAT, obj: element, "Setting caps {}", caps);
|
||||||
|
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
let state = match &mut *state_storage {
|
||||||
|
None => return Err(gst::loggable_error!(CAT, "Sink not started yet")),
|
||||||
|
Some(ref mut state) => state,
|
||||||
|
};
|
||||||
|
|
||||||
|
let s = caps.structure(0).unwrap();
|
||||||
|
if s.name() == "video/x-raw" {
|
||||||
|
let info = gst_video::VideoInfo::from_caps(caps)
|
||||||
|
.map_err(|_| gst::loggable_error!(CAT, "Couldn't parse caps {}", caps))?;
|
||||||
|
|
||||||
|
state.video_info = Some(info);
|
||||||
|
state.audio_info = None;
|
||||||
|
} else {
|
||||||
|
let info = gst_audio::AudioInfo::from_caps(caps)
|
||||||
|
.map_err(|_| gst::loggable_error!(CAT, "Couldn't parse caps {}", caps))?;
|
||||||
|
|
||||||
|
state.audio_info = Some(info);
|
||||||
|
state.video_info = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&self,
|
||||||
|
element: &Self::Type,
|
||||||
|
buffer: &gst::Buffer,
|
||||||
|
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
let state = match &mut *state_storage {
|
||||||
|
None => return Err(gst::FlowError::Error),
|
||||||
|
Some(ref mut state) => state,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref info) = state.video_info {
|
||||||
|
if let Some(audio_meta) = buffer.meta::<crate::ndisinkmeta::NdiSinkAudioMeta>() {
|
||||||
|
for (buffer, info, timecode) in audio_meta.buffers() {
|
||||||
|
let frame = crate::ndi::AudioFrame::try_from_buffer(info, buffer, *timecode)
|
||||||
|
.map_err(|_| {
|
||||||
|
gst_error!(CAT, obj: element, "Unsupported audio frame");
|
||||||
|
gst::FlowError::NotNegotiated
|
||||||
|
})?;
|
||||||
|
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: element,
|
||||||
|
"Sending audio buffer {:?} with timecode {} and format {:?}",
|
||||||
|
buffer,
|
||||||
|
if *timecode < 0 {
|
||||||
|
gst::ClockTime::NONE.display()
|
||||||
|
} else {
|
||||||
|
Some(gst::ClockTime::from_nseconds(*timecode as u64 * 100)).display()
|
||||||
|
},
|
||||||
|
info,
|
||||||
|
);
|
||||||
|
state.send.send_audio(&frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip empty/gap buffers from ndisinkcombiner
|
||||||
|
if buffer.size() != 0 {
|
||||||
|
let timecode = element
|
||||||
|
.segment()
|
||||||
|
.downcast::<gst::ClockTime>()
|
||||||
|
.ok()
|
||||||
|
.and_then(|segment| {
|
||||||
|
segment
|
||||||
|
.to_running_time(buffer.pts())
|
||||||
|
.zip(element.base_time())
|
||||||
|
})
|
||||||
|
.and_then(|(running_time, base_time)| running_time.checked_add(base_time))
|
||||||
|
.map(|time| (time.nseconds() / 100) as i64)
|
||||||
|
.unwrap_or(crate::ndisys::NDIlib_send_timecode_synthesize);
|
||||||
|
|
||||||
|
let frame = gst_video::VideoFrameRef::from_buffer_ref_readable(buffer, info)
|
||||||
|
.map_err(|_| {
|
||||||
|
gst_error!(CAT, obj: element, "Failed to map buffer");
|
||||||
|
gst::FlowError::Error
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let frame = crate::ndi::VideoFrame::try_from_video_frame(&frame, timecode)
|
||||||
|
.map_err(|_| {
|
||||||
|
gst_error!(CAT, obj: element, "Unsupported video frame");
|
||||||
|
gst::FlowError::NotNegotiated
|
||||||
|
})?;
|
||||||
|
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: element,
|
||||||
|
"Sending video buffer {:?} with timecode {} and format {:?}",
|
||||||
|
buffer,
|
||||||
|
if timecode < 0 {
|
||||||
|
gst::ClockTime::NONE.display()
|
||||||
|
} else {
|
||||||
|
Some(gst::ClockTime::from_nseconds(timecode as u64 * 100)).display()
|
||||||
|
},
|
||||||
|
info
|
||||||
|
);
|
||||||
|
state.send.send_video(&frame);
|
||||||
|
}
|
||||||
|
} else if let Some(ref info) = state.audio_info {
|
||||||
|
let timecode = element
|
||||||
|
.segment()
|
||||||
|
.downcast::<gst::ClockTime>()
|
||||||
|
.ok()
|
||||||
|
.and_then(|segment| {
|
||||||
|
segment
|
||||||
|
.to_running_time(buffer.pts())
|
||||||
|
.zip(element.base_time())
|
||||||
|
})
|
||||||
|
.and_then(|(running_time, base_time)| running_time.checked_add(base_time))
|
||||||
|
.map(|time| (time.nseconds() / 100) as i64)
|
||||||
|
.unwrap_or(crate::ndisys::NDIlib_send_timecode_synthesize);
|
||||||
|
|
||||||
|
let frame =
|
||||||
|
crate::ndi::AudioFrame::try_from_buffer(info, buffer, timecode).map_err(|_| {
|
||||||
|
gst_error!(CAT, obj: element, "Unsupported audio frame");
|
||||||
|
gst::FlowError::NotNegotiated
|
||||||
|
})?;
|
||||||
|
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: element,
|
||||||
|
"Sending audio buffer {:?} with timecode {} and format {:?}",
|
||||||
|
buffer,
|
||||||
|
if timecode < 0 {
|
||||||
|
gst::ClockTime::NONE.display()
|
||||||
|
} else {
|
||||||
|
Some(gst::ClockTime::from_nseconds(timecode as u64 * 100)).display()
|
||||||
|
},
|
||||||
|
info,
|
||||||
|
);
|
||||||
|
state.send.send_audio(&frame);
|
||||||
|
} else {
|
||||||
|
return Err(gst::FlowError::Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(gst::FlowSuccess::Ok)
|
||||||
|
}
|
||||||
|
}
|
19
net/ndi/src/ndisink/mod.rs
Normal file
19
net/ndi/src/ndisink/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use glib::prelude::*;
|
||||||
|
|
||||||
|
mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct NdiSink(ObjectSubclass<imp::NdiSink>) @extends gst_base::BaseSink, gst::Element, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NdiSink {}
|
||||||
|
unsafe impl Sync for NdiSink {}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
gst::Element::register(
|
||||||
|
Some(plugin),
|
||||||
|
"ndisink",
|
||||||
|
gst::Rank::None,
|
||||||
|
NdiSink::static_type(),
|
||||||
|
)
|
||||||
|
}
|
634
net/ndi/src/ndisinkcombiner/imp.rs
Normal file
634
net/ndi/src/ndisinkcombiner/imp.rs
Normal file
|
@ -0,0 +1,634 @@
|
||||||
|
use glib::prelude::*;
|
||||||
|
use glib::subclass::prelude::*;
|
||||||
|
use gst::prelude::*;
|
||||||
|
use gst::subclass::prelude::*;
|
||||||
|
use gst::{gst_debug, gst_error, gst_trace, gst_warning};
|
||||||
|
use gst_base::prelude::*;
|
||||||
|
use gst_base::subclass::prelude::*;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use std::mem;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
static CAT: once_cell::sync::Lazy<gst::DebugCategory> = once_cell::sync::Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new(
|
||||||
|
"ndisinkcombiner",
|
||||||
|
gst::DebugColorFlags::empty(),
|
||||||
|
Some("NDI sink audio/video combiner"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
// Note that this applies to the currently pending buffer on the pad and *not*
|
||||||
|
// to the current_video_buffer below!
|
||||||
|
video_info: Option<gst_video::VideoInfo>,
|
||||||
|
audio_info: Option<gst_audio::AudioInfo>,
|
||||||
|
current_video_buffer: Option<(gst::Buffer, gst::ClockTime)>,
|
||||||
|
current_audio_buffers: Vec<(gst::Buffer, gst_audio::AudioInfo, i64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NdiSinkCombiner {
|
||||||
|
video_pad: gst_base::AggregatorPad,
|
||||||
|
audio_pad: Mutex<Option<gst_base::AggregatorPad>>,
|
||||||
|
state: Mutex<Option<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for NdiSinkCombiner {
|
||||||
|
const NAME: &'static str = "NdiSinkCombiner";
|
||||||
|
type Type = super::NdiSinkCombiner;
|
||||||
|
type ParentType = gst_base::Aggregator;
|
||||||
|
|
||||||
|
fn with_class(klass: &Self::Class) -> Self {
|
||||||
|
let templ = klass.pad_template("video").unwrap();
|
||||||
|
let video_pad =
|
||||||
|
gst::PadBuilder::<gst_base::AggregatorPad>::from_template(&templ, Some("video"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
video_pad,
|
||||||
|
audio_pad: Mutex::new(None),
|
||||||
|
state: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for NdiSinkCombiner {
|
||||||
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
|
obj.add_pad(&self.video_pad).unwrap();
|
||||||
|
|
||||||
|
self.parent_constructed(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GstObjectImpl for NdiSinkCombiner {}
|
||||||
|
|
||||||
|
impl ElementImpl for NdiSinkCombiner {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||||
|
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"NDI Sink Combiner",
|
||||||
|
"Combiner/Audio/Video",
|
||||||
|
"NDI sink audio/video combiner",
|
||||||
|
"Sebastian Dröge <sebastian@centricular.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||||
|
let caps = gst::Caps::builder("video/x-raw")
|
||||||
|
.field(
|
||||||
|
"format",
|
||||||
|
&gst::List::new(&[
|
||||||
|
&gst_video::VideoFormat::Uyvy.to_str(),
|
||||||
|
&gst_video::VideoFormat::I420.to_str(),
|
||||||
|
&gst_video::VideoFormat::Nv12.to_str(),
|
||||||
|
&gst_video::VideoFormat::Nv21.to_str(),
|
||||||
|
&gst_video::VideoFormat::Yv12.to_str(),
|
||||||
|
&gst_video::VideoFormat::Bgra.to_str(),
|
||||||
|
&gst_video::VideoFormat::Bgrx.to_str(),
|
||||||
|
&gst_video::VideoFormat::Rgba.to_str(),
|
||||||
|
&gst_video::VideoFormat::Rgbx.to_str(),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.field("width", &gst::IntRange::<i32>::new(1, i32::MAX))
|
||||||
|
.field("height", &gst::IntRange::<i32>::new(1, i32::MAX))
|
||||||
|
.field(
|
||||||
|
"framerate",
|
||||||
|
&gst::FractionRange::new(
|
||||||
|
gst::Fraction::new(1, i32::MAX),
|
||||||
|
gst::Fraction::new(i32::MAX, 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
let src_pad_template = gst::PadTemplate::with_gtype(
|
||||||
|
"src",
|
||||||
|
gst::PadDirection::Src,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&caps,
|
||||||
|
gst_base::AggregatorPad::static_type(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let video_sink_pad_template = gst::PadTemplate::with_gtype(
|
||||||
|
"video",
|
||||||
|
gst::PadDirection::Sink,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&caps,
|
||||||
|
gst_base::AggregatorPad::static_type(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let caps = gst::Caps::builder("audio/x-raw")
|
||||||
|
.field("format", &gst_audio::AUDIO_FORMAT_F32.to_str())
|
||||||
|
.field("rate", &gst::IntRange::<i32>::new(1, i32::MAX))
|
||||||
|
.field("channels", &gst::IntRange::<i32>::new(1, i32::MAX))
|
||||||
|
.field("layout", &"interleaved")
|
||||||
|
.build();
|
||||||
|
let audio_sink_pad_template = gst::PadTemplate::with_gtype(
|
||||||
|
"audio",
|
||||||
|
gst::PadDirection::Sink,
|
||||||
|
gst::PadPresence::Request,
|
||||||
|
&caps,
|
||||||
|
gst_base::AggregatorPad::static_type(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
vec![
|
||||||
|
src_pad_template,
|
||||||
|
video_sink_pad_template,
|
||||||
|
audio_sink_pad_template,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PAD_TEMPLATES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_pad(&self, element: &Self::Type, pad: &gst::Pad) {
|
||||||
|
let mut audio_pad_storage = self.audio_pad.lock().unwrap();
|
||||||
|
|
||||||
|
if audio_pad_storage.as_ref().map(|p| p.upcast_ref()) == Some(pad) {
|
||||||
|
gst_debug!(CAT, obj: element, "Release audio pad");
|
||||||
|
self.parent_release_pad(element, pad);
|
||||||
|
*audio_pad_storage = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AggregatorImpl for NdiSinkCombiner {
|
||||||
|
fn create_new_pad(
|
||||||
|
&self,
|
||||||
|
agg: &Self::Type,
|
||||||
|
templ: &gst::PadTemplate,
|
||||||
|
_req_name: Option<&str>,
|
||||||
|
_caps: Option<&gst::Caps>,
|
||||||
|
) -> Option<gst_base::AggregatorPad> {
|
||||||
|
let mut audio_pad_storage = self.audio_pad.lock().unwrap();
|
||||||
|
|
||||||
|
if audio_pad_storage.is_some() {
|
||||||
|
gst_error!(CAT, obj: agg, "Audio pad already requested");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sink_templ = agg.pad_template("audio").unwrap();
|
||||||
|
if templ != &sink_templ {
|
||||||
|
gst_error!(CAT, obj: agg, "Wrong pad template");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pad =
|
||||||
|
gst::PadBuilder::<gst_base::AggregatorPad>::from_template(templ, Some("audio")).build();
|
||||||
|
*audio_pad_storage = Some(pad.clone());
|
||||||
|
|
||||||
|
gst_debug!(CAT, obj: agg, "Requested audio pad");
|
||||||
|
|
||||||
|
Some(pad)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self, agg: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
*state_storage = Some(State {
|
||||||
|
audio_info: None,
|
||||||
|
video_info: None,
|
||||||
|
current_video_buffer: None,
|
||||||
|
current_audio_buffers: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
gst_debug!(CAT, obj: agg, "Started");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, agg: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
// Drop our state now
|
||||||
|
let _ = self.state.lock().unwrap().take();
|
||||||
|
|
||||||
|
gst_debug!(CAT, obj: agg, "Stopped");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_time(&self, _agg: &Self::Type) -> Option<gst::ClockTime> {
|
||||||
|
// FIXME: What to do here? We don't really know when the next buffer is expected
|
||||||
|
gst::ClockTime::NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip(
|
||||||
|
&self,
|
||||||
|
agg: &Self::Type,
|
||||||
|
agg_pad: &gst_base::AggregatorPad,
|
||||||
|
mut buffer: gst::Buffer,
|
||||||
|
) -> Option<gst::Buffer> {
|
||||||
|
let segment = match agg_pad.segment().downcast::<gst::ClockTime>() {
|
||||||
|
Ok(segment) => segment,
|
||||||
|
Err(_) => {
|
||||||
|
gst_error!(CAT, obj: agg, "Only TIME segments supported");
|
||||||
|
return Some(buffer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pts = buffer.pts();
|
||||||
|
if pts.is_none() {
|
||||||
|
gst_error!(CAT, obj: agg, "Only buffers with PTS supported");
|
||||||
|
return Some(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = buffer.duration();
|
||||||
|
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: agg_pad,
|
||||||
|
"Clipping buffer {:?} with PTS {} and duration {}",
|
||||||
|
buffer,
|
||||||
|
pts.display(),
|
||||||
|
duration.display(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let state_storage = self.state.lock().unwrap();
|
||||||
|
let state = match &*state_storage {
|
||||||
|
Some(ref state) => state,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let duration = if duration.is_some() {
|
||||||
|
duration
|
||||||
|
} else if let Some(ref audio_info) = state.audio_info {
|
||||||
|
gst::ClockTime::SECOND.mul_div_floor(
|
||||||
|
buffer.size() as u64,
|
||||||
|
audio_info.rate() as u64 * audio_info.bpf() as u64,
|
||||||
|
)
|
||||||
|
} else if let Some(ref video_info) = state.video_info {
|
||||||
|
if video_info.fps().numer() > 0 {
|
||||||
|
gst::ClockTime::SECOND.mul_div_floor(
|
||||||
|
video_info.fps().denom() as u64,
|
||||||
|
video_info.fps().numer() as u64,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gst::ClockTime::NONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: agg_pad,
|
||||||
|
"Clipping buffer {:?} with PTS {} and duration {}",
|
||||||
|
buffer,
|
||||||
|
pts.display(),
|
||||||
|
duration.display(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if agg_pad == &self.video_pad {
|
||||||
|
let end_pts = pts
|
||||||
|
.zip(duration)
|
||||||
|
.and_then(|(pts, duration)| pts.checked_add(duration));
|
||||||
|
|
||||||
|
segment.clip(pts, end_pts).map(|(start, stop)| {
|
||||||
|
{
|
||||||
|
let buffer = buffer.make_mut();
|
||||||
|
buffer.set_pts(start);
|
||||||
|
buffer.set_duration(
|
||||||
|
stop.zip(start)
|
||||||
|
.and_then(|(stop, start)| stop.checked_sub(start)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
})
|
||||||
|
} else if let Some(ref audio_info) = state.audio_info {
|
||||||
|
gst_audio::audio_buffer_clip(
|
||||||
|
buffer,
|
||||||
|
segment.upcast_ref(),
|
||||||
|
audio_info.rate(),
|
||||||
|
audio_info.bpf(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Can't really have audio buffers without caps
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate(
|
||||||
|
&self,
|
||||||
|
agg: &Self::Type,
|
||||||
|
timeout: bool,
|
||||||
|
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
// FIXME: Can't really happen because we always return NONE from get_next_time() but that
|
||||||
|
// should be improved!
|
||||||
|
assert!(!timeout);
|
||||||
|
|
||||||
|
// Because peek_buffer() can call into clip() and that would take the state lock again,
|
||||||
|
// first try getting buffers from both pads here
|
||||||
|
let video_buffer_and_segment = match self.video_pad.peek_buffer() {
|
||||||
|
Some(video_buffer) => {
|
||||||
|
let video_segment = self.video_pad.segment();
|
||||||
|
let video_segment = match video_segment.downcast::<gst::ClockTime>() {
|
||||||
|
Ok(video_segment) => video_segment,
|
||||||
|
Err(video_segment) => {
|
||||||
|
gst_error!(
|
||||||
|
CAT,
|
||||||
|
obj: agg,
|
||||||
|
"Video segment of wrong format {:?}",
|
||||||
|
video_segment.format()
|
||||||
|
);
|
||||||
|
return Err(gst::FlowError::Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((video_buffer, video_segment))
|
||||||
|
}
|
||||||
|
None if !self.video_pad.is_eos() => {
|
||||||
|
gst_trace!(CAT, obj: agg, "Waiting for video buffer");
|
||||||
|
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let audio_buffer_segment_and_pad;
|
||||||
|
if let Some(audio_pad) = self.audio_pad.lock().unwrap().clone() {
|
||||||
|
audio_buffer_segment_and_pad = match audio_pad.peek_buffer() {
|
||||||
|
Some(audio_buffer) if audio_buffer.size() == 0 => {
|
||||||
|
// Skip empty/gap audio buffer
|
||||||
|
audio_pad.drop_buffer();
|
||||||
|
gst_trace!(CAT, obj: agg, "Empty audio buffer, waiting for next");
|
||||||
|
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
|
||||||
|
}
|
||||||
|
Some(audio_buffer) => {
|
||||||
|
let audio_segment = audio_pad.segment();
|
||||||
|
let audio_segment = match audio_segment.downcast::<gst::ClockTime>() {
|
||||||
|
Ok(audio_segment) => audio_segment,
|
||||||
|
Err(audio_segment) => {
|
||||||
|
gst_error!(
|
||||||
|
CAT,
|
||||||
|
obj: agg,
|
||||||
|
"Audio segment of wrong format {:?}",
|
||||||
|
audio_segment.format()
|
||||||
|
);
|
||||||
|
return Err(gst::FlowError::Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((audio_buffer, audio_segment, audio_pad))
|
||||||
|
}
|
||||||
|
None if !audio_pad.is_eos() => {
|
||||||
|
gst_trace!(CAT, obj: agg, "Waiting for audio buffer");
|
||||||
|
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
audio_buffer_segment_and_pad = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
let state = match &mut *state_storage {
|
||||||
|
Some(ref mut state) => state,
|
||||||
|
None => return Err(gst::FlowError::Flushing),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut current_video_buffer, current_video_running_time_end, next_video_buffer) =
|
||||||
|
if let Some((video_buffer, video_segment)) = video_buffer_and_segment {
|
||||||
|
let video_running_time = video_segment.to_running_time(video_buffer.pts()).unwrap();
|
||||||
|
|
||||||
|
match state.current_video_buffer {
|
||||||
|
None => {
|
||||||
|
gst_trace!(CAT, obj: agg, "First video buffer, waiting for second");
|
||||||
|
state.current_video_buffer = Some((video_buffer, video_running_time));
|
||||||
|
drop(state_storage);
|
||||||
|
self.video_pad.drop_buffer();
|
||||||
|
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
|
||||||
|
}
|
||||||
|
Some((ref buffer, _)) => (
|
||||||
|
buffer.clone(),
|
||||||
|
Some(video_running_time),
|
||||||
|
Some((video_buffer, video_running_time)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match (&state.current_video_buffer, &audio_buffer_segment_and_pad) {
|
||||||
|
(None, None) => {
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: agg,
|
||||||
|
"All pads are EOS and no buffers are queued, finishing"
|
||||||
|
);
|
||||||
|
return Err(gst::FlowError::Eos);
|
||||||
|
}
|
||||||
|
(None, Some((ref audio_buffer, ref audio_segment, _))) => {
|
||||||
|
// Create an empty dummy buffer for attaching the audio. This is going to
|
||||||
|
// be dropped by the sink later.
|
||||||
|
let audio_running_time =
|
||||||
|
audio_segment.to_running_time(audio_buffer.pts()).unwrap();
|
||||||
|
|
||||||
|
let video_segment = self.video_pad.segment();
|
||||||
|
let video_segment = match video_segment.downcast::<gst::ClockTime>() {
|
||||||
|
Ok(video_segment) => video_segment,
|
||||||
|
Err(video_segment) => {
|
||||||
|
gst_error!(
|
||||||
|
CAT,
|
||||||
|
obj: agg,
|
||||||
|
"Video segment of wrong format {:?}",
|
||||||
|
video_segment.format()
|
||||||
|
);
|
||||||
|
return Err(gst::FlowError::Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let video_pts =
|
||||||
|
video_segment.position_from_running_time(audio_running_time);
|
||||||
|
if video_pts.is_none() {
|
||||||
|
gst_warning!(CAT, obj: agg, "Can't output more audio after video EOS");
|
||||||
|
return Err(gst::FlowError::Eos);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = gst::Buffer::new();
|
||||||
|
{
|
||||||
|
let buffer = buffer.get_mut().unwrap();
|
||||||
|
buffer.set_pts(video_pts);
|
||||||
|
}
|
||||||
|
|
||||||
|
(buffer, gst::ClockTime::NONE, None)
|
||||||
|
}
|
||||||
|
(Some((ref buffer, _)), _) => (buffer.clone(), gst::ClockTime::NONE, None),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((audio_buffer, audio_segment, audio_pad)) = audio_buffer_segment_and_pad {
|
||||||
|
let audio_info = match state.audio_info {
|
||||||
|
Some(ref audio_info) => audio_info,
|
||||||
|
None => {
|
||||||
|
gst_error!(CAT, obj: agg, "Have no audio caps");
|
||||||
|
return Err(gst::FlowError::NotNegotiated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let audio_running_time = audio_segment.to_running_time(audio_buffer.pts());
|
||||||
|
let duration = gst::ClockTime::SECOND.mul_div_floor(
|
||||||
|
audio_buffer.size() as u64 / audio_info.bpf() as u64,
|
||||||
|
audio_info.rate() as u64,
|
||||||
|
);
|
||||||
|
let audio_running_time_end = audio_running_time
|
||||||
|
.zip(duration)
|
||||||
|
.and_then(|(running_time, duration)| running_time.checked_add(duration));
|
||||||
|
|
||||||
|
if audio_running_time_end
|
||||||
|
.zip(current_video_running_time_end)
|
||||||
|
.map(|(audio, video)| audio <= video)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
let timecode = agg
|
||||||
|
.base_time()
|
||||||
|
.zip(audio_running_time)
|
||||||
|
.map(|(base_time, audio_running_time)| {
|
||||||
|
((base_time.nseconds() + audio_running_time.nseconds()) / 100) as i64
|
||||||
|
})
|
||||||
|
.unwrap_or(crate::ndisys::NDIlib_send_timecode_synthesize);
|
||||||
|
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: agg,
|
||||||
|
"Including audio buffer {:?} with timecode {}: {} <= {}",
|
||||||
|
audio_buffer,
|
||||||
|
timecode,
|
||||||
|
audio_running_time_end.display(),
|
||||||
|
current_video_running_time_end.display(),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.current_audio_buffers
|
||||||
|
.push((audio_buffer, audio_info.clone(), timecode));
|
||||||
|
audio_pad.drop_buffer();
|
||||||
|
|
||||||
|
// If there is still video data, wait for the next audio buffer or EOS,
|
||||||
|
// otherwise just output the dummy video buffer directly.
|
||||||
|
if current_video_running_time_end.is_some() {
|
||||||
|
return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise finish this video buffer with all audio that has accumulated so
|
||||||
|
// far
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio_buffers = mem::take(&mut state.current_audio_buffers);
|
||||||
|
|
||||||
|
if !audio_buffers.is_empty() {
|
||||||
|
let current_video_buffer = current_video_buffer.make_mut();
|
||||||
|
crate::ndisinkmeta::NdiSinkAudioMeta::add(current_video_buffer, audio_buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((video_buffer, video_running_time)) = next_video_buffer {
|
||||||
|
state.current_video_buffer = Some((video_buffer, video_running_time));
|
||||||
|
drop(state_storage);
|
||||||
|
self.video_pad.drop_buffer();
|
||||||
|
} else {
|
||||||
|
state.current_video_buffer = None;
|
||||||
|
drop(state_storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
gst_trace!(
|
||||||
|
CAT,
|
||||||
|
obj: agg,
|
||||||
|
"Finishing video buffer {:?}",
|
||||||
|
current_video_buffer
|
||||||
|
);
|
||||||
|
agg.finish_buffer(current_video_buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink_event(
|
||||||
|
&self,
|
||||||
|
agg: &Self::Type,
|
||||||
|
pad: &gst_base::AggregatorPad,
|
||||||
|
event: gst::Event,
|
||||||
|
) -> bool {
|
||||||
|
use gst::EventView;
|
||||||
|
|
||||||
|
match event.view() {
|
||||||
|
EventView::Caps(caps) => {
|
||||||
|
let caps = caps.caps_owned();
|
||||||
|
|
||||||
|
let mut state_storage = self.state.lock().unwrap();
|
||||||
|
let state = match &mut *state_storage {
|
||||||
|
Some(ref mut state) => state,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if pad == &self.video_pad {
|
||||||
|
let info = match gst_video::VideoInfo::from_caps(&caps) {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(_) => {
|
||||||
|
gst_error!(CAT, obj: pad, "Failed to parse caps {:?}", caps);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2 frames latency because we queue 1 frame and wait until audio
|
||||||
|
// up to the end of that frame has arrived.
|
||||||
|
let latency = if info.fps().numer() > 0 {
|
||||||
|
gst::ClockTime::SECOND
|
||||||
|
.mul_div_floor(2 * info.fps().denom() as u64, info.fps().numer() as u64)
|
||||||
|
.unwrap_or(80 * gst::ClockTime::MSECOND)
|
||||||
|
} else {
|
||||||
|
// let's assume 25fps and 2 frames latency
|
||||||
|
80 * gst::ClockTime::MSECOND
|
||||||
|
};
|
||||||
|
|
||||||
|
state.video_info = Some(info);
|
||||||
|
|
||||||
|
drop(state_storage);
|
||||||
|
|
||||||
|
agg.set_latency(latency, gst::ClockTime::NONE);
|
||||||
|
|
||||||
|
// The video caps are passed through as the audio is included only in a meta
|
||||||
|
agg.set_src_caps(&caps);
|
||||||
|
} else {
|
||||||
|
let info = match gst_audio::AudioInfo::from_caps(&caps) {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(_) => {
|
||||||
|
gst_error!(CAT, obj: pad, "Failed to parse caps {:?}", caps);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.audio_info = Some(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The video segment is passed through as-is and the video timestamps are preserved
|
||||||
|
EventView::Segment(segment) if pad == &self.video_pad => {
|
||||||
|
let segment = segment.segment();
|
||||||
|
gst_debug!(CAT, obj: agg, "Updating segment {:?}", segment);
|
||||||
|
agg.update_segment(segment);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parent_sink_event(agg, pad, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink_query(
|
||||||
|
&self,
|
||||||
|
agg: &Self::Type,
|
||||||
|
pad: &gst_base::AggregatorPad,
|
||||||
|
query: &mut gst::QueryRef,
|
||||||
|
) -> bool {
|
||||||
|
use gst::QueryView;
|
||||||
|
|
||||||
|
match query.view_mut() {
|
||||||
|
QueryView::Caps(_) if pad == &self.video_pad => {
|
||||||
|
// Directly forward caps queries
|
||||||
|
let srcpad = agg.static_pad("src").unwrap();
|
||||||
|
return srcpad.peer_query(query);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parent_sink_query(agg, pad, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn negotiate(&self, _agg: &Self::Type) -> bool {
|
||||||
|
// No negotiation needed as the video caps are just passed through
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
19
net/ndi/src/ndisinkcombiner/mod.rs
Normal file
19
net/ndi/src/ndisinkcombiner/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use glib::prelude::*;
|
||||||
|
|
||||||
|
mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct NdiSinkCombiner(ObjectSubclass<imp::NdiSinkCombiner>) @extends gst_base::Aggregator, gst::Element, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NdiSinkCombiner {}
|
||||||
|
unsafe impl Sync for NdiSinkCombiner {}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
gst::Element::register(
|
||||||
|
Some(plugin),
|
||||||
|
"ndisinkcombiner",
|
||||||
|
gst::Rank::None,
|
||||||
|
NdiSinkCombiner::static_type(),
|
||||||
|
)
|
||||||
|
}
|
142
net/ndi/src/ndisinkmeta.rs
Normal file
142
net/ndi/src/ndisinkmeta.rs
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
use gst::prelude::*;
|
||||||
|
use std::fmt;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct NdiSinkAudioMeta(imp::NdiSinkAudioMeta);
|
||||||
|
|
||||||
|
unsafe impl Send for NdiSinkAudioMeta {}
|
||||||
|
unsafe impl Sync for NdiSinkAudioMeta {}
|
||||||
|
|
||||||
|
impl NdiSinkAudioMeta {
|
||||||
|
pub fn add(
|
||||||
|
buffer: &mut gst::BufferRef,
|
||||||
|
buffers: Vec<(gst::Buffer, gst_audio::AudioInfo, i64)>,
|
||||||
|
) -> gst::MetaRefMut<Self, gst::meta::Standalone> {
|
||||||
|
unsafe {
|
||||||
|
// Manually dropping because gst_buffer_add_meta() takes ownership of the
|
||||||
|
// content of the struct
|
||||||
|
let mut params = mem::ManuallyDrop::new(imp::NdiSinkAudioMetaParams { buffers });
|
||||||
|
|
||||||
|
let meta = gst::ffi::gst_buffer_add_meta(
|
||||||
|
buffer.as_mut_ptr(),
|
||||||
|
imp::ndi_sink_audio_meta_get_info(),
|
||||||
|
&mut *params as *mut imp::NdiSinkAudioMetaParams as glib::ffi::gpointer,
|
||||||
|
) as *mut imp::NdiSinkAudioMeta;
|
||||||
|
|
||||||
|
Self::from_mut_ptr(buffer, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffers(&self) -> &[(gst::Buffer, gst_audio::AudioInfo, i64)] {
|
||||||
|
&self.0.buffers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl MetaAPI for NdiSinkAudioMeta {
|
||||||
|
type GstType = imp::NdiSinkAudioMeta;
|
||||||
|
|
||||||
|
fn meta_api() -> glib::Type {
|
||||||
|
imp::ndi_sink_audio_meta_api_get_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for NdiSinkAudioMeta {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("NdiSinkAudioMeta")
|
||||||
|
.field("buffers", &self.buffers())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use glib::translate::*;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::mem;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
pub(super) struct NdiSinkAudioMetaParams {
|
||||||
|
pub buffers: Vec<(gst::Buffer, gst_audio::AudioInfo, i64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct NdiSinkAudioMeta {
|
||||||
|
parent: gst::ffi::GstMeta,
|
||||||
|
pub(super) buffers: Vec<(gst::Buffer, gst_audio::AudioInfo, i64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ndi_sink_audio_meta_api_get_type() -> glib::Type {
|
||||||
|
static TYPE: Lazy<glib::Type> = Lazy::new(|| unsafe {
|
||||||
|
let t = from_glib(gst::ffi::gst_meta_api_type_register(
|
||||||
|
b"GstNdiSinkAudioMetaAPI\0".as_ptr() as *const _,
|
||||||
|
[ptr::null::<std::os::raw::c_char>()].as_ptr() as *mut *const _,
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_ne!(t, glib::Type::INVALID);
|
||||||
|
|
||||||
|
t
|
||||||
|
});
|
||||||
|
|
||||||
|
*TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn ndi_sink_audio_meta_init(
|
||||||
|
meta: *mut gst::ffi::GstMeta,
|
||||||
|
params: glib::ffi::gpointer,
|
||||||
|
_buffer: *mut gst::ffi::GstBuffer,
|
||||||
|
) -> glib::ffi::gboolean {
|
||||||
|
assert!(!params.is_null());
|
||||||
|
|
||||||
|
let meta = &mut *(meta as *mut NdiSinkAudioMeta);
|
||||||
|
let params = ptr::read(params as *const NdiSinkAudioMetaParams);
|
||||||
|
|
||||||
|
ptr::write(&mut meta.buffers, params.buffers);
|
||||||
|
|
||||||
|
true.into_glib()
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn ndi_sink_audio_meta_free(
|
||||||
|
meta: *mut gst::ffi::GstMeta,
|
||||||
|
_buffer: *mut gst::ffi::GstBuffer,
|
||||||
|
) {
|
||||||
|
let meta = &mut *(meta as *mut NdiSinkAudioMeta);
|
||||||
|
|
||||||
|
ptr::drop_in_place(&mut meta.buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn ndi_sink_audio_meta_transform(
|
||||||
|
dest: *mut gst::ffi::GstBuffer,
|
||||||
|
meta: *mut gst::ffi::GstMeta,
|
||||||
|
_buffer: *mut gst::ffi::GstBuffer,
|
||||||
|
_type_: glib::ffi::GQuark,
|
||||||
|
_data: glib::ffi::gpointer,
|
||||||
|
) -> glib::ffi::gboolean {
|
||||||
|
let meta = &*(meta as *mut NdiSinkAudioMeta);
|
||||||
|
|
||||||
|
super::NdiSinkAudioMeta::add(gst::BufferRef::from_mut_ptr(dest), meta.buffers.clone());
|
||||||
|
|
||||||
|
true.into_glib()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ndi_sink_audio_meta_get_info() -> *const gst::ffi::GstMetaInfo {
|
||||||
|
struct MetaInfo(ptr::NonNull<gst::ffi::GstMetaInfo>);
|
||||||
|
unsafe impl Send for MetaInfo {}
|
||||||
|
unsafe impl Sync for MetaInfo {}
|
||||||
|
|
||||||
|
static META_INFO: Lazy<MetaInfo> = Lazy::new(|| unsafe {
|
||||||
|
MetaInfo(
|
||||||
|
ptr::NonNull::new(gst::ffi::gst_meta_register(
|
||||||
|
ndi_sink_audio_meta_api_get_type().into_glib(),
|
||||||
|
b"GstNdiSinkAudioMeta\0".as_ptr() as *const _,
|
||||||
|
mem::size_of::<NdiSinkAudioMeta>(),
|
||||||
|
Some(ndi_sink_audio_meta_init),
|
||||||
|
Some(ndi_sink_audio_meta_free),
|
||||||
|
Some(ndi_sink_audio_meta_transform),
|
||||||
|
) as *mut gst::ffi::GstMetaInfo)
|
||||||
|
.expect("Failed to register meta API"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
META_INFO.0.as_ptr()
|
||||||
|
}
|
||||||
|
}
|
632
net/ndi/src/ndisrc/imp.rs
Normal file
632
net/ndi/src/ndisrc/imp.rs
Normal file
|
@ -0,0 +1,632 @@
|
||||||
|
use gst::prelude::*;
|
||||||
|
use gst::subclass::prelude::*;
|
||||||
|
use gst::{gst_debug, gst_error};
|
||||||
|
use gst_base::prelude::*;
|
||||||
|
use gst_base::subclass::base_src::CreateSuccess;
|
||||||
|
use gst_base::subclass::prelude::*;
|
||||||
|
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::{i32, u32};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::ndisys;
|
||||||
|
|
||||||
|
use crate::ndisrcmeta;
|
||||||
|
use crate::Buffer;
|
||||||
|
use crate::Receiver;
|
||||||
|
use crate::ReceiverControlHandle;
|
||||||
|
use crate::ReceiverItem;
|
||||||
|
use crate::RecvColorFormat;
|
||||||
|
use crate::TimestampMode;
|
||||||
|
use crate::DEFAULT_RECEIVER_NDI_NAME;
|
||||||
|
|
||||||
|
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new(
|
||||||
|
"ndisrc",
|
||||||
|
gst::DebugColorFlags::empty(),
|
||||||
|
Some("NewTek NDI Source"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Settings {
|
||||||
|
ndi_name: Option<String>,
|
||||||
|
url_address: Option<String>,
|
||||||
|
connect_timeout: u32,
|
||||||
|
timeout: u32,
|
||||||
|
max_queue_length: u32,
|
||||||
|
receiver_ndi_name: String,
|
||||||
|
bandwidth: ndisys::NDIlib_recv_bandwidth_e,
|
||||||
|
color_format: RecvColorFormat,
|
||||||
|
timestamp_mode: TimestampMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Settings {
|
||||||
|
ndi_name: None,
|
||||||
|
url_address: None,
|
||||||
|
receiver_ndi_name: DEFAULT_RECEIVER_NDI_NAME.clone(),
|
||||||
|
connect_timeout: 10000,
|
||||||
|
timeout: 5000,
|
||||||
|
max_queue_length: 10,
|
||||||
|
bandwidth: ndisys::NDIlib_recv_bandwidth_highest,
|
||||||
|
color_format: RecvColorFormat::UyvyBgra,
|
||||||
|
timestamp_mode: TimestampMode::ReceiveTimeTimecode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
video_info: Option<crate::VideoInfo>,
|
||||||
|
video_caps: Option<gst::Caps>,
|
||||||
|
audio_info: Option<crate::AudioInfo>,
|
||||||
|
audio_caps: Option<gst::Caps>,
|
||||||
|
current_latency: Option<gst::ClockTime>,
|
||||||
|
receiver: Option<Receiver>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> State {
|
||||||
|
State {
|
||||||
|
video_info: None,
|
||||||
|
video_caps: None,
|
||||||
|
audio_info: None,
|
||||||
|
audio_caps: None,
|
||||||
|
current_latency: gst::ClockTime::NONE,
|
||||||
|
receiver: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NdiSrc {
|
||||||
|
settings: Mutex<Settings>,
|
||||||
|
state: Mutex<State>,
|
||||||
|
receiver_controller: Mutex<Option<ReceiverControlHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for NdiSrc {
|
||||||
|
const NAME: &'static str = "NdiSrc";
|
||||||
|
type Type = super::NdiSrc;
|
||||||
|
type ParentType = gst_base::BaseSrc;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
settings: Mutex::new(Default::default()),
|
||||||
|
state: Mutex::new(Default::default()),
|
||||||
|
receiver_controller: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for NdiSrc {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpecString::new(
|
||||||
|
"ndi-name",
|
||||||
|
"NDI Name",
|
||||||
|
"NDI stream name of the sender",
|
||||||
|
None,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecString::new(
|
||||||
|
"url-address",
|
||||||
|
"URL/Address",
|
||||||
|
"URL/address and port of the sender, e.g. 127.0.0.1:5961",
|
||||||
|
None,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecString::new(
|
||||||
|
"receiver-ndi-name",
|
||||||
|
"Receiver NDI Name",
|
||||||
|
"NDI stream name of this receiver",
|
||||||
|
Some(&*DEFAULT_RECEIVER_NDI_NAME),
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecUInt::new(
|
||||||
|
"connect-timeout",
|
||||||
|
"Connect Timeout",
|
||||||
|
"Connection timeout in ms",
|
||||||
|
0,
|
||||||
|
u32::MAX,
|
||||||
|
10000,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecUInt::new(
|
||||||
|
"timeout",
|
||||||
|
"Timeout",
|
||||||
|
"Receive timeout in ms",
|
||||||
|
0,
|
||||||
|
u32::MAX,
|
||||||
|
5000,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecUInt::new(
|
||||||
|
"max-queue-length",
|
||||||
|
"Max Queue Length",
|
||||||
|
"Maximum receive queue length",
|
||||||
|
0,
|
||||||
|
u32::MAX,
|
||||||
|
10,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecInt::new(
|
||||||
|
"bandwidth",
|
||||||
|
"Bandwidth",
|
||||||
|
"Bandwidth, -10 metadata-only, 10 audio-only, 100 highest",
|
||||||
|
-10,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecEnum::new(
|
||||||
|
"color-format",
|
||||||
|
"Color Format",
|
||||||
|
"Receive color format",
|
||||||
|
RecvColorFormat::static_type(),
|
||||||
|
RecvColorFormat::UyvyBgra as u32 as i32,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecEnum::new(
|
||||||
|
"timestamp-mode",
|
||||||
|
"Timestamp Mode",
|
||||||
|
"Timestamp information to use for outgoing PTS",
|
||||||
|
TimestampMode::static_type(),
|
||||||
|
TimestampMode::ReceiveTimeTimecode as i32,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
|
self.parent_constructed(obj);
|
||||||
|
|
||||||
|
// Initialize live-ness and notify the base class that
|
||||||
|
// we'd like to operate in Time format
|
||||||
|
obj.set_live(true);
|
||||||
|
obj.set_format(gst::Format::Time);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"ndi-name" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let ndi_name = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing ndi-name from {:?} to {:?}",
|
||||||
|
settings.ndi_name,
|
||||||
|
ndi_name,
|
||||||
|
);
|
||||||
|
settings.ndi_name = ndi_name;
|
||||||
|
}
|
||||||
|
"url-address" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let url_address = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing url-address from {:?} to {:?}",
|
||||||
|
settings.url_address,
|
||||||
|
url_address,
|
||||||
|
);
|
||||||
|
settings.url_address = url_address;
|
||||||
|
}
|
||||||
|
"receiver-ndi-name" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let receiver_ndi_name = value.get::<Option<String>>().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing receiver-ndi-name from {:?} to {:?}",
|
||||||
|
settings.receiver_ndi_name,
|
||||||
|
receiver_ndi_name,
|
||||||
|
);
|
||||||
|
settings.receiver_ndi_name =
|
||||||
|
receiver_ndi_name.unwrap_or_else(|| DEFAULT_RECEIVER_NDI_NAME.clone());
|
||||||
|
}
|
||||||
|
"connect-timeout" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let connect_timeout = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing connect-timeout from {} to {}",
|
||||||
|
settings.connect_timeout,
|
||||||
|
connect_timeout,
|
||||||
|
);
|
||||||
|
settings.connect_timeout = connect_timeout;
|
||||||
|
}
|
||||||
|
"timeout" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let timeout = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing timeout from {} to {}",
|
||||||
|
settings.timeout,
|
||||||
|
timeout,
|
||||||
|
);
|
||||||
|
settings.timeout = timeout;
|
||||||
|
}
|
||||||
|
"max-queue-length" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let max_queue_length = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing max-queue-length from {} to {}",
|
||||||
|
settings.max_queue_length,
|
||||||
|
max_queue_length,
|
||||||
|
);
|
||||||
|
settings.max_queue_length = max_queue_length;
|
||||||
|
}
|
||||||
|
"bandwidth" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let bandwidth = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing bandwidth from {} to {}",
|
||||||
|
settings.bandwidth,
|
||||||
|
bandwidth,
|
||||||
|
);
|
||||||
|
settings.bandwidth = bandwidth;
|
||||||
|
}
|
||||||
|
"color-format" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let color_format = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing color format from {:?} to {:?}",
|
||||||
|
settings.color_format,
|
||||||
|
color_format,
|
||||||
|
);
|
||||||
|
settings.color_format = color_format;
|
||||||
|
}
|
||||||
|
"timestamp-mode" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let timestamp_mode = value.get().unwrap();
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing timestamp mode from {:?} to {:?}",
|
||||||
|
settings.timestamp_mode,
|
||||||
|
timestamp_mode
|
||||||
|
);
|
||||||
|
if settings.timestamp_mode != timestamp_mode {
|
||||||
|
let _ = obj.post_message(gst::message::Latency::builder().src(obj).build());
|
||||||
|
}
|
||||||
|
settings.timestamp_mode = timestamp_mode;
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"ndi-name" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.ndi_name.to_value()
|
||||||
|
}
|
||||||
|
"url-address" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.url_address.to_value()
|
||||||
|
}
|
||||||
|
"receiver-ndi-name" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.receiver_ndi_name.to_value()
|
||||||
|
}
|
||||||
|
"connect-timeout" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.connect_timeout.to_value()
|
||||||
|
}
|
||||||
|
"timeout" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.timeout.to_value()
|
||||||
|
}
|
||||||
|
"max-queue-length" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.max_queue_length.to_value()
|
||||||
|
}
|
||||||
|
"bandwidth" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.bandwidth.to_value()
|
||||||
|
}
|
||||||
|
"color-format" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.color_format.to_value()
|
||||||
|
}
|
||||||
|
"timestamp-mode" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.timestamp_mode.to_value()
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GstObjectImpl for NdiSrc {}
|
||||||
|
|
||||||
|
impl ElementImpl for NdiSrc {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||||
|
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"NewTek NDI Source",
|
||||||
|
"Source/Audio/Video/Network",
|
||||||
|
"NewTek NDI source",
|
||||||
|
"Ruben Gonzalez <rubenrua@teltek.es>, Daniel Vilar <daniel.peiteado@teltek.es>, Sebastian Dröge <sebastian@centricular.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||||
|
let src_pad_template = gst::PadTemplate::new(
|
||||||
|
"src",
|
||||||
|
gst::PadDirection::Src,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&gst::Caps::builder("application/x-ndi").build(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
vec![src_pad_template]
|
||||||
|
});
|
||||||
|
|
||||||
|
PAD_TEMPLATES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_state(
|
||||||
|
&self,
|
||||||
|
element: &Self::Type,
|
||||||
|
transition: gst::StateChange,
|
||||||
|
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
|
||||||
|
match transition {
|
||||||
|
gst::StateChange::PausedToPlaying => {
|
||||||
|
if let Some(ref controller) = *self.receiver_controller.lock().unwrap() {
|
||||||
|
controller.set_playing(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gst::StateChange::PlayingToPaused => {
|
||||||
|
if let Some(ref controller) = *self.receiver_controller.lock().unwrap() {
|
||||||
|
controller.set_playing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gst::StateChange::PausedToReady => {
|
||||||
|
if let Some(ref controller) = *self.receiver_controller.lock().unwrap() {
|
||||||
|
controller.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parent_change_state(element, transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseSrcImpl for NdiSrc {
|
||||||
|
fn negotiate(&self, element: &Self::Type) -> Result<(), gst::LoggableError> {
|
||||||
|
element
|
||||||
|
.set_caps(&gst::Caps::builder("application/x-ndi").build())
|
||||||
|
.map_err(|_| gst::loggable_error!(CAT, "Failed to negotiate caps",))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
gst_debug!(CAT, obj: element, "Unlocking",);
|
||||||
|
if let Some(ref controller) = *self.receiver_controller.lock().unwrap() {
|
||||||
|
controller.set_flushing(true);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_stop(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
gst_debug!(CAT, obj: element, "Stop unlocking",);
|
||||||
|
if let Some(ref controller) = *self.receiver_controller.lock().unwrap() {
|
||||||
|
controller.set_flushing(false);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
*self.state.lock().unwrap() = Default::default();
|
||||||
|
let settings = self.settings.lock().unwrap().clone();
|
||||||
|
|
||||||
|
if settings.ndi_name.is_none() && settings.url_address.is_none() {
|
||||||
|
return Err(gst::error_msg!(
|
||||||
|
gst::LibraryError::Settings,
|
||||||
|
["No NDI name or URL/address given"]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let receiver = Receiver::connect(
|
||||||
|
element.upcast_ref(),
|
||||||
|
settings.ndi_name.as_deref(),
|
||||||
|
settings.url_address.as_deref(),
|
||||||
|
&settings.receiver_ndi_name,
|
||||||
|
settings.connect_timeout,
|
||||||
|
settings.bandwidth,
|
||||||
|
settings.color_format.into(),
|
||||||
|
settings.timestamp_mode,
|
||||||
|
settings.timeout,
|
||||||
|
settings.max_queue_length as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
match receiver {
|
||||||
|
None => Err(gst::error_msg!(
|
||||||
|
gst::ResourceError::NotFound,
|
||||||
|
["Could not connect to this source"]
|
||||||
|
)),
|
||||||
|
Some(receiver) => {
|
||||||
|
*self.receiver_controller.lock().unwrap() =
|
||||||
|
Some(receiver.receiver_control_handle());
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.receiver = Some(receiver);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, _element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
if let Some(ref controller) = self.receiver_controller.lock().unwrap().take() {
|
||||||
|
controller.shutdown();
|
||||||
|
}
|
||||||
|
*self.state.lock().unwrap() = State::default();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self, element: &Self::Type, query: &mut gst::QueryRef) -> bool {
|
||||||
|
use gst::QueryView;
|
||||||
|
|
||||||
|
match query.view_mut() {
|
||||||
|
QueryView::Scheduling(ref mut q) => {
|
||||||
|
q.set(gst::SchedulingFlags::SEQUENTIAL, 1, -1, 0);
|
||||||
|
q.add_scheduling_modes(&[gst::PadMode::Push]);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
QueryView::Latency(ref mut q) => {
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(latency) = state.current_latency {
|
||||||
|
let min = if matches!(
|
||||||
|
settings.timestamp_mode,
|
||||||
|
TimestampMode::ReceiveTimeTimecode | TimestampMode::ReceiveTimeTimestamp
|
||||||
|
) {
|
||||||
|
latency
|
||||||
|
} else {
|
||||||
|
gst::ClockTime::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
let max = settings.max_queue_length as u64 * latency;
|
||||||
|
|
||||||
|
gst_debug!(
|
||||||
|
CAT,
|
||||||
|
obj: element,
|
||||||
|
"Returning latency min {} max {}",
|
||||||
|
min,
|
||||||
|
max
|
||||||
|
);
|
||||||
|
q.set(true, min, max);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => BaseSrcImplExt::parent_query(self, element, query),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
&self,
|
||||||
|
element: &Self::Type,
|
||||||
|
_offset: u64,
|
||||||
|
_buffer: Option<&mut gst::BufferRef>,
|
||||||
|
_length: u32,
|
||||||
|
) -> Result<CreateSuccess, gst::FlowError> {
|
||||||
|
let recv = {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
match state.receiver.take() {
|
||||||
|
Some(recv) => recv,
|
||||||
|
None => {
|
||||||
|
gst_error!(CAT, obj: element, "Have no receiver");
|
||||||
|
return Err(gst::FlowError::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = recv.capture();
|
||||||
|
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.receiver = Some(recv);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
ReceiverItem::Buffer(buffer) => {
|
||||||
|
let buffer = match buffer {
|
||||||
|
Buffer::Audio(mut buffer, info) => {
|
||||||
|
if state.audio_info.as_ref() != Some(&info) {
|
||||||
|
let caps = info.to_caps().map_err(|_| {
|
||||||
|
gst::element_error!(
|
||||||
|
element,
|
||||||
|
gst::ResourceError::Settings,
|
||||||
|
["Invalid audio info received: {:?}", info]
|
||||||
|
);
|
||||||
|
gst::FlowError::NotNegotiated
|
||||||
|
})?;
|
||||||
|
state.audio_info = Some(info);
|
||||||
|
state.audio_caps = Some(caps);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let buffer = buffer.get_mut().unwrap();
|
||||||
|
ndisrcmeta::NdiSrcMeta::add(
|
||||||
|
buffer,
|
||||||
|
ndisrcmeta::StreamType::Audio,
|
||||||
|
state.audio_caps.as_ref().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
Buffer::Video(mut buffer, info) => {
|
||||||
|
let mut latency_changed = false;
|
||||||
|
|
||||||
|
if state.video_info.as_ref() != Some(&info) {
|
||||||
|
let caps = info.to_caps().map_err(|_| {
|
||||||
|
gst::element_error!(
|
||||||
|
element,
|
||||||
|
gst::ResourceError::Settings,
|
||||||
|
["Invalid audio info received: {:?}", info]
|
||||||
|
);
|
||||||
|
gst::FlowError::NotNegotiated
|
||||||
|
})?;
|
||||||
|
state.video_info = Some(info);
|
||||||
|
state.video_caps = Some(caps);
|
||||||
|
latency_changed = state.current_latency != buffer.duration();
|
||||||
|
state.current_latency = buffer.duration();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let buffer = buffer.get_mut().unwrap();
|
||||||
|
ndisrcmeta::NdiSrcMeta::add(
|
||||||
|
buffer,
|
||||||
|
ndisrcmeta::StreamType::Video,
|
||||||
|
state.video_caps.as_ref().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(state);
|
||||||
|
if latency_changed {
|
||||||
|
let _ = element.post_message(
|
||||||
|
gst::message::Latency::builder().src(element).build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CreateSuccess::NewBuffer(buffer))
|
||||||
|
}
|
||||||
|
ReceiverItem::Timeout => Err(gst::FlowError::Eos),
|
||||||
|
ReceiverItem::Flushing => Err(gst::FlowError::Flushing),
|
||||||
|
ReceiverItem::Error(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
net/ndi/src/ndisrc/mod.rs
Normal file
19
net/ndi/src/ndisrc/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use glib::prelude::*;
|
||||||
|
|
||||||
|
mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct NdiSrc(ObjectSubclass<imp::NdiSrc>) @extends gst_base::BaseSrc, gst::Element, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NdiSrc {}
|
||||||
|
unsafe impl Sync for NdiSrc {}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
gst::Element::register(
|
||||||
|
Some(plugin),
|
||||||
|
"ndisrc",
|
||||||
|
gst::Rank::None,
|
||||||
|
NdiSrc::static_type(),
|
||||||
|
)
|
||||||
|
}
|
312
net/ndi/src/ndisrcdemux/imp.rs
Normal file
312
net/ndi/src/ndisrcdemux/imp.rs
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
use gst::prelude::*;
|
||||||
|
use gst::subclass::prelude::*;
|
||||||
|
use gst::{gst_debug, gst_error, gst_log};
|
||||||
|
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::ndisrcmeta;
|
||||||
|
|
||||||
|
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new(
|
||||||
|
"ndisrcdemux",
|
||||||
|
gst::DebugColorFlags::empty(),
|
||||||
|
Some("NewTek NDI Source Demuxer"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
combiner: gst_base::UniqueFlowCombiner,
|
||||||
|
video_pad: Option<gst::Pad>,
|
||||||
|
video_caps: Option<gst::Caps>,
|
||||||
|
|
||||||
|
audio_pad: Option<gst::Pad>,
|
||||||
|
audio_caps: Option<gst::Caps>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NdiSrcDemux {
|
||||||
|
sinkpad: gst::Pad,
|
||||||
|
state: Mutex<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for NdiSrcDemux {
|
||||||
|
const NAME: &'static str = "NdiSrcDemux";
|
||||||
|
type Type = super::NdiSrcDemux;
|
||||||
|
type ParentType = gst::Element;
|
||||||
|
|
||||||
|
fn with_class(klass: &Self::Class) -> Self {
|
||||||
|
let templ = klass.pad_template("sink").unwrap();
|
||||||
|
let sinkpad = gst::Pad::builder_with_template(&templ, Some("sink"))
|
||||||
|
.flags(gst::PadFlags::FIXED_CAPS)
|
||||||
|
.chain_function(|pad, parent, buffer| {
|
||||||
|
NdiSrcDemux::catch_panic_pad_function(
|
||||||
|
parent,
|
||||||
|
|| Err(gst::FlowError::Error),
|
||||||
|
|self_, element| self_.sink_chain(pad, element, buffer),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.event_function(|pad, parent, event| {
|
||||||
|
NdiSrcDemux::catch_panic_pad_function(
|
||||||
|
parent,
|
||||||
|
|| false,
|
||||||
|
|self_, element| self_.sink_event(pad, element, event),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sinkpad,
|
||||||
|
state: Mutex::new(State::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for NdiSrcDemux {
|
||||||
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
|
self.parent_constructed(obj);
|
||||||
|
|
||||||
|
obj.add_pad(&self.sinkpad).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GstObjectImpl for NdiSrcDemux {}
|
||||||
|
|
||||||
|
impl ElementImpl for NdiSrcDemux {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||||
|
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"NewTek NDI Source Demuxer",
|
||||||
|
"Demuxer/Audio/Video",
|
||||||
|
"NewTek NDI source demuxer",
|
||||||
|
"Sebastian Dröge <sebastian@centricular.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||||
|
let sink_pad_template = gst::PadTemplate::new(
|
||||||
|
"sink",
|
||||||
|
gst::PadDirection::Sink,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&gst::Caps::builder("application/x-ndi").build(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let audio_src_pad_template = gst::PadTemplate::new(
|
||||||
|
"audio",
|
||||||
|
gst::PadDirection::Src,
|
||||||
|
gst::PadPresence::Sometimes,
|
||||||
|
&gst::Caps::builder("audio/x-raw").build(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let video_src_pad_template = gst::PadTemplate::new(
|
||||||
|
"video",
|
||||||
|
gst::PadDirection::Src,
|
||||||
|
gst::PadPresence::Sometimes,
|
||||||
|
&gst::Caps::builder("video/x-raw").build(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
vec![
|
||||||
|
sink_pad_template,
|
||||||
|
audio_src_pad_template,
|
||||||
|
video_src_pad_template,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PAD_TEMPLATES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_state(
|
||||||
|
&self,
|
||||||
|
element: &Self::Type,
|
||||||
|
transition: gst::StateChange,
|
||||||
|
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
|
||||||
|
let res = self.parent_change_state(element, transition)?;
|
||||||
|
|
||||||
|
match transition {
|
||||||
|
gst::StateChange::PausedToReady => {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
for pad in [state.audio_pad.take(), state.video_pad.take()]
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
element.remove_pad(pad).unwrap();
|
||||||
|
}
|
||||||
|
*state = State::default();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NdiSrcDemux {
|
||||||
|
fn sink_chain(
|
||||||
|
&self,
|
||||||
|
pad: &gst::Pad,
|
||||||
|
element: &super::NdiSrcDemux,
|
||||||
|
mut buffer: gst::Buffer,
|
||||||
|
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
gst_log!(CAT, obj: pad, "Handling buffer {:?}", buffer);
|
||||||
|
|
||||||
|
let meta = buffer.make_mut().meta_mut::<ndisrcmeta::NdiSrcMeta>().ok_or_else(|| {
|
||||||
|
gst_error!(CAT, obj: element, "Buffer without NDI source meta");
|
||||||
|
gst::FlowError::Error
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut events = vec![];
|
||||||
|
let srcpad;
|
||||||
|
let mut add_pad = false;
|
||||||
|
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
let caps = meta.caps();
|
||||||
|
match meta.stream_type() {
|
||||||
|
ndisrcmeta::StreamType::Audio => {
|
||||||
|
if let Some(ref pad) = state.audio_pad {
|
||||||
|
srcpad = pad.clone();
|
||||||
|
} else {
|
||||||
|
gst_debug!(CAT, obj: element, "Adding audio pad with caps {}", caps);
|
||||||
|
|
||||||
|
let klass = element.element_class();
|
||||||
|
let templ = klass.pad_template("audio").unwrap();
|
||||||
|
let pad = gst::Pad::builder_with_template(&templ, Some("audio"))
|
||||||
|
.flags(gst::PadFlags::FIXED_CAPS)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut caps_event = Some(gst::event::Caps::new(&caps));
|
||||||
|
|
||||||
|
self.sinkpad.sticky_events_foreach(|ev| {
|
||||||
|
if ev.type_() < gst::EventType::Caps {
|
||||||
|
events.push(ev.clone());
|
||||||
|
} else {
|
||||||
|
if let Some(ev) = caps_event.take() {
|
||||||
|
events.push(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev.type_() != gst::EventType::Caps {
|
||||||
|
events.push(ev.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ops::ControlFlow::Continue(gst::EventForeachAction::Keep)
|
||||||
|
});
|
||||||
|
|
||||||
|
state.audio_caps = Some(caps.clone());
|
||||||
|
state.audio_pad = Some(pad.clone());
|
||||||
|
|
||||||
|
let _ = pad.set_active(true);
|
||||||
|
for ev in events.drain(..) {
|
||||||
|
let _ = pad.store_sticky_event(&ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.combiner.add_pad(&pad);
|
||||||
|
|
||||||
|
add_pad = true;
|
||||||
|
srcpad = pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.audio_caps.as_ref() != Some(&caps) {
|
||||||
|
gst_debug!(CAT, obj: element, "Audio caps changed to {}", caps);
|
||||||
|
events.push(gst::event::Caps::new(&caps));
|
||||||
|
state.audio_caps = Some(caps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ndisrcmeta::StreamType::Video => {
|
||||||
|
if let Some(ref pad) = state.video_pad {
|
||||||
|
srcpad = pad.clone();
|
||||||
|
} else {
|
||||||
|
gst_debug!(CAT, obj: element, "Adding video pad with caps {}", caps);
|
||||||
|
|
||||||
|
let klass = element.element_class();
|
||||||
|
let templ = klass.pad_template("video").unwrap();
|
||||||
|
let pad = gst::Pad::builder_with_template(&templ, Some("video"))
|
||||||
|
.flags(gst::PadFlags::FIXED_CAPS)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut caps_event = Some(gst::event::Caps::new(&caps));
|
||||||
|
|
||||||
|
self.sinkpad.sticky_events_foreach(|ev| {
|
||||||
|
if ev.type_() < gst::EventType::Caps {
|
||||||
|
events.push(ev.clone());
|
||||||
|
} else {
|
||||||
|
if let Some(ev) = caps_event.take() {
|
||||||
|
events.push(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev.type_() != gst::EventType::Caps {
|
||||||
|
events.push(ev.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ops::ControlFlow::Continue(gst::EventForeachAction::Keep)
|
||||||
|
});
|
||||||
|
|
||||||
|
state.video_caps = Some(caps.clone());
|
||||||
|
state.video_pad = Some(pad.clone());
|
||||||
|
|
||||||
|
let _ = pad.set_active(true);
|
||||||
|
for ev in events.drain(..) {
|
||||||
|
let _ = pad.store_sticky_event(&ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.combiner.add_pad(&pad);
|
||||||
|
|
||||||
|
add_pad = true;
|
||||||
|
srcpad = pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.video_caps.as_ref() != Some(&caps) {
|
||||||
|
gst_debug!(CAT, obj: element, "Video caps changed to {}", caps);
|
||||||
|
events.push(gst::event::Caps::new(&caps));
|
||||||
|
state.video_caps = Some(caps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
meta.remove().unwrap();
|
||||||
|
|
||||||
|
if add_pad {
|
||||||
|
element.add_pad(&srcpad).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for ev in events {
|
||||||
|
srcpad.push_event(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = srcpad.push(buffer);
|
||||||
|
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.combiner.update_pad_flow(&srcpad, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink_event(&self,
|
||||||
|
pad: &gst::Pad,
|
||||||
|
element: &super::NdiSrcDemux,
|
||||||
|
event: gst::Event
|
||||||
|
) -> bool {
|
||||||
|
use gst::EventView;
|
||||||
|
|
||||||
|
gst_log!(CAT, obj: pad, "Handling event {:?}", event);
|
||||||
|
if let EventView::Eos(_) = event.view() {
|
||||||
|
if element.num_src_pads() == 0 {
|
||||||
|
// error out on EOS if no src pad are available
|
||||||
|
gst::element_error!(
|
||||||
|
element,
|
||||||
|
gst::StreamError::Demux,
|
||||||
|
["EOS without available srcpad(s)"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pad.event_default(Some(element), event)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
net/ndi/src/ndisrcdemux/mod.rs
Normal file
19
net/ndi/src/ndisrcdemux/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use glib::prelude::*;
|
||||||
|
|
||||||
|
mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct NdiSrcDemux(ObjectSubclass<imp::NdiSrcDemux>) @extends gst::Element, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NdiSrcDemux {}
|
||||||
|
unsafe impl Sync for NdiSrcDemux {}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
gst::Element::register(
|
||||||
|
Some(plugin),
|
||||||
|
"ndisrcdemux",
|
||||||
|
gst::Rank::Primary,
|
||||||
|
NdiSrcDemux::static_type(),
|
||||||
|
)
|
||||||
|
}
|
158
net/ndi/src/ndisrcmeta.rs
Normal file
158
net/ndi/src/ndisrcmeta.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use gst::prelude::*;
|
||||||
|
use std::fmt;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct NdiSrcMeta(imp::NdiSrcMeta);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum StreamType {
|
||||||
|
Audio,
|
||||||
|
Video,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NdiSrcMeta {}
|
||||||
|
unsafe impl Sync for NdiSrcMeta {}
|
||||||
|
|
||||||
|
impl NdiSrcMeta {
|
||||||
|
pub fn add<'a>(
|
||||||
|
buffer: &'a mut gst::BufferRef,
|
||||||
|
stream_type: StreamType,
|
||||||
|
caps: &gst::Caps,
|
||||||
|
) -> gst::MetaRefMut<'a, Self, gst::meta::Standalone> {
|
||||||
|
unsafe {
|
||||||
|
// Manually dropping because gst_buffer_add_meta() takes ownership of the
|
||||||
|
// content of the struct
|
||||||
|
let mut params = mem::ManuallyDrop::new(imp::NdiSrcMetaParams {
|
||||||
|
caps: caps.clone(),
|
||||||
|
stream_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
let meta = gst::ffi::gst_buffer_add_meta(
|
||||||
|
buffer.as_mut_ptr(),
|
||||||
|
imp::ndi_src_meta_get_info(),
|
||||||
|
&mut *params as *mut imp::NdiSrcMetaParams as glib::ffi::gpointer,
|
||||||
|
) as *mut imp::NdiSrcMeta;
|
||||||
|
|
||||||
|
Self::from_mut_ptr(buffer, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream_type(&self) -> StreamType {
|
||||||
|
self.0.stream_type
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caps(&self) -> gst::Caps {
|
||||||
|
self.0.caps.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl MetaAPI for NdiSrcMeta {
|
||||||
|
type GstType = imp::NdiSrcMeta;
|
||||||
|
|
||||||
|
fn meta_api() -> glib::Type {
|
||||||
|
imp::ndi_src_meta_api_get_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for NdiSrcMeta {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("NdiSrcMeta")
|
||||||
|
.field("stream_type", &self.stream_type())
|
||||||
|
.field("caps", &self.caps())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::StreamType;
|
||||||
|
use glib::translate::*;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::mem;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
pub(super) struct NdiSrcMetaParams {
|
||||||
|
pub caps: gst::Caps,
|
||||||
|
pub stream_type: StreamType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct NdiSrcMeta {
|
||||||
|
parent: gst::ffi::GstMeta,
|
||||||
|
pub(super) caps: gst::Caps,
|
||||||
|
pub(super) stream_type: StreamType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ndi_src_meta_api_get_type() -> glib::Type {
|
||||||
|
static TYPE: Lazy<glib::Type> = Lazy::new(|| unsafe {
|
||||||
|
let t = from_glib(gst::ffi::gst_meta_api_type_register(
|
||||||
|
b"GstNdiSrcMetaAPI\0".as_ptr() as *const _,
|
||||||
|
[ptr::null::<std::os::raw::c_char>()].as_ptr() as *mut *const _,
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_ne!(t, glib::Type::INVALID);
|
||||||
|
|
||||||
|
t
|
||||||
|
});
|
||||||
|
|
||||||
|
*TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn ndi_src_meta_init(
|
||||||
|
meta: *mut gst::ffi::GstMeta,
|
||||||
|
params: glib::ffi::gpointer,
|
||||||
|
_buffer: *mut gst::ffi::GstBuffer,
|
||||||
|
) -> glib::ffi::gboolean {
|
||||||
|
assert!(!params.is_null());
|
||||||
|
|
||||||
|
let meta = &mut *(meta as *mut NdiSrcMeta);
|
||||||
|
let params = ptr::read(params as *const NdiSrcMetaParams);
|
||||||
|
|
||||||
|
ptr::write(&mut meta.stream_type, params.stream_type);
|
||||||
|
ptr::write(&mut meta.caps, params.caps);
|
||||||
|
|
||||||
|
true.into_glib()
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn ndi_src_meta_free(
|
||||||
|
meta: *mut gst::ffi::GstMeta,
|
||||||
|
_buffer: *mut gst::ffi::GstBuffer,
|
||||||
|
) {
|
||||||
|
let meta = &mut *(meta as *mut NdiSrcMeta);
|
||||||
|
|
||||||
|
ptr::drop_in_place(&mut meta.stream_type);
|
||||||
|
ptr::drop_in_place(&mut meta.caps);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn ndi_src_meta_transform(
|
||||||
|
_dest: *mut gst::ffi::GstBuffer,
|
||||||
|
_meta: *mut gst::ffi::GstMeta,
|
||||||
|
_buffer: *mut gst::ffi::GstBuffer,
|
||||||
|
_type_: glib::ffi::GQuark,
|
||||||
|
_data: glib::ffi::gpointer,
|
||||||
|
) -> glib::ffi::gboolean {
|
||||||
|
false.into_glib()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ndi_src_meta_get_info() -> *const gst::ffi::GstMetaInfo {
|
||||||
|
struct MetaInfo(ptr::NonNull<gst::ffi::GstMetaInfo>);
|
||||||
|
unsafe impl Send for MetaInfo {}
|
||||||
|
unsafe impl Sync for MetaInfo {}
|
||||||
|
|
||||||
|
static META_INFO: Lazy<MetaInfo> = Lazy::new(|| unsafe {
|
||||||
|
MetaInfo(
|
||||||
|
ptr::NonNull::new(gst::ffi::gst_meta_register(
|
||||||
|
ndi_src_meta_api_get_type().into_glib(),
|
||||||
|
b"GstNdiSrcMeta\0".as_ptr() as *const _,
|
||||||
|
mem::size_of::<NdiSrcMeta>(),
|
||||||
|
Some(ndi_src_meta_init),
|
||||||
|
Some(ndi_src_meta_free),
|
||||||
|
Some(ndi_src_meta_transform),
|
||||||
|
) as *mut gst::ffi::GstMetaInfo)
|
||||||
|
.expect("Failed to register meta API"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
META_INFO.0.as_ptr()
|
||||||
|
}
|
||||||
|
}
|
326
net/ndi/src/ndisys.rs
Normal file
326
net/ndi/src/ndisys.rs
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
#![allow(non_camel_case_types, non_upper_case_globals, non_snake_case)]
|
||||||
|
|
||||||
|
#[cfg_attr(
|
||||||
|
all(target_arch = "x86_64", target_os = "windows"),
|
||||||
|
link(name = "Processing.NDI.Lib.x64")
|
||||||
|
)]
|
||||||
|
#[cfg_attr(
|
||||||
|
all(target_arch = "x86", target_os = "windows"),
|
||||||
|
link(name = "Processing.NDI.Lib.x86")
|
||||||
|
)]
|
||||||
|
#[cfg_attr(
|
||||||
|
not(any(target_os = "windows", target_os = "macos")),
|
||||||
|
link(name = "ndi")
|
||||||
|
)]
|
||||||
|
extern "C" {
|
||||||
|
pub fn NDIlib_initialize() -> bool;
|
||||||
|
pub fn NDIlib_destroy();
|
||||||
|
pub fn NDIlib_find_create_v2(
|
||||||
|
p_create_settings: *const NDIlib_find_create_t,
|
||||||
|
) -> NDIlib_find_instance_t;
|
||||||
|
pub fn NDIlib_find_destroy(p_instance: NDIlib_find_instance_t);
|
||||||
|
pub fn NDIlib_find_wait_for_sources(
|
||||||
|
p_instance: NDIlib_find_instance_t,
|
||||||
|
timeout_in_ms: u32,
|
||||||
|
) -> bool;
|
||||||
|
pub fn NDIlib_find_get_current_sources(
|
||||||
|
p_instance: NDIlib_find_instance_t,
|
||||||
|
p_no_sources: *mut u32,
|
||||||
|
) -> *const NDIlib_source_t;
|
||||||
|
pub fn NDIlib_recv_create_v3(
|
||||||
|
p_create_settings: *const NDIlib_recv_create_v3_t,
|
||||||
|
) -> NDIlib_recv_instance_t;
|
||||||
|
pub fn NDIlib_recv_destroy(p_instance: NDIlib_recv_instance_t);
|
||||||
|
pub fn NDIlib_recv_set_tally(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_tally: *const NDIlib_tally_t,
|
||||||
|
) -> bool;
|
||||||
|
pub fn NDIlib_recv_send_metadata(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_metadata: *const NDIlib_metadata_frame_t,
|
||||||
|
) -> bool;
|
||||||
|
pub fn NDIlib_recv_capture_v3(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_video_data: *mut NDIlib_video_frame_v2_t,
|
||||||
|
p_audio_data: *mut NDIlib_audio_frame_v3_t,
|
||||||
|
p_metadata: *mut NDIlib_metadata_frame_t,
|
||||||
|
timeout_in_ms: u32,
|
||||||
|
) -> NDIlib_frame_type_e;
|
||||||
|
pub fn NDIlib_recv_free_video_v2(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_video_data: *mut NDIlib_video_frame_v2_t,
|
||||||
|
);
|
||||||
|
pub fn NDIlib_recv_free_audio_v3(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_audio_data: *mut NDIlib_audio_frame_v3_t,
|
||||||
|
);
|
||||||
|
pub fn NDIlib_recv_free_metadata(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_metadata: *mut NDIlib_metadata_frame_t,
|
||||||
|
);
|
||||||
|
pub fn NDIlib_recv_get_queue(
|
||||||
|
p_instance: NDIlib_recv_instance_t,
|
||||||
|
p_total: *mut NDIlib_recv_queue_t,
|
||||||
|
);
|
||||||
|
pub fn NDIlib_send_create(
|
||||||
|
p_create_settings: *const NDIlib_send_create_t,
|
||||||
|
) -> NDIlib_send_instance_t;
|
||||||
|
pub fn NDIlib_send_destroy(p_instance: NDIlib_send_instance_t);
|
||||||
|
pub fn NDIlib_send_send_video_v2(
|
||||||
|
p_instance: NDIlib_send_instance_t,
|
||||||
|
p_video_data: *const NDIlib_video_frame_v2_t,
|
||||||
|
);
|
||||||
|
pub fn NDIlib_send_send_audio_v3(
|
||||||
|
p_instance: NDIlib_send_instance_t,
|
||||||
|
p_audio_data: *const NDIlib_audio_frame_v3_t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NDIlib_find_instance_t = *mut ::std::os::raw::c_void;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_find_create_t {
|
||||||
|
pub show_local_sources: bool,
|
||||||
|
pub p_groups: *const ::std::os::raw::c_char,
|
||||||
|
pub p_extra_ips: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_source_t {
|
||||||
|
pub p_ndi_name: *const ::std::os::raw::c_char,
|
||||||
|
pub p_url_address: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum NDIlib_frame_type_e {
|
||||||
|
NDIlib_frame_type_none = 0,
|
||||||
|
NDIlib_frame_type_video = 1,
|
||||||
|
NDIlib_frame_type_audio = 2,
|
||||||
|
NDIlib_frame_type_metadata = 3,
|
||||||
|
NDIlib_frame_type_error = 4,
|
||||||
|
NDIlib_frame_type_status_change = 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NDIlib_recv_bandwidth_e = i32;
|
||||||
|
|
||||||
|
pub const NDIlib_recv_bandwidth_metadata_only: NDIlib_recv_bandwidth_e = -10;
|
||||||
|
pub const NDIlib_recv_bandwidth_audio_only: NDIlib_recv_bandwidth_e = 10;
|
||||||
|
pub const NDIlib_recv_bandwidth_lowest: NDIlib_recv_bandwidth_e = 0;
|
||||||
|
pub const NDIlib_recv_bandwidth_highest: NDIlib_recv_bandwidth_e = 100;
|
||||||
|
|
||||||
|
pub type NDIlib_recv_color_format_e = u32;
|
||||||
|
pub const NDIlib_recv_color_format_BGRX_BGRA: NDIlib_recv_color_format_e = 0;
|
||||||
|
pub const NDIlib_recv_color_format_UYVY_BGRA: NDIlib_recv_color_format_e = 1;
|
||||||
|
pub const NDIlib_recv_color_format_RGBX_RGBA: NDIlib_recv_color_format_e = 2;
|
||||||
|
pub const NDIlib_recv_color_format_UYVY_RGBA: NDIlib_recv_color_format_e = 3;
|
||||||
|
pub const NDIlib_recv_color_format_fastest: NDIlib_recv_color_format_e = 100;
|
||||||
|
pub const NDIlib_recv_color_format_best: NDIlib_recv_color_format_e = 101;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed: NDIlib_recv_color_format_e = 300;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v2: NDIlib_recv_color_format_e = 301;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v3: NDIlib_recv_color_format_e = 302;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v3_with_audio: NDIlib_recv_color_format_e = 304;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v4: NDIlib_recv_color_format_e = 303;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v4_with_audio: NDIlib_recv_color_format_e = 305;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v5: NDIlib_recv_color_format_e = 307;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_recv_color_format_ex_compressed_v5_with_audio: NDIlib_recv_color_format_e = 308;
|
||||||
|
|
||||||
|
const fn make_fourcc(fourcc: &[u8; 4]) -> u32 {
|
||||||
|
((fourcc[0] as u32) << 0)
|
||||||
|
| ((fourcc[1] as u32) << 8)
|
||||||
|
| ((fourcc[2] as u32) << 16)
|
||||||
|
| ((fourcc[3] as u32) << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NDIlib_FourCC_video_type_e = u32;
|
||||||
|
pub const NDIlib_FourCC_video_type_UYVY: NDIlib_FourCC_video_type_e = make_fourcc(b"UYVY");
|
||||||
|
pub const NDIlib_FourCC_video_type_UYVA: NDIlib_FourCC_video_type_e = make_fourcc(b"UYVA");
|
||||||
|
pub const NDIlib_FourCC_video_type_P216: NDIlib_FourCC_video_type_e = make_fourcc(b"P216");
|
||||||
|
pub const NDIlib_FourCC_video_type_PA16: NDIlib_FourCC_video_type_e = make_fourcc(b"PA16");
|
||||||
|
pub const NDIlib_FourCC_video_type_YV12: NDIlib_FourCC_video_type_e = make_fourcc(b"YV12");
|
||||||
|
pub const NDIlib_FourCC_video_type_I420: NDIlib_FourCC_video_type_e = make_fourcc(b"I420");
|
||||||
|
pub const NDIlib_FourCC_video_type_NV12: NDIlib_FourCC_video_type_e = make_fourcc(b"NV12");
|
||||||
|
pub const NDIlib_FourCC_video_type_BGRA: NDIlib_FourCC_video_type_e = make_fourcc(b"BGRA");
|
||||||
|
pub const NDIlib_FourCC_video_type_BGRX: NDIlib_FourCC_video_type_e = make_fourcc(b"BGRX");
|
||||||
|
pub const NDIlib_FourCC_video_type_RGBA: NDIlib_FourCC_video_type_e = make_fourcc(b"RGBA");
|
||||||
|
pub const NDIlib_FourCC_video_type_RGBX: NDIlib_FourCC_video_type_e = make_fourcc(b"RGBX");
|
||||||
|
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_SHQ0_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"SHQ0");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_SHQ2_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"SHQ2");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_SHQ7_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"SHQ7");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_SHQ0_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"shq0");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_SHQ2_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"shq2");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_SHQ7_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"shq7");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_H264_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"H264");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_H264_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"h264");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_HEVC_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"HEVC");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_HEVC_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"hevc");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_H264_alpha_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"A264");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_H264_alpha_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"a264");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_HEVC_alpha_highest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"AEVC");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_video_type_ex_HEVC_alpha_lowest_bandwidth: NDIlib_FourCC_video_type_e =
|
||||||
|
make_fourcc(b"aevc");
|
||||||
|
|
||||||
|
pub type NDIlib_FourCC_audio_type_e = u32;
|
||||||
|
pub const NDIlib_FourCC_audio_type_FLTp: NDIlib_FourCC_video_type_e = make_fourcc(b"FLTp");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_audio_type_AAC: NDIlib_FourCC_audio_type_e = 0x000000ff;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_FourCC_audio_type_Opus: NDIlib_FourCC_audio_type_e = make_fourcc(b"Opus");
|
||||||
|
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub type NDIlib_compressed_FourCC_type_e = u32;
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_compressed_FourCC_type_H264: NDIlib_compressed_FourCC_type_e =
|
||||||
|
make_fourcc(b"H264");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_compressed_FourCC_type_HEVC: NDIlib_compressed_FourCC_type_e =
|
||||||
|
make_fourcc(b"HEVC");
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_compressed_FourCC_type_AAC: NDIlib_compressed_FourCC_type_e = 0x000000ff;
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum NDIlib_frame_format_type_e {
|
||||||
|
NDIlib_frame_format_type_progressive = 1,
|
||||||
|
NDIlib_frame_format_type_interleaved = 0,
|
||||||
|
NDIlib_frame_format_type_field_0 = 2,
|
||||||
|
NDIlib_frame_format_type_field_1 = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const NDIlib_send_timecode_synthesize: i64 = ::std::i64::MAX;
|
||||||
|
pub const NDIlib_recv_timestamp_undefined: i64 = ::std::i64::MAX;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_recv_create_v3_t {
|
||||||
|
pub source_to_connect_to: NDIlib_source_t,
|
||||||
|
pub color_format: NDIlib_recv_color_format_e,
|
||||||
|
pub bandwidth: NDIlib_recv_bandwidth_e,
|
||||||
|
pub allow_video_fields: bool,
|
||||||
|
pub p_ndi_recv_name: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NDIlib_recv_instance_t = *mut ::std::os::raw::c_void;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_send_create_t {
|
||||||
|
pub p_ndi_name: *const ::std::os::raw::c_char,
|
||||||
|
pub p_groups: *const ::std::os::raw::c_char,
|
||||||
|
pub clock_video: bool,
|
||||||
|
pub clock_audio: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NDIlib_send_instance_t = *mut ::std::os::raw::c_void;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_tally_t {
|
||||||
|
pub on_program: bool,
|
||||||
|
pub on_preview: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_recv_queue_t {
|
||||||
|
pub video_frames: i32,
|
||||||
|
pub audio_frames: i32,
|
||||||
|
pub metadata_frames: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_metadata_frame_t {
|
||||||
|
pub length: ::std::os::raw::c_int,
|
||||||
|
pub timecode: i64,
|
||||||
|
pub p_data: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_video_frame_v2_t {
|
||||||
|
pub xres: ::std::os::raw::c_int,
|
||||||
|
pub yres: ::std::os::raw::c_int,
|
||||||
|
pub FourCC: NDIlib_FourCC_video_type_e,
|
||||||
|
pub frame_rate_N: ::std::os::raw::c_int,
|
||||||
|
pub frame_rate_D: ::std::os::raw::c_int,
|
||||||
|
pub picture_aspect_ratio: ::std::os::raw::c_float,
|
||||||
|
pub frame_format_type: NDIlib_frame_format_type_e,
|
||||||
|
pub timecode: i64,
|
||||||
|
pub p_data: *const ::std::os::raw::c_char,
|
||||||
|
pub line_stride_or_data_size_in_bytes: ::std::os::raw::c_int,
|
||||||
|
pub p_metadata: *const ::std::os::raw::c_char,
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_audio_frame_v3_t {
|
||||||
|
pub sample_rate: ::std::os::raw::c_int,
|
||||||
|
pub no_channels: ::std::os::raw::c_int,
|
||||||
|
pub no_samples: ::std::os::raw::c_int,
|
||||||
|
pub timecode: i64,
|
||||||
|
pub FourCC: NDIlib_FourCC_audio_type_e,
|
||||||
|
pub p_data: *const ::std::os::raw::c_float,
|
||||||
|
pub channel_stride_or_data_size_in_bytes: ::std::os::raw::c_int,
|
||||||
|
pub p_metadata: *const ::std::os::raw::c_char,
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
#[repr(packed)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct NDIlib_compressed_packet_t {
|
||||||
|
pub version: u32,
|
||||||
|
pub fourcc: NDIlib_compressed_FourCC_type_e,
|
||||||
|
pub pts: i64,
|
||||||
|
pub dts: i64,
|
||||||
|
pub reserved: u64,
|
||||||
|
pub flags: u32,
|
||||||
|
pub data_size: u32,
|
||||||
|
pub extra_data_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_compressed_packet_flags_keyframe: u32 = 1;
|
||||||
|
|
||||||
|
#[cfg(feature = "advanced-sdk")]
|
||||||
|
pub const NDIlib_compressed_packet_version_0: u32 = 44;
|
1618
net/ndi/src/receiver.rs
Normal file
1618
net/ndi/src/receiver.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue