Initial commit

This commit is contained in:
Alex Andres 2021-03-18 21:53:30 +01:00
commit d2aa06efaa
No known key found for this signature in database
GPG key ID: 340764C7851D7041
2934 changed files with 324742 additions and 0 deletions

58
.azure/build-pipeline.yml Normal file
View file

@ -0,0 +1,58 @@
trigger:
batch: true
branches:
include:
- master
pr:
branches:
include:
- master
variables:
MAVEN_REPO_FOLDER: $(Pipeline.Workspace)/.m2/repository
strategy:
matrix:
linux:
imageName: "ubuntu-latest"
mac:
imageName: "macos-latest"
windows:
imageName: "windows-latest"
pool:
vmImage: $(imageName)
steps:
- task: DownloadSecureFile@1
displayName: "Download Maven settings"
name: mvnSettings
inputs:
secureFile: settings.xml
- task: Cache@2
displayName: "Set up Maven cache"
inputs:
key: 'maven | "$(Agent.OS)" | **/pom.xml, !**/target/**'
restoreKeys: |
maven | "$(Agent.OS)"
maven
path: $(MAVEN_REPO_FOLDER)
- task: Maven@3
displayName: "Build"
inputs:
jdkVersionOption: "1.11"
publishJUnitResults: false
goals: "package"
options: "-DskipTests -s $(mvnSettings.secureFilePath)"
- task: Maven@3
displayName: "Test"
inputs:
jdkVersionOption: "1.11"
mavenPomFile: 'pom.xml'
goals: "jar:jar surefire:test"
options: "-s $(mvnSettings.secureFilePath)"
testRunTitle: "$(Agent.OS) ($(Agent.OSArchitecture))"

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# Eclipse Core
**/.classpath
**/.project
**/.settings/
# IntelliJ IDEA
**/*.iml
**/.idea
# VS Code
.vscode
# Maven Build
**/target
# Node
node_modules/

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, 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
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If 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 convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state 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 program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

35
README.md Normal file
View file

@ -0,0 +1,35 @@
## lectureStudio
lectureStudio is a open and royalty-free software for e-teaching with support for Windows, Mac OS and Linux. It offers a multitude of possibilities: This starts with the presentation of PDF-based slide sets, including the possibility of pen annotations and digital whiteboards. These two features for use in interactive teaching scenarios (classroom, lecture hall) are enhanced by a quiz system with which surveys can be carried out very easily, in which the audience can participate directly by using own mobile devices in accordance with GDPR without the use of special apps or external servers. For remote teaching, recordings and bandwidth-saving live streaming are integrated directly into the software.
### Screenshots
#### lecturePresenter
![lecturePresenter Screenshot](doc/readme/lecturePresenter.png)
#### lectureEditor
![lectureEditor Screenshot](doc/readme/lectureEditor.png)
### Build Notes
In order to build the code, be sure to install the prerequisite software:
Please make sure you have installed and selected at least JDK 14 on your build system.
<table>
<tr>
<td>Windows</td>
<td><a href="https://wixtoolset.org">WiX toolset</a> and <a href="https://developer.microsoft.com/windows/downloads/windows-10-sdk">Windows 10 SDK</a> <b>(Only, if you want to build the MSI installer)</b></td>
</tr>
</table>
Assuming you have all the prerequisites installed for your OS, run:
```
mvn install
```
If you don't want to create the MSI installer on Windows, run:
```
mvn install -P !package:msi
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

202
lect-broadcast/pom.xml Normal file
View file

@ -0,0 +1,202 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.lecturestudio</groupId>
<artifactId>lect-studio</artifactId>
<version>4.0.0</version>
</parent>
<groupId>org.lecturestudio.broadcast</groupId>
<artifactId>lect-broadcast</artifactId>
<version>4.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<cxf.version>3.3.6</cxf.version>
<jackson.version>2.9.10</jackson.version>
<web.auth.version>1.0.0</web.auth.version>
<web.client.version>1.0.0</web.client.version>
<web.service.version>1.0.0</web.service.version>
<meecrowave.version>1.2.9</meecrowave.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<classifier>${envClassifier}</classifier>
<archive>
<manifest>
<mainClass>org.lecturestudio.broadcast.BroadcasterApplication</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<classpathLayoutType>custom</classpathLayoutType>
<customClasspathLayout>${artifact.artifactId}.${artifact.extension}</customClasspathLayout>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<excludeTypes>jar</excludeTypes>
<includeTypes>war</includeTypes>
<includeScope>provided</includeScope>
<outputDirectory>${project.build.outputDirectory}/resources/tomcat/apps</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<targetPath>resources</targetPath>
<filtering>false</filtering>
<excludes>
<exclude>log4j2.xml</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>log4j2.xml</include>
<include>*.war</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
<dependencies>
<dependency>
<groupId>org.lecturestudio.core</groupId>
<artifactId>lect-core</artifactId>
<version>4.0.0</version>
<exclusions>
<exclusion>
<artifactId>*</artifactId>
<groupId>*</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.lecturestudio.web.api</groupId>
<artifactId>lect-web-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<!-- Web applications -->
<dependency>
<groupId>org.lecturestudio.web.auth</groupId>
<artifactId>lect-web-auth</artifactId>
<version>${web.auth.version}</version>
<type>war</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.lecturestudio.web.service</groupId>
<artifactId>lect-web-service</artifactId>
<version>${web.service.version}</version>
<type>war</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.lecturestudio.web.http.client</groupId>
<artifactId>lect-web-http-client</artifactId>
<version>${web.client.version}</version>
<type>war</type>
<scope>provided</scope>
</dependency>
<!-- Shared web applications database driver -->
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- Meecrowave application server -->
<dependency>
<groupId>org.apache.meecrowave</groupId>
<artifactId>meecrowave-specs-api</artifactId>
<version>${meecrowave.version}</version>
</dependency>
<dependency>
<groupId>org.apache.meecrowave</groupId>
<artifactId>meecrowave-core</artifactId>
<version>${meecrowave.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.4</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-security-oauth2</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-security-sso-oidc</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,57 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast;
import org.lecturestudio.core.ExecutableBase;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.net.ApplicationServer;
import org.lecturestudio.broadcast.config.Configuration;
import org.lecturestudio.broadcast.server.MeecrowaveServer;
public class Broadcaster extends ExecutableBase {
private final Configuration config;
private ApplicationServer appServer;
public Broadcaster(Configuration config) {
this.config = config;
}
@Override
protected void initInternal() throws ExecutableException {
appServer = new MeecrowaveServer(config);
}
@Override
protected void startInternal() throws ExecutableException {
appServer.start();
}
@Override
protected void stopInternal() throws ExecutableException {
appServer.stop();
}
@Override
protected void destroyInternal() throws ExecutableException {
appServer.destroy();
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast;
import static java.util.Objects.nonNull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.stream.Stream;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lecturestudio.broadcast.config.Configuration;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.app.AppDataLocator;
import org.lecturestudio.core.app.ApplicationBase;
import org.lecturestudio.core.app.configuration.ConfigurationService;
import org.lecturestudio.core.app.configuration.YamlConfigurationService;
public class BroadcasterApplication extends ApplicationBase {
private static final Logger LOG = LogManager.getLogger(BroadcasterApplication.class);
private static final AppDataLocator LOCATOR = new AppDataLocator("lectureBroadcaster");
private static final File CONFIG_FILE = new File(LOCATOR.toAppDataPath("broadcaster.cfg"));
private static final Options OPTIONS = new Options();
private Broadcaster broadcaster;
static {
OPTIONS.addOption("help", false, "Print out a usage message");
OPTIONS.addOption("c", false, "Clear web server cache");
OPTIONS.addOption("p", true, "HTTP port");
OPTIONS.addOption("tls", true, "HTTP TLS port");
}
/**
* The entry point of the application. This method calls the static {@link
* #launch(String[])} method to fire up the application.
*
* @param args the main method's arguments.
*/
public static void main(String[] args) {
CommandLineParser parser = new DefaultParser();
CommandLine cmd;
try {
cmd = parser.parse(OPTIONS, args);
}
catch (ParseException e) {
System.err.println("Invalid arguments: " + Arrays.toString(args));
return;
}
if (cmd.hasOption("help")) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("lectureBroadcaster", OPTIONS);
return;
}
BroadcasterApplication.launch(args);
// Keep it running.
try {
System.in.read();
}
catch (IOException e) {
// Ignore.
}
}
@Override
protected void initInternal(String[] args) throws ExecutableException {
ConfigurationService<Configuration> configService = new YamlConfigurationService<>();
Configuration config;
File configFile = CONFIG_FILE;
if (!configFile.exists()) {
configFile = new File("/resources/broadcaster.cfg");
}
try {
config = configService.load(configFile, Configuration.class);
if (!CONFIG_FILE.exists()) {
File parent = CONFIG_FILE.getParentFile();
if (nonNull(parent)) {
parent.mkdirs();
// Write the default config to the file system.
configService.save(CONFIG_FILE, config);
}
}
}
catch (Exception e) {
throw new ExecutableException(e);
}
try {
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(OPTIONS, args);
if (cmd.hasOption("c")) {
Path basePath = Paths.get(LOCATOR.toAppDataPath(config.baseDir));
if (Files.exists(basePath)) {
try (Stream<Path> walk = Files.walk(basePath)) {
walk.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
}
if (cmd.hasOption("p")) {
config.port = Integer.parseInt(cmd.getOptionValue("p"));
}
if (cmd.hasOption("tls")) {
config.tlsPort = Integer.parseInt(cmd.getOptionValue("tls"));
}
}
catch (Exception e) {
LOG.error("Parse input options failed", e);
}
broadcaster = new Broadcaster(config);
broadcaster.init();
}
@Override
protected void startInternal() throws ExecutableException {
broadcaster.start();
}
@Override
protected void stopInternal() throws ExecutableException {
broadcaster.stop();
}
@Override
protected void destroyInternal() throws ExecutableException {
broadcaster.destroy();
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast.config;
import java.util.List;
import java.util.Map;
public class Configuration {
/**
* The keystore configuration in order to use TLS enabled streams.
*/
public KeystoreConfiguration keystoreConfig;
/**
* List of web-applications to start during the bootstrap process.
*/
public List<Map<String, String>> applications;
/**
* The application server's port number.
*/
public int port;
/**
* The application server's TLS port number.
*/
public int tlsPort;
/**
* Indicator whether to use TLS.
*/
public boolean tlsEnabled;
/**
* Indicator whether to redirect HTTP to HTTPS.
*/
public boolean redirectToHttps;
/**
* The application server's web-root directory.
*/
public String baseDir;
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast.config;
public class KeystoreConfiguration {
/**
* The alias used to for the server certificate in the keystore.
* If not specified, the first alias in the keystore will be used.
*/
public String keyAlias;
/**
* The pathname of the keystore file where the server certificate
* is stored. If undefined, the default path is '.keystore' in the
* users home directory who is running the server. If the specified
* keystore could not be loaded, the default packed keystore will
* be used.
*/
public String keystorePath;
/**
* The type of the keystore file where the server certificate is
* stored. The default value is 'JKS', if not specified.
*/
public String keystoreType;
/**
* The password used to access the server certificate from the
* specified keystore file.
*/
public String keystorePassword;
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast.log;
import org.lecturestudio.core.app.AppDataLocator;
import org.lecturestudio.core.log.Log4jXMLConfigurationFactory;
import org.apache.logging.log4j.core.config.Order;
import org.apache.logging.log4j.core.config.plugins.Plugin;
@Plugin(name = "Log4jConfigurationFactory", category = "ConfigurationFactory")
@Order(10)
public class Log4jConfigurationFactory extends Log4jXMLConfigurationFactory {
public Log4jConfigurationFactory() {
AppDataLocator dataLocator = new AppDataLocator("lectureBroadcaster");
System.setProperty("logFilePath", dataLocator.getAppDataPath());
}
}

View file

@ -0,0 +1,247 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast.server;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import org.lecturestudio.core.ExecutableBase;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.app.AppDataLocator;
import org.lecturestudio.core.io.ResourceLoader;
import org.lecturestudio.core.net.ApplicationServer;
import org.lecturestudio.core.util.DirUtils;
import org.lecturestudio.core.util.FileUtils;
import org.lecturestudio.broadcast.config.Configuration;
import org.lecturestudio.broadcast.servlet.ApplicationManagerServlet;
import org.lecturestudio.broadcast.servlet.ErrorReportValve;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import org.apache.meecrowave.Meecrowave;
public class MeecrowaveServer extends ExecutableBase implements ApplicationServer {
private static final AppDataLocator LOCATOR = new AppDataLocator("lectureBroadcaster");
private static final String TOMCAT_ROOT = "resources/tomcat";
private final Configuration config;
private Meecrowave meecrowave;
public MeecrowaveServer(Configuration config) {
this.config = config;
}
@Override
public void startWebApp(String contextPath, String appName) throws Exception {
if (isNull(meecrowave)) {
throw new Exception("Server has to be started first.");
}
File warFile = new File(toAppDataPath(config.baseDir) + "/apps/" + appName);
meecrowave.deployWebapp(contextPath, warFile);
}
@Override
public void stopWebApp(String contextPath) throws Exception {
if (isNull(meecrowave)) {
throw new Exception("Server has to be started first.");
}
contextPath = getContextPath(contextPath);
meecrowave.undeploy(contextPath);
}
@Override
protected void initInternal() throws ExecutableException {
try {
initWebRoot();
}
catch (Exception e) {
throw new ExecutableException("Initialize web-root failed.", e);
}
}
@Override
protected void startInternal() throws ExecutableException {
String keyAlias = config.keystoreConfig.keyAlias;
String keystorePassword = config.keystoreConfig.keystorePassword;
String keystorePath = config.keystoreConfig.keystorePath;
String keystoreType = config.keystoreConfig.keystoreType;
Meecrowave.Builder builder = new Meecrowave.Builder();
builder.setDir(toAppDataPath(config.baseDir));
builder.setHttpPort(config.port);
builder.includePackages("org.lecturestudio.web");
if (config.tlsEnabled) {
Connector httpsConnector = new Connector();
httpsConnector.setPort(config.tlsPort);
httpsConnector.setSecure(true);
httpsConnector.setScheme("https");
httpsConnector.setAttribute("sslProtocol", "TLS");
httpsConnector.setAttribute("SSLEnabled", true);
httpsConnector.setAttribute("keyAlias", keyAlias);
httpsConnector.setAttribute("keystorePass", keystorePassword);
httpsConnector.setAttribute("keystoreFile", keystorePath);
httpsConnector.setAttribute("keystoreType", keystoreType);
builder.getConnectors().add(httpsConnector);
}
try {
meecrowave = new Meecrowave(builder);
meecrowave.bake();
setErrorReportValve();
startApplications();
startApplicationManager();
}
catch (Exception e) {
throw new ExecutableException(e);
}
}
@Override
protected void stopInternal() throws ExecutableException {
if (nonNull(meecrowave)) {
try {
meecrowave.close();
meecrowave = null;
}
catch (Exception e) {
throw new ExecutableException(e);
}
}
}
@Override
protected void destroyInternal() {
}
private void setErrorReportValve() throws LifecycleException, IOException {
StandardHost host = (StandardHost) meecrowave.getTomcat().getHost();
String errorValve = host.getErrorReportValveClass();
// Remove default ErrorReportValve.
for (Valve valve : host.getPipeline().getValves()) {
if (errorValve.equals(valve.getClass().getName())) {
host.getPipeline().removeValve(valve);
break;
}
}
File errorFile = new File(toAppDataPath(config.baseDir) + "/static/error.html");
String contents = new String(Files.readAllBytes(Paths.get(errorFile.getAbsolutePath())));
ErrorReportValve errorReportValve = new ErrorReportValve();
errorReportValve.setErrorTemplate(contents);
host.getPipeline().addValve(errorReportValve);
host.setErrorReportValveClass(errorReportValve.getClass().getName());
// Restart host.
host.stop();
host.start();
}
private void startApplications() throws Exception {
for (Map<String, String> serviceMap : config.applications) {
String path = serviceMap.get("path");
String name = serviceMap.get("name");
startWebApp(path, name);
}
}
private void startApplicationManager() {
String appManagerName = ApplicationManagerServlet.class.getName();
Context ctx = meecrowave.getTomcat().addContext("/server", null);
Tomcat.addServlet(ctx, appManagerName, new ApplicationManagerServlet(this));
ctx.addServletMappingDecoded("/manager", appManagerName);
}
private static String getContextPath(String contextPath) {
if (contextPath.equals("/")) {
contextPath = "";
}
return contextPath;
}
private void initWebRoot() throws Exception {
String baseDir = toAppDataPath(config.baseDir);
File webRoot = new File(baseDir);
if (!webRoot.exists()) {
webRoot.mkdirs();
copyResourceToFilesystem(TOMCAT_ROOT, webRoot.getAbsolutePath());
}
System.setProperty("catalina.base", webRoot.getAbsolutePath());
System.setProperty("catalina.home", webRoot.getAbsolutePath());
}
private void copyResourceToFilesystem(String resName, String baseDir) throws Exception {
URL resURL = ResourceLoader.getResourceURL(resName);
if (ResourceLoader.isJarResource(resURL)) {
String jarPath = ResourceLoader.getJarPath(this.getClass());
FileUtils.copyJarResource(jarPath, resName, baseDir);
}
else {
File resFile = new File(resURL.getPath());
Path sourcePath = resFile.toPath();
if (resFile.isFile()) {
Path targetPath = Paths.get(baseDir, resFile.getName());
Files.copy(sourcePath, targetPath);
}
else if (resFile.isDirectory()) {
Path targetPath = Paths.get(baseDir);
DirUtils.copy(sourcePath, targetPath);
}
}
}
private String toAppDataPath(String path) {
return LOCATOR.toAppDataPath(path);
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast.servlet;
import static java.util.Objects.isNull;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.lecturestudio.core.net.ApplicationServer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ApplicationManagerServlet extends HttpServlet {
private static final Logger LOG = LogManager.getLogger(ApplicationManagerServlet.class);
private final ApplicationServer applicationServer;
public ApplicationManagerServlet(ApplicationServer applicationServer) {
this.applicationServer = applicationServer;
}
@Override
public void init() {
LOG.debug("Initialize " + getClass().getSimpleName());
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String host = req.getRemoteHost();
if (!host.equals("127.0.0.1")) {
resp.sendError(403);
return;
}
String opcode = req.getParameter("opcode");
if (isInvalid(opcode)) {
resp.sendError(400);
return;
}
try {
switch (opcode) {
case "start":
start(req);
break;
case "stop":
stop(req);
break;
default:
break;
}
}
catch (Exception e) {
LOG.error("Manage application failed.", e);
resp.sendError(400);
return;
}
resp.sendError(200);
}
private void start(HttpServletRequest req) throws Exception {
String contextPath = req.getParameter("contextPath");
String appName = req.getParameter("appName");
LOG.debug("Starting application {} on {}", appName, contextPath);
if (isInvalid(contextPath) || isInvalid(appName)) {
throw new IllegalArgumentException("Missing or invalid parameters passed.");
}
applicationServer.startWebApp(contextPath, appName);
}
private void stop(HttpServletRequest req) throws Exception {
String contextPath = req.getParameter("contextPath");
LOG.debug("Stopping application on {}", contextPath);
if (isInvalid(contextPath)) {
throw new IllegalArgumentException("Missing or invalid parameters passed.");
}
applicationServer.stopWebApp(contextPath);
}
private boolean isInvalid(String param) {
return isNull(param) || param.isEmpty();
}
}

View file

@ -0,0 +1,222 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast.servlet;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.Constants;
import org.apache.catalina.valves.ValveBase;
import org.apache.commons.text.StringSubstitutor;
import org.apache.coyote.ActionCode;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.res.StringManager;
/**
* Modified and simplified version of the default Tomcat ErrorReportValve.
* This ErrorReportValve is able to load and return an HTML template file.
*
* @author Alex Andres
*/
public class ErrorReportValve extends ValveBase {
private String errorTemplate;
public ErrorReportValve() {
super(true);
}
public void setErrorTemplate(String template) {
this.errorTemplate = template;
}
/**
* Invoke the next Valve in the sequence. When the invoke returns, check
* the response state. If the status code is greater than or equal to 400
* or an uncaught exception was thrown then the error handling will be
* triggered.
*
* @param request The servlet request to be processed
* @param response The servlet response to be created
*
* @throws IOException if an input/output error occurs
* @throws ServletException if a servlet error occurs
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// Perform the request
getNext().invoke(request, response);
if (response.isCommitted()) {
if (response.setErrorReported()) {
// Error wasn't previously reported but we can't write an error
// page because the response has already been committed. Attempt
// to flush any data that is still to be written to the client.
try {
response.flushBuffer();
}
catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
// Close immediately to signal to the client that something went wrong.
response.getCoyoteResponse().action(ActionCode.CLOSE_NOW, null);
}
return;
}
Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// If an async request is in progress and is not going to end once this
// container thread finishes, do not process any error page here.
if (request.isAsync() && !request.isAsyncCompleting()) {
return;
}
if (throwable != null && !response.isError()) {
// Make sure that the necessary methods have been called on the
// response. (It is possible a component may just have set the
// Throwable. Tomcat won't do that but other components might.)
// These are safe to call at this point as we know that the response
// has not been committed.
response.reset();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
// One way or another, response.sendError() will have been called before
// execution reaches this point and suspended the response. Need to
// reverse that so this valve can write to the response.
response.setSuspended(false);
try {
report(request, response, throwable);
}
catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
}
/**
* Prints out an error report.
*
* @param request The request being processed
* @param response The response being generated
* @param throwable The exception that occurred (which possibly wraps
* a root cause exception
*/
private void report(Request request, Response response, Throwable throwable) {
int statusCode = response.getStatus();
// Do nothing on a 1xx, 2xx and 3xx status
// Do nothing if anything has been written already
// Do nothing if the response hasn't been explicitly marked as in error
// and that error has not been reported.
if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
return;
}
// If an error has occurred that prevents further I/O, don't waste time
// producing an error report that will never be read.
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (!result.get()) {
return;
}
// Do nothing if there is no reason phrase for the specified status code and
// no error message provided
String reason = null;
StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales());
response.setLocale(smClient.getLocale());
try {
reason = smClient.getString("http." + statusCode + ".reason");
}
catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
if (reason == null) {
reason = smClient.getString("errorReportValve.unknownReason");
}
StringBuilder sb = new StringBuilder();
if (errorTemplate != null) {
Map<String, String> valuesMap = new HashMap<>();
valuesMap.put("statusCode", String.valueOf(statusCode));
valuesMap.put("reason", reason);
sb.append(StringSubstitutor.replace(errorTemplate, valuesMap));
}
else {
String message = String.valueOf(statusCode) + " - " + reason;
sb.append("<!doctype html>");
sb.append("<head>");
sb.append("<title>");
sb.append(reason);
sb.append("</title>");
sb.append("</head>");
sb.append("<body>");
sb.append("<h1>");
sb.append(message);
sb.append("</h1>");
sb.append("</body>");
sb.append("</html>");
}
try {
try {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
}
catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug("status.setContentType", t);
}
}
Writer writer = response.getReporter();
if (writer != null) {
// If writer is null, it's an indication that the response has
// been hard committed already, which should never happen
writer.write(sb.toString());
response.finishResponse();
}
}
catch (Exception e) {
// Ignore
}
}
}

View file

@ -0,0 +1,18 @@
---
keystoreConfig:
keyAlias: tomcat
keystorePath: conf/app-server.ks
keystoreType: PKCS12
keystorePassword: tomcat.ks.password
port: 80
tlsPort: 443
tlsEnabled: true
redirectToHttps: false
baseDir: app-server
applications:
- name: lect-web-auth.war
path: /bcast/auth
- name: lect-web-service.war
path: /bcast/ws
- name: lect-web-http-client.war
path: /

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<Property name="filename">broadcaster</Property>
</Properties>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5p [%25.25c{1}] %m%n" />
</Console>
<RollingFile
name="JavaLookup"
fileName="${sys:logFilePath}/${filename}.log"
filePattern="${sys:logFilePath}/${filename}-%d{MM-dd-yyyy}-%i.log">
<PatternLayout header="# ${java:runtime} %n# ${java:vm} %n# ${java:os} %n">
<Pattern>%d %m%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
<RollingFile
name="FILE"
fileName="${sys:logFilePath}/${filename}.log"
filePattern="${sys:logFilePath}/${filename}-%d{MM-dd-yyyy}-%i.log"
immediateFlush="true">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5p [%25.25c{1}] %m%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.lecturestudio" level="error" />
<Logger name="org.apache.openjpa" level="error" />
<Root level="info">
<AppenderRef ref="STDOUT" />
<AppenderRef ref="FILE" />
</Root>
</Loggers>
</Configuration>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<JarScanner scanBootstrapClassPath="false" scanClassPath="false" scanManifest="false">
<JarScanFilter defaultPluggabilityScan="false" defaultTldScan="false" pluggabilityScan="" tldScan=""/>
</JarScanner>
</Context>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>${reason}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { background-color: #dfe3e5; color: #002538; text-transform: uppercase; font-size: 1.25em; font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif; width: 100%; height: 100%; }
.container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; width: 100%; }
.reason { background: #002538; color: #fff; display: inline-block; font-size: 1.25em; border-radius: 0.4em; padding: 0.5rem 1rem; }
.statusCode { text-decoration: underline dotted; }
</style>
</head>
<body>
<div class="container">
<div class="reason">${reason}</div>
<div class="statusCode">${statusCode}</div>
</div>
</body>
</html>

View file

@ -0,0 +1,107 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.broadcast;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.lecturestudio.web.api.filter.IpRangeRule;
import org.lecturestudio.web.api.filter.RegexRule;
import org.lecturestudio.web.api.model.Classroom;
import org.lecturestudio.web.api.model.ClassroomDocument;
import org.lecturestudio.web.api.model.MessageService;
import org.lecturestudio.web.api.model.QuizService;
import org.lecturestudio.web.api.model.quiz.Quiz;
import org.lecturestudio.web.api.ws.ClassroomServiceClient;
import org.lecturestudio.web.api.ws.ConnectionParameters;
import org.lecturestudio.web.api.ws.MessageServiceClient;
import org.lecturestudio.web.api.ws.QuizServiceClient;
import org.lecturestudio.web.api.ws.rs.ClassroomRestClient;
import org.lecturestudio.web.api.ws.rs.MessageRestClient;
import org.lecturestudio.web.api.ws.rs.QuizRestClient;
public class ClassroomTest {
public static void main(final String[] args) throws Exception {
run();
}
public static void run() throws Exception {
ConnectionParameters parameters = new ConnectionParameters("127.0.0.1", 80, false);
ClassroomTest test = new ClassroomTest();
test.testMessageService(parameters);
//test.testQuizService(parameters);
test.testClassroomService(parameters);
}
private void testClassroomService(ConnectionParameters parameters) throws Exception {
ClassroomServiceClient serviceClient = new ClassroomRestClient(parameters);
System.out.println(serviceClient.getClassrooms());
}
private void testMessageService(ConnectionParameters parameters) throws Exception {
Classroom classroom = createClassroom();
MessageService messageService = new MessageService();
MessageServiceClient serviceClient = new MessageRestClient(parameters);
System.out.println(serviceClient.startService(classroom, messageService));
}
private void testQuizService(ConnectionParameters parameters) throws Exception {
Classroom classroom = createClassroom();
List<RegexRule> regexRules = new ArrayList<>();
regexRules.add(new RegexRule("^23"));
regexRules.add(new RegexRule("^42"));
regexRules.add(new RegexRule("^666"));
Quiz quiz = new Quiz();
quiz.setType(Quiz.QuizType.MULTIPLE);
quiz.setQuestion("What's wrong?");
quiz.addOption("nothing");
quiz.addOption("everything");
quiz.addOption("something");
QuizService quizService = new QuizService();
quizService.setQuiz(quiz);
quizService.setRegexRules(regexRules);
QuizServiceClient serviceClient = new QuizRestClient(parameters);
System.out.println(serviceClient.startService(classroom, quizService));
}
private Classroom createClassroom() {
List<IpRangeRule> ipRules = new ArrayList<>();
ipRules.add(new IpRangeRule("192.168.0.0", "192.168.0.255"));
ipRules.add(new IpRangeRule("192.168.2.0", "192.168.2.255"));
ipRules.add(new IpRangeRule("127.0.0.0", "127.0.0.255"));
Classroom classroom = new Classroom("Test Classroom", "");
classroom.setLocale(Locale.GERMANY);
classroom.setIpFilterRules(ipRules);
classroom.getDocuments().add(new ClassroomDocument("hello.pdf"));
return classroom;
}
}

Binary file not shown.

Binary file not shown.

135
lect-core/pom.xml Normal file
View file

@ -0,0 +1,135 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.lecturestudio</groupId>
<artifactId>lect-studio</artifactId>
<version>4.0.0</version>
</parent>
<groupId>org.lecturestudio.core</groupId>
<artifactId>lect-core</artifactId>
<version>4.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.9.10</jackson.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<targetPath>resources</targetPath>
<filtering>false</filtering>
<excludes>
<exclude>**/META-INF/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<targetPath>.</targetPath>
<filtering>false</filtering>
<includes>
<include>**/META-INF/**</include>
</includes>
</resource>
</resources>
</build>
<dependencies>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>5.0.0-BETA-1</version>
</dependency>
<dependency>
<groupId>org.knowm.xchart</groupId>
<artifactId>xchart</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.21</version>
</dependency>
<dependency>
<groupId>com.artifex.mupdf</groupId>
<artifactId>mupdf</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>com.github.javaffmpeg</groupId>
<artifactId>JavaFFmpeg</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>org.scilab.forge</groupId>
<artifactId>jlatexmath</artifactId>
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>com.github.wendykierp</groupId>
<artifactId>JTransforms</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.10.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.66</version>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,72 @@
module org.lecturestudio.core {
uses org.lecturestudio.core.audio.codec.AudioCodecProvider;
requires com.artifex.mupdf;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.dataformat.yaml;
requires com.google.common;
requires com.google.guice;
requires commons.math3;
requires java.desktop;
requires java.logging;
requires javax.inject;
requires jlatexmath;
requires JTransforms;
requires org.apache.logging.log4j;
requires org.apache.logging.log4j.core;
requires org.apache.pdfbox;
requires org.apache.fontbox;
requires org.bouncycastle.provider;
requires org.bouncycastle.pkix;
requires org.knowm.xchart;
requires org.lecturestudio.javaffmpeg;
exports org.lecturestudio.core;
exports org.lecturestudio.core.app;
exports org.lecturestudio.core.app.configuration;
exports org.lecturestudio.core.app.configuration.bind;
exports org.lecturestudio.core.app.dictionary;
exports org.lecturestudio.core.audio;
exports org.lecturestudio.core.audio.bus;
exports org.lecturestudio.core.audio.bus.event;
exports org.lecturestudio.core.audio.codec;
exports org.lecturestudio.core.audio.device;
exports org.lecturestudio.core.audio.sink;
exports org.lecturestudio.core.audio.source;
exports org.lecturestudio.core.beans;
exports org.lecturestudio.core.bus;
exports org.lecturestudio.core.bus.event;
exports org.lecturestudio.core.camera;
exports org.lecturestudio.core.camera.bus.event;
exports org.lecturestudio.core.codec;
exports org.lecturestudio.core.codec.h264;
exports org.lecturestudio.core.converter;
exports org.lecturestudio.core.geometry;
exports org.lecturestudio.core.graphics;
exports org.lecturestudio.core.inject;
exports org.lecturestudio.core.input;
exports org.lecturestudio.core.io;
exports org.lecturestudio.core.model;
exports org.lecturestudio.core.model.action;
exports org.lecturestudio.core.model.listener;
exports org.lecturestudio.core.model.shape;
exports org.lecturestudio.core.net;
exports org.lecturestudio.core.net.protocol;
exports org.lecturestudio.core.net.rtp;
exports org.lecturestudio.core.pdf;
exports org.lecturestudio.core.presenter;
exports org.lecturestudio.core.recording;
exports org.lecturestudio.core.recording.action;
exports org.lecturestudio.core.recording.edit;
exports org.lecturestudio.core.recording.file;
exports org.lecturestudio.core.render;
exports org.lecturestudio.core.service;
exports org.lecturestudio.core.text;
exports org.lecturestudio.core.tool;
exports org.lecturestudio.core.util;
exports org.lecturestudio.core.view;
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
/**
* Common interface to provide a consistent mechanism for executable components
* managed by life cycle methods. The current state can be monitored by
* observing the {@link ExecutableState}. The state will change to the error
* state if the attempted transition is not valid.
*
* @author Alex Andres
*/
public interface Executable {
/**
* Prepare the executable component for starting. This method should perform
* any initialization required post object creation.
*
* @throws ExecutableException if this component detects a fatal error that
* prevents this component from being used.
*/
void init() throws ExecutableException;
/**
* Prepare for the beginning of active use of this executable component.
*
* @throws ExecutableException if this component detects a fatal error that
* prevents this component from being used.
*/
void start() throws ExecutableException;
/**
* Stops this executable component.
*
* @throws ExecutableException if this component detects a fatal error that
* needs to be reported.
*/
void stop() throws ExecutableException;
/**
* Suspends this executable component.
*
* @throws ExecutableException if this component detects a fatal error that
* needs to be reported.
*/
void suspend() throws ExecutableException;
/**
* Dispose this executable component.
*
* @throws ExecutableException if this component detects a fatal error that
* prevents this component from being destroyed.
*/
void destroy() throws ExecutableException;
/**
* Obtain the current state of this executable component.
*
* @return The current state of this component.
*/
ExecutableState getState();
}

View file

@ -0,0 +1,387 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
import static java.util.Objects.isNull;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Sub-classes may extend this executable base class by only implementing the
* internal life cycle methods. This base implementation of the {@link
* Executable} interface handles the proper state transition rules for the life
* cycle methods.
*
* @author Alex Andres
*/
public abstract class ExecutableBase implements Executable {
private static final Logger LOG = LogManager.getLogger(ExecutableBase.class);
/** The list of registered state listeners for event notifications. */
private final List<ExecutableStateListener> stateListeners = new CopyOnWriteArrayList<>();
/** The current state of this component. */
private volatile ExecutableState state = ExecutableState.Created;
/** The previous state of this component. */
private volatile ExecutableState prevState = ExecutableState.Created;
/**
* Add a ExecutableStateListener listener to this component.
*
* @param listener The listener to add.
*/
public void addStateListener(ExecutableStateListener listener) {
stateListeners.add(listener);
}
/**
* Remove a ExecutableStateListener listener from this component.
*
* @param listener The listener to remove.
*/
public void removeStateListener(ExecutableStateListener listener) {
stateListeners.remove(listener);
}
@Override
public final synchronized void init() throws ExecutableException {
setState(ExecutableState.Initializing);
try {
initInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to initialize Executable component [%s]", e, this);
}
setState(ExecutableState.Initialized);
}
/**
* @throws ExecutableException if the sub-class fails to initialize this
* component.
*/
protected abstract void initInternal() throws ExecutableException;
@Override
public final synchronized void start() throws ExecutableException {
if (created() || destroyed()) {
init();
}
setState(ExecutableState.Starting);
try {
startInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to start Executable component [%s]", e, this);
}
setState(ExecutableState.Started);
}
/**
* @throws ExecutableException if the sub-class fails to start this
* component.
*/
protected abstract void startInternal() throws ExecutableException;
@Override
public final synchronized void stop() throws ExecutableException {
setState(ExecutableState.Stopping);
try {
stopInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to stop Executable component [%s]", e, this);
}
setState(ExecutableState.Stopped);
}
/**
* @throws ExecutableException if the sub-class fails to stop this component.
*/
protected abstract void stopInternal() throws ExecutableException;
@Override
public final synchronized void suspend() throws ExecutableException {
setState(ExecutableState.Suspending);
try {
suspendInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to suspend Executable component [%s]", e, this);
}
setState(ExecutableState.Suspended);
}
/**
* This method is meant to be overridden by sub-classes in order to implement
* a custom suspend routine, if required.
*
* @throws ExecutableException if the sub-class fails to stop this component.
*/
protected void suspendInternal() throws ExecutableException {
}
@Override
public final synchronized void destroy() throws ExecutableException {
if (started() || suspended()) {
stop();
}
setState(ExecutableState.Destroying);
try {
destroyInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to destroy Executable component [%s]", e, this);
}
setState(ExecutableState.Destroyed);
}
/**
* @throws ExecutableException if the sub-class fails to destroy this
* component.
*/
protected abstract void destroyInternal() throws ExecutableException;
@Override
public ExecutableState getState() {
return state;
}
/**
* Obtain the previous state of this component.
*
* @return The previous state of this component.
*/
public ExecutableState getPreviousState() {
return prevState;
}
/**
* Indicates whether this component has been created.
*
* @return true if this component has been created.
*/
public final boolean created() {
return state == ExecutableState.Created;
}
/**
* Indicates whether this component has been initialized.
*
* @return true if this component has been initialized.
*/
public final boolean initialized() {
return state == ExecutableState.Initialized;
}
/**
* Indicates whether this component has been started.
*
* @return true if this component has been started.
*/
public final boolean started() {
return state == ExecutableState.Started;
}
/**
* Indicates whether this component has been stopped.
*
* @return true if this component has been stopped.
*/
public final boolean stopped() {
return state == ExecutableState.Stopped;
}
/**
* Indicates whether this component has been suspended.
*
* @return true if this component has been suspended.
*/
public final boolean suspended() {
return state == ExecutableState.Suspended;
}
/**
* Indicates whether this component has been destroyed.
*
* @return true if this component has been destroyed.
*/
public final boolean destroyed() {
return state == ExecutableState.Destroyed;
}
/**
* Indicates whether an error has occurred during a state transition of this
* component.
*
* @return true if an error has occurred.
*/
public final boolean error() {
return state == ExecutableState.Error;
}
/**
* Update the component state if, and only if, the attempted state transition
* is valid.
*
* @param state The new state for this component.
*
* @exception ExecutableException if the state transition fails.
*/
protected final synchronized void setState(ExecutableState state) throws ExecutableException {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting state for [{}] to [{}]", this, state);
}
if (!validateNextState(state)) {
throw new ExecutableException("Invalid state transition for Executable component [%s] in state [%s] to [%s]",
this, getState(), state);
}
this.prevState = this.state;
this.state = state;
fireStateChanged();
}
/**
* Notify state listeners about the new state. This method can be overridden
* by sub-classes in order to use a custom state notification.
*/
protected void fireStateChanged() {
for (ExecutableStateListener listener : stateListeners) {
listener.onExecutableStateChange(prevState, state);
}
}
private boolean validateNextState(ExecutableState nextState) {
switch (this.state) {
case Created:
return isAllowed(nextState,
ExecutableState.Initializing,
ExecutableState.Destroying);
case Initializing:
return isAllowed(nextState,
ExecutableState.Initialized,
ExecutableState.Error);
case Initialized:
return isAllowed(nextState,
ExecutableState.Starting,
ExecutableState.Destroying);
case Starting:
return isAllowed(nextState,
ExecutableState.Started,
ExecutableState.Error);
case Started:
return isAllowed(nextState,
ExecutableState.Suspending,
ExecutableState.Stopping,
ExecutableState.Destroying,
ExecutableState.Error);
case Stopping:
return isAllowed(nextState,
ExecutableState.Stopped,
ExecutableState.Error);
case Stopped:
return isAllowed(nextState,
ExecutableState.Starting,
ExecutableState.Destroying);
case Suspending:
return isAllowed(nextState,
ExecutableState.Suspended,
ExecutableState.Error);
case Suspended:
return isAllowed(nextState,
ExecutableState.Starting,
ExecutableState.Stopping,
ExecutableState.Destroying);
case Destroying:
return isAllowed(nextState,
ExecutableState.Destroyed,
ExecutableState.Error);
case Destroyed:
return isAllowed(nextState,
ExecutableState.Initializing);
case Error:
// Allow to recover from previous operation failure.
return isAllowed(nextState,
ExecutableState.Starting,
ExecutableState.Stopping,
ExecutableState.Destroying);
default:
return false;
}
}
private boolean isAllowed(ExecutableState nextState, ExecutableState... allowedStates) {
if (isNull(allowedStates)) {
throw new NullPointerException("No allowed states provided.");
}
for (ExecutableState allowedState : allowedStates) {
if (nextState == allowedState) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
/**
* This exception is thrown to indicate a problem while operating an executable
* component which implements the {@link Executable} interface. Throwing this
* exception should be considered fatal to the operation of the application
* containing this component.
*
* @author Alex Andres
*/
public class ExecutableException extends Exception {
private static final long serialVersionUID = 5074925163804690325L;
/**
* Construct a new ExecutableException with no other information.
*/
public ExecutableException() {
super();
}
/**
* Construct a new ExecutableException with the specified message.
*
* @param message A Message describing this exception.
*/
public ExecutableException(String message) {
super(message);
}
/**
* Construct a new ExecutableException with the specified formatted
* message.
*
* @param message A Message describing this exception.
* @param args Arguments used in the formatted message.
*/
public ExecutableException(String message, Object... args) {
super(String.format(message, args));
}
/**
* Construct a new ExecutableException with the specified throwable.
*
* @param throwable A Throwable that caused this exception.
*/
public ExecutableException(Throwable throwable) {
super(throwable);
}
/**
* Construct a new ExecutableException with the specified message and
* throwable.
*
* @param message A Message describing this exception.
* @param throwable A Throwable that caused this exception.
*/
public ExecutableException(String message, Throwable throwable) {
super(message, throwable);
}
/**
* Construct a new ExecutableException with the specified formatted message
* and provided throwable.
*
* @param message A Message describing this exception.
* @param throwable A Throwable that caused this exception.
* @param args Arguments used in the formatted message.
*/
public ExecutableException(String message, Throwable throwable, Object... args) {
super(String.format(message, args), throwable);
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
/**
* Valid states for executable components that implement the {@link Executable}
* interface.
*
* @author Alex Andres
*/
public enum ExecutableState {
/** The component has been created but not initialized yet. */
Created,
/** The component is being initialized. */
Initializing,
/** The component has been successfully initialized. */
Initialized,
/** The component is being started. */
Starting,
/** The component has been successfully started. */
Started,
/** The component is being stopped. */
Stopping,
/** The component has been successfully stopped. */
Stopped,
/** The component is being suspended. */
Suspending,
/** The component has been successfully suspended. */
Suspended,
/** The component is being destroyed. */
Destroying,
/** The component has been successfully destroyed. */
Destroyed,
/** An fatal error has occurred during a state transition. */
Error
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
/**
* A listener for executable components implementing the {@link ExecutableBase}
* class. The listener will be notified after a state transition has taken
* place.
*
* @author Alex Andres
*/
@FunctionalInterface
public interface ExecutableStateListener {
/**
* Receive the state transition event.
*
* @param oldState The previous state of the component.
* @param newState The new state of the component.
*/
void onExecutableStateChange(ExecutableState oldState, ExecutableState newState);
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
import org.lecturestudio.core.geometry.Dimension2D;
/**
* A helper class to compute and keep the aspect ratio of an rectangle. This
* class is meant to keep the aspect ratio of slides that are rendered at
* different sizes.
*
* @author Alex Andres
*/
public class PageMetrics {
/** The width of the rectangle. */
private final double m_width;
/** The height of the rectangle. */
private final double m_height;
/** The initial aspect ratio of the rectangle. */
private final double m_ratio;
/**
* Create a new PageMetrics instance with the given width and height.
*
* @param width The initial width of the rectangle.
* @param height The initial height of the rectangle.
*/
public PageMetrics(double width, double height) {
m_width = width;
m_height = height;
m_ratio = width / height;
}
/**
* Obtain the initial width.
*
* @return the initial width.
*/
public double getWidth() {
return m_width;
}
/**
* Obtain the new width of the rectangle by keeping the aspect ratio
* according to the specified height.
*
* @param height The height of the rectangle.
*
* @return the width of the rectangle in relation to the height.
*/
public double getWidth(double height) {
return Math.round((height / m_height) * m_width);
}
/**
* Obtain the initial height.
*
* @return the initial height.
*/
public double getHeight() {
return m_height;
}
/**
* Obtain the new height of the rectangle by keeping the aspect ratio
* according to the specified width.
*
* @param width The width of the rectangle.
*
* @return the height of the rectangle in relation to the width.
*/
public double getHeight(double width) {
return (width / m_width) * m_height;
}
/**
* Obtain the initial ratio.
*
* @return the initial ratio.
*/
public double getRatio() {
return m_ratio;
}
/**
* Convert the rectangle defined by the provided width and height to a
* rectangle that has the aspect ratio of this page metrics.
*
* @param width The width of the rectangle to convert.
* @param height The height of the rectangle to convert.
*
* @return the converted rectangle having the aspect ratio of this page
* metrics.
*/
public Dimension2D convert(double width, double height) {
double ratio = width / height;
if (ratio > m_ratio) {
width = getWidth(height);
}
else {
height = getHeight(width);
}
return new Dimension2D(width, height);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core;
import org.lecturestudio.core.bus.event.ProgressEvent;
/**
* A listener to monitor the progress of an running process.
*
* @author Alex Adnres
*/
@FunctionalInterface
public interface ProgressListener {
/**
* Called when progress of an individual running process changes.
*
* @param event The current progress event.
*/
void onProgress(ProgressEvent event);
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import static java.util.Objects.nonNull;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.lecturestudio.core.util.OsInfo;
/**
* The application data locator translates the paths of application related
* files to the corresponding application data folder.
*
* @author Alex Andres
*/
public class AppDataLocator {
/** The application name. */
private final String appName;
/**
* Create a new AppDataLocator with the specified application name and main
* application class.
*
* @param appName The application name.
*/
public AppDataLocator(String appName) {
this.appName = appName;
}
/**
* Obtain the application data folder path.
*
* @return the application data folder path.
*/
public String getAppDataPath() {
String userHome = System.getProperty("user.home");
String path = "";
Path appPath = null;
if (nonNull(appName)) {
path = appName;
}
if (OsInfo.isLinux()) {
appPath = Paths.get(userHome, ".config");
}
else if (OsInfo.isMac()) {
appPath = Paths.get(userHome, "Library", "Application Support");
}
else if (OsInfo.isWindows()) {
appPath = Paths.get(userHome, "AppData", "Local");
}
if (nonNull(appPath)) {
path = appPath.resolve(path).toString();
}
return path;
}
/**
* Translate the specified sub-path to the complete application data folder
* path. Once the complete path has been resolved, the path will end with
* the specified sub-path.
*
* @param subPath The sub-path in the application data folder.
*
* @return the resolved application data folder path.
*/
public String toAppDataPath(String subPath) {
return getAppDataPath() + File.separator + subPath;
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.ExecutableState;
/**
* Sub-classes may implement this interface to provide a consistent mechanism to
* start and stop the application.
*
* @author Alex Andres
*/
public interface Application {
/**
* Prepare the application for starting. This method should perform any
* initialization required post object creation, optionally using the
* arguments provided by the {@code #main(String[])} method.
*
* @param args the main method's arguments.
*
* @throws ExecutableException If this application detects a fatal error
* that prevents this application from being
* used.
*/
void init(final String[] args) throws ExecutableException;
/**
* Responsible for starting the application; e.g. for creating and showing
* the initial UI.
*
* @throws ExecutableException If this application detects a fatal error
* that prevents this application from being
* used.
*/
void start() throws ExecutableException;
/**
* Prepare the application to shut down. Subclasses may override this method
* to do any cleanup that is necessary before exiting.
*
* @throws ExecutableException If this application detects a fatal error
* that needs to be reported.
*/
void stop() throws ExecutableException;
/**
* Cleanup resources used by this application.
*
* @throws ExecutableException If this application detects a fatal error
* that prevents this application from being
* destroyed.
*/
void destroy() throws ExecutableException;
/**
* Registers a ApplicationStateListener on this application.
*
* @param listener the state listener to be registered.
*/
void addStateListener(ApplicationStateListener listener);
/**
* Removes a ApplicationStateListener from this application.
*
* @param listener the state listener to be removed.
*/
void removeStateListener(ApplicationStateListener listener);
/**
* Obtain the current state of this application.
*
* @return The current state of this application.
*/
ExecutableState getState();
}

View file

@ -0,0 +1,466 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.ExecutableState;
/**
* Base Application implementation that manages the application life cycle
* methods. Sub-classes may extend this base class by only implementing the
* internal life cycle methods. This base implementation of the {@link
* Application} interface handles the proper state transition rules.
*
* @author Alex Andres
*/
public abstract class ApplicationBase implements Application {
static {
try (InputStream stream = ApplicationBase.class.getResourceAsStream("/log.properties")) {
if (nonNull(stream)) {
java.util.logging.LogManager.getLogManager().readConfiguration(stream);
}
}
catch (IOException e) {
// Ignore
}
}
private static final Logger LOG = LogManager.getLogger(ApplicationBase.class);
protected static final List<File> OPEN_FILES = new ArrayList<>();
/**
* The handler is notified when the application is asked to open a list of
* files.
*/
protected static Consumer<List<File>> openFilesHandler;
/** The list of all registered state listeners. */
private final List<ApplicationStateListener> stateListeners = new ArrayList<>();
/** The current state of the application. */
private ExecutableState state = ExecutableState.Created;
/**
* Not to be called directly.
* <p>
* Subclasses can provide a no-args constructor to initialize the private
* final state. Anything else that might refer to public API, should be done
* in the {@link #init(String[])} and {@link #start()} method.
*/
protected ApplicationBase() {
}
@Override
public final synchronized void init(final String[] args) throws ExecutableException {
setState(ExecutableState.Initializing);
if (args.length > 0) {
// First argument must be the file to open.
String fileEncoding = System.getProperty("file.encoding");
String utf8Path;
try {
utf8Path = new String(args[0].getBytes(fileEncoding),
StandardCharsets.UTF_8);
}
catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
File file = new File(utf8Path);
if (file.exists()) {
OPEN_FILES.add(file);
}
}
try {
initInternal(args);
}
catch (Exception e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to initialize Executable component [%s].", e, this);
}
setState(ExecutableState.Initialized);
}
@Override
public final synchronized void start() throws ExecutableException {
setState(ExecutableState.Starting);
try {
startInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to start Executable component [%s].", e, this);
}
setState(ExecutableState.Started);
}
@Override
public final synchronized void stop() throws ExecutableException {
setState(ExecutableState.Stopping);
try {
stopInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to stop Executable component [%s].", e, this);
}
setState(ExecutableState.Stopped);
}
@Override
public final synchronized void destroy() throws ExecutableException {
if (state == ExecutableState.Started) {
stop();
}
setState(ExecutableState.Destroying);
try {
destroyInternal();
}
catch (ExecutableException e) {
setState(ExecutableState.Error);
throw new ExecutableException("Failed to destroy Executable component [%s].", e, this);
}
setState(ExecutableState.Destroyed);
System.exit(0);
}
@Override
public final void addStateListener(ApplicationStateListener listener) {
requireNonNull(listener, "ApplicationStateListener must not be null.");
if (!stateListeners.contains(listener)) {
stateListeners.add(listener);
}
}
@Override
public final void removeStateListener(ApplicationStateListener listener) {
requireNonNull(listener, "ApplicationStateListener must not be null.");
stateListeners.remove(listener);
}
@Override
public final synchronized ExecutableState getState() {
return state;
}
/**
* Notify state listeners about the new state.
*/
protected final void fireStateChanged() {
stateListeners.forEach(listener -> listener.applicationState(getState()));
}
/**
* @throws ExecutableException If the sub-class fails to initialize this
* application.
*/
protected abstract void initInternal(final String[] args)
throws ExecutableException;
/**
* @throws ExecutableException If the sub-class fails to start this
* application.
*/
protected abstract void startInternal() throws ExecutableException;
/**
* @throws ExecutableException If the sub-class fails to stop this
* application.
*/
protected abstract void stopInternal() throws ExecutableException;
/**
* @throws ExecutableException If the sub-class fails to destroy this
* application.
*/
protected abstract void destroyInternal() throws ExecutableException;
/**
* Creates an instance of the concrete {@code Application} subclass, then
* calls the sequence of following methods:
* <li>{@link #init(String[])}
* <li>{@link #start()}
* <p>
* If a {@link Preloader} class was specified via the system property
* "application.preloader", this concrete preloader will be instantiated
* prior the application startup routine lasting as long as the application
* is initializing. Once the application reaches the starting state, the
* preloader will be closed.
*
* @param args The main method's arguments.
*/
public static void launch(final String[] args) {
try {
Class<? extends Preloader> preloaderClass = null;
String preloaderByProperty = AccessController.doPrivileged((PrivilegedAction<String>) () -> {
return System.getProperty("application.preloader");
});
if (nonNull(preloaderByProperty)) {
Class<?> pClass = null;
try {
pClass = Class.forName(preloaderByProperty, false, Thread.currentThread().getContextClassLoader());
}
catch (Exception e) {
LOG.warn("Could not load application preloader class.", e);
}
if (nonNull(pClass)) {
if (Preloader.class.isAssignableFrom(pClass)) {
preloaderClass = (Class<? extends Preloader>) pClass;
}
else {
LOG.warn("Preloader class is not a subclass of " + Preloader.class.getName() + ".");
}
}
}
launch(args, preloaderClass);
}
catch (Exception e) {
LOG.fatal("Could not launch application.", e);
// Hard exit.
System.exit(0);
}
}
/**
* Creates an instance of the concrete {@code Application} subclass, then
* calls the sequence of following methods:
* <li>{@link #init(String[])}
* <li>{@link #start()}
* <p>
* The specified {@link Preloader} class will be instantiated prior the
* application startup routine lasting as long as the application is
* initializing. Once the application reaches the starting state, the
* preloader will be closed.
*
* @param args The main method's arguments.
* @param preloaderClass The class of the preloader to show while the
* application is loading.
*/
public static void launch(final String[] args, Class<? extends Preloader> preloaderClass) {
try {
StackTraceElement[] cause = Thread.currentThread().getStackTrace();
Class<? extends Application> appClass = null;
for (StackTraceElement se : cause) {
String className = se.getClassName();
String methodName = se.getMethodName();
Class<?> callingClass = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
if ("main".equals(methodName) && Application.class.isAssignableFrom(callingClass)) {
appClass = (Class<? extends Application>) callingClass;
break;
}
}
requireNonNull(appClass, "No application class found.");
launch(args, appClass, preloaderClass);
}
catch (Exception e) {
LOG.fatal("Could not launch application.", e);
// Hard exit.
System.exit(0);
}
}
private static void launch(final String[] args,
Class<? extends Application> appClass,
Class<? extends Preloader> preloaderClass) throws Exception {
requireNonNull(appClass, "Application class must not be null.");
Preloader preloader = null;
if (nonNull(preloaderClass)) {
try {
preloader = preloaderClass.getConstructor().newInstance();
preloader.init(args);
preloader.start();
}
catch (Exception e) {
LOG.warn("Start preloader failed.", e);
}
}
StateListener stateListener = new StateListener(preloader);
Application application = appClass.getConstructor().newInstance();
application.addStateListener(stateListener);
application.init(args);
application.start();
if (nonNull(stateListener.getException())) {
throw stateListener.getException();
}
application.removeStateListener(stateListener);
}
/**
* Update the component state if, and only if, the attempted state
* transition is valid.
*
* @param state The new state for this component.
*
* @throws ExecutableException If the state transition fails.
*/
private synchronized void setState(ExecutableState state) throws ExecutableException {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting state for [{}] to [{}]", this, state);
}
if (!validateNextState(state)) {
throw new ExecutableException(
"Invalid state transition for Executable component [%s] in state [%s] to [%s].",
this, getState(), state);
}
this.state = state;
fireStateChanged();
}
private boolean validateNextState(ExecutableState nextState) {
switch (this.state) {
case Created:
return isAllowed(nextState, ExecutableState.Initializing, ExecutableState.Destroying);
case Initializing:
return isAllowed(nextState, ExecutableState.Initialized, ExecutableState.Error);
case Initialized:
return isAllowed(nextState, ExecutableState.Starting, ExecutableState.Destroying);
case Starting:
return isAllowed(nextState, ExecutableState.Started, ExecutableState.Error);
case Started:
return isAllowed(nextState, ExecutableState.Stopping, ExecutableState.Destroying);
case Stopping:
return isAllowed(nextState, ExecutableState.Stopped, ExecutableState.Error);
case Stopped:
return isAllowed(nextState, ExecutableState.Starting, ExecutableState.Destroying);
case Destroying:
return isAllowed(nextState, ExecutableState.Destroyed, ExecutableState.Error);
case Destroyed:
return isAllowed(nextState, ExecutableState.Initializing);
case Error:
// Allow to recover from previous operation failure.
return isAllowed(nextState, ExecutableState.Starting, ExecutableState.Stopping, ExecutableState.Destroying);
default:
return false;
}
}
private boolean isAllowed(ExecutableState nextState, ExecutableState... allowedStates) {
requireNonNull(allowedStates, "No allowed states provided.");
for (ExecutableState allowedState : allowedStates) {
if (nextState == allowedState) {
return true;
}
}
return false;
}
private static class StateListener implements ApplicationStateListener {
private final Preloader preloader;
private Exception exception;
StateListener(Preloader preloader) {
this.preloader = preloader;
}
@Override
public void applicationState(ExecutableState state) {
if (state == ExecutableState.Starting) {
if (nonNull(preloader)) {
try {
preloader.close();
preloader.destroy();
}
catch (Exception e) {
exception = e;
}
}
}
}
public Exception getException() {
return exception;
}
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import java.io.File;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.lecturestudio.core.exception.UncaughtExceptionHandler;
import org.lecturestudio.core.util.OsInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Base application bootstrapper implementation meant to be extended by specific
* bootstrappers to configure the Virtual Machine for individual applications.
* The bootstrapper will create and start a new Virtual Machine process which
* runs the application.
*
* @param <T> The type of the applications main class.
*
* @author Alex Andres
*/
public abstract class ApplicationBootstrapper<T> {
private final static Logger LOG = LogManager.getLogger(ApplicationBootstrapper.class);
/** The list of Virtual Machine arguments. */
private final List<String> vmArguments = new ArrayList<>();
/** The list of application arguments. */
private final List<String> appArguments = new ArrayList<>();
/**
* Create a new ApplicationBootstrapper instance. This constructor sets the
* default "java.library.path" to "lib/native/{platform}".
*/
public ApplicationBootstrapper() {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(LOG));
addVMArgument("-Djava.library.path=lib/native/" + OsInfo.getPlatformName());
}
/**
* Add application arguments.
*
* @param args The application arguments to add.
*/
public void addAppArguments(String... args) {
appArguments.addAll(Arrays.asList(args));
}
/**
* Add a new Virtual Machine argument.
*
* @param arg The Virtual Machine argument to add.
*/
public void addVMArgument(String arg) {
vmArguments.add(arg);
}
/**
* Initiates the bootstrapping of the application by creating and starting a
* new Virtual Machine process. The previously set arguments via the {@link
* #addVMArgument(String)} method will be passed to the new Virtual Machine
* process.
*/
public final void bootstrap() {
String separator = File.separator;
String javaBin = System.getProperty("java.home") + separator + "bin" + separator + "java";
String classpath = System.getProperty("java.class.path");
// Get main class by reflection.
ParameterizedType parameterizedType = (ParameterizedType) getClass().getGenericSuperclass();
Class<?> mainClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
List<String> parameter = new ArrayList<>();
parameter.add(javaBin);
parameter.add("-cp");
parameter.add(classpath);
parameter.addAll(vmArguments);
parameter.add(mainClass.getCanonicalName());
parameter.addAll(appArguments);
ProcessBuilder processBuilder = new ProcessBuilder(parameter);
try {
processBuilder.start();
}
catch (Exception e) {
LOG.error("Bootstrapping failed.", e);
}
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import java.util.HashMap;
import java.util.Map;
import org.lecturestudio.core.app.configuration.Configuration;
import org.lecturestudio.core.app.dictionary.Dictionary;
import org.lecturestudio.core.bus.EventBus;
import org.lecturestudio.core.model.DocumentList;
import org.lecturestudio.core.service.DocumentService;
import org.lecturestudio.core.view.PresentationParameterProvider;
import org.lecturestudio.core.view.ViewType;
/**
* Base application context implementation that holds data object required by
* the application. Such objects are, for instance, the {@link Configuration},
* the {@link Dictionary} or the {@link DocumentService}.
*
* @author Alex Andres
*/
public abstract class ApplicationContext {
/** The application resource data locator. */
private final AppDataLocator dataLocator;
/** The application configuration. */
private final Configuration configuration;
/** The application dictionary. */
private final Dictionary dictionary;
/** The document service to manage all slide documents. */
private final DocumentService documentService;
/** The presentation provider to manage the presentation of each page. */
private final Map<ViewType, PresentationParameterProvider> ppProvider;
/** The application event data bus. */
private final EventBus eventBus;
/** The audio event bus. */
private final EventBus audioBus;
/**
* This method is meant to be implemented by concrete application context
* class that implement their own configuration handling, like specific
* configuration paths and names.
*
* @throws Exception If an fatal error occurs while saving the configuration.
*/
abstract public void saveConfiguration() throws Exception;
/**
* Create a new ApplicationContext instance with the given parameters.
*
* @param dataLocator The application resource data locator.
* @param config The application configuration.
* @param dict The application dictionary.
* @param eventBus The application event data bus.
* @param audioBus The audio event bus.
*/
public ApplicationContext(AppDataLocator dataLocator, Configuration config,
Dictionary dict, EventBus eventBus, EventBus audioBus) {
this.dataLocator = dataLocator;
this.configuration = config;
this.dictionary = dict;
this.eventBus = eventBus;
this.audioBus = audioBus;
this.ppProvider = new HashMap<>();
this.documentService = new DocumentService(this);
ppProvider.put(ViewType.User, new PresentationParameterProvider(config));
ppProvider.put(ViewType.Preview, new PresentationParameterProvider(config));
ppProvider.put(ViewType.Presentation, new PresentationParameterProvider(config));
}
/**
* Obtain the application configuration.
*
* @return the application configuration.
*/
public Configuration getConfiguration() {
return configuration;
}
/**
* Obtain the application dictionary.
*
* @return the application dictionary.
*/
public Dictionary getDictionary() {
return dictionary;
}
/**
* Obtain the document service.
*
* @return the document service.
*/
public DocumentService getDocumentService() {
return documentService;
}
/**
* Obtain the application event data bus.
*
* @return the application event data bus.
*/
public EventBus getEventBus() {
return eventBus;
}
/**
* Obtain the audio event bus.
*
* @return the audio event bus.
*/
public EventBus getAudioBus() {
return audioBus;
}
/**
* Obtain the list of all opened documents.
*
* @return the list of all opened documents.
*/
public DocumentList getDocuments() {
return documentService.getDocuments();
}
/**
* Obtain the {@code AppDataLocator} to access application specific data.
*
* @return the {@code AppDataLocator}.
*/
public AppDataLocator getDataLocator() {
return dataLocator;
}
/**
* Obtain the PresentationParameterProvider for the given ViewType.
*
* @param type The ViewType of the presentation provider.
*
* @return the PresentationParameterProvider bound to the ViewType.
*/
public PresentationParameterProvider getPagePropertyPropvider(ViewType type) {
return ppProvider.get(type);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import org.lecturestudio.core.presenter.MainPresenter;
/**
* Common interface to provide a consistent mechanism for creating an
* application.
*
* @author Alex Andres
*/
public interface ApplicationFactory {
/**
* Create the application specific ApplicationContext.
*
* @return the application specific ApplicationContext.
*/
ApplicationContext getApplicationContext();
/**
* Create the start presenter that will initialize the initial (start) view
* of the application.
*
* @return the start context.
*/
MainPresenter<?> getStartPresenter();
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import org.lecturestudio.core.ExecutableState;
/**
* A listener for application state transition events. The listener will be
* notified after a state transition has taken place.
*
* @author Alex Andres
*/
public interface ApplicationStateListener {
/**
* Receive the state transition event.
*
* @param state The new state of the application.
*/
void applicationState(ExecutableState state);
}

View file

@ -0,0 +1,189 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingDeque;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.bus.event.ControllerEvent;
import org.lecturestudio.core.controller.CommandController;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Asynchronous {@link CommandController} that executes ControllerEvents in
* receiving order.
*
* @param <T> The type of the controller event.
*
* @author Alex Andres
*/
public abstract class AsyncCommandController<T extends ControllerEvent> extends CommandController<T> {
private static final Logger LOG = LogManager.getLogger(AsyncCommandController.class);
/** A thread safe event queue. */
private final BlockingDeque<Callable<Boolean>> eventQueue = new LinkedBlockingDeque<>();
/** The event queue worker used by a thread. */
private final Runnable worker = () -> {
try {
processEvents();
}
catch (Exception e) {
LOG.error("Could not process controller events.", e);
}
};
/** The thread that is responsible for event processing. */
private final Thread thread = new Thread(worker, getClass().getSimpleName());
/**
* Create a new AsyncCommandController with the specified ApplicationContext.
*
* @param context The ApplicationContext.
*/
protected AsyncCommandController(ApplicationContext context) {
super(context);
}
@Override
protected void startInternal() throws ExecutableException {
super.startInternal();
thread.start();
}
@Override
protected void stopInternal() throws ExecutableException {
super.stopInternal();
// Use poison pill to stop the event-queue thread.
Callable<Boolean> task = () -> {
return false;
};
addTask(task);
}
@Override
protected void processEvent(T event) {
if (event.isSynchronous()) {
try {
super.processEvent(event);
}
catch (Exception e) {
LOG.error("Could not process controller event.", e);
}
return;
}
addEvent(event);
}
/**
* Adds an event to the end of the event queue for later processing.
*
* @param event The controller event.
*/
protected void addEvent(final T event) {
Callable<Boolean> task = getDefaultTask(event);
addTask(task);
}
/**
* Adds an event to the beginning of the event queue for priority
* processing.
*
* @param event The controller event.
*/
protected void addUrgentEvent(final T event) {
Callable<Boolean> task = getDefaultTask(event);
addUrgentTask(task);
}
/**
* Adds a {@code Runnable} to the end of the event queue for later
* processing.
*
* @param task The callable task.
*/
protected void addTask(Callable<Boolean> task) {
eventQueue.addLast(task);
}
/**
* Adds a {@code Runnable} to the beginning of the event queue for priority
* processing.
*
* @param task The callable task.
*/
protected void addUrgentTask(Callable<Boolean> task) {
eventQueue.addFirst(task);
}
/**
* Creates a default controller task as {@code Runnable}.
*
* @param event the {@code ControllerEvent}.
*
* @return a new callable task.
*/
private Callable<Boolean> getDefaultTask(final T event) {
return () -> {
try {
super.processEvent(event);
}
catch (Exception e) {
LOG.error("Could not process controller event", e);
}
return true;
};
}
/**
* Not to be called directly. This method must be called in a separate
* thread.
* <p>
* This method takes tasks represented by {@code Runnable} from the queue
* and invokes them. If the task is based on a {@code ControllerEvent}
* {@link #processEvent} is called for further processing.
*
* @throws Exception If an event can't be executed.
*
* @see Thread#run()
* @see Thread#start()
*/
private void processEvents() throws Exception {
boolean result;
while (true) {
result = eventQueue.takeFirst().call();
if (!result) {
break;
}
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
/**
* Common interface to provide a consistent mechanism for creating an graphical
* application.
*
* @author Alex Andres
*/
public interface GraphicalApplication {
/**
* Create the application specific {@code ApplicationFactory} to bootstrap
* the application.
*
* @return the ApplicationFactory.
*/
ApplicationFactory createApplicationFactory();
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.lecturestudio.core.util.FileUtils;
/**
* Locale helper class to conveniently handle application locales. Application
* internationalisation dictionaries are located in "i18n" resource folders.
*
* @author Alex Andres
*/
public class LocaleProvider {
/**
* Get a list of supported locales which are based on the files located
* in the "i18n" folder.
*
* @return the available localizations.
*/
public List<Locale> getLocales() throws Exception {
List<Locale> locales = new ArrayList<>();
// Load only files which have '.properties' as extension.
String[] listing = FileUtils.getResourceListing("/resources/i18n",
(name) -> name.endsWith(".properties"));
for (String fileName : listing) {
String tag = fileName.substring(0, fileName.lastIndexOf("."));
tag = tag.substring(tag.indexOf("_") + 1);
tag = tag.replace("_", "-");
locales.add(Locale.forLanguageTag(tag));
}
return locales;
}
/**
* Get the locale that best matches the specified locale. If no match can be
* found, the first available locale is returned.
*
* @param locale The locale for which to find the best match.
*
* @return The locale that best matches the provided locale.
*
* @throws Exception - if the application locales could not be loaded.
*/
public Locale getBestSupported(Locale locale) throws Exception {
List<Locale> locales = getLocales();
// Try to find an exact match.
var result = locales.stream().filter(l -> l.equals(locale)).findFirst();
if (result.isPresent()) {
return result.get();
}
// Compare by language.
result = locales.stream()
.filter(l -> l.getLanguage().equals(locale.getLanguage()))
.findFirst();
return result.orElseGet(() -> locales.get(0));
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import org.lecturestudio.core.ExecutableException;
/**
* Common interface to provide a consistent mechanism for creating a graphical
* preloader for an application.
*
* @author Alex Andres
*/
public interface Preloader {
/**
* Prepare the preloader for starting. This method should perform any
* initialization required post object creation, optionally using the
* arguments provided by the {@code #main(String[])} method.
*
* @param args The main method's arguments.
*
* @throws ExecutableException If an fatal error occurred that prevents this
* preloader from being used.
*/
void init(final String[] args) throws ExecutableException;
/**
* Start the preloader and show the UI.
*
* @throws ExecutableException If an fatal error occurred that prevents this
* preloader from being used.
*/
void start() throws ExecutableException;
/**
* Close the preloader and hide the UI.
*
* @throws ExecutableException If an fatal error occurred that prevents this
* preloader from being closed.
*/
void close() throws ExecutableException;
/**
* Cleanup resources used by this preloader.
*
* @throws ExecutableException If an fatal error occurred that prevents this
* preloader from being destroyed.
*/
void destroy() throws ExecutableException;
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import java.util.Objects;
/**
* A Theme defines the graphical appearance of the user interface. Themes can be
* used to customize the the look and feel of the window and its graphical
* control elements. Themes are loaded from theme files.
*
* @author Alex Andres
*/
public class Theme {
/** The theme name. */
private String name;
/** The theme file. */
private String file;
/**
* Create a new Theme with empty name.
*/
public Theme() {
this("", null);
}
/**
* Create a new Theme with the specified name and source file.
*
* @param name The name of the theme.
* @param file The file containing theme definitions.
*/
public Theme(String name, String file) {
this.name = name;
this.file = file;
}
/**
* Obtain the theme name.
*
* @return the theme name.
*/
public String getName() {
return name;
}
/**
* Obtain the theme file.
*
* @return the theme file.
*/
public String getFile() {
return file;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
Theme theme = (Theme) other;
return name.equals(theme.name) && Objects.equals(file, theme.file);
}
@Override
public int hashCode() {
return Objects.hash(name, file);
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app;
import java.util.ArrayList;
import java.util.List;
import org.lecturestudio.core.util.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Theme helper class to conveniently handle application themes. Application
* theme files have the suffix '.theme.css'.
*
* @author Alex Andres
*/
public class ThemeProvider {
private final static Logger LOG = LogManager.getLogger(ThemeProvider.class);
/**
* Get all available UI themes.
*
* @return the list of all available UI themes.
*/
public List<Theme> getThemes() {
List<Theme> themes = new ArrayList<>();
themes.add(new Theme("default", null));
String suffix = ".theme.css";
try {
String[] listing = FileUtils.getResourceListing("/resources/css",
(name) -> name.endsWith(suffix));
for (String fileName : listing) {
String name = fileName.substring(0, fileName.lastIndexOf(suffix));
name = name.substring(name.lastIndexOf("/") + 1);
themes.add(new Theme(name, fileName));
}
}
catch (Exception e) {
LOG.warn("Load themes failed", e);
}
return themes;
}
}

View file

@ -0,0 +1,317 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.beans.DoubleProperty;
import org.lecturestudio.core.beans.FloatProperty;
import org.lecturestudio.core.beans.ObjectProperty;
import org.lecturestudio.core.beans.StringProperty;
import org.lecturestudio.core.util.ObservableHashMap;
import org.lecturestudio.core.util.ObservableMap;
/**
* The AudioConfiguration specifies audio related properties for the
* application.
*
* @author Alex Andres
*/
public class AudioConfiguration {
/** The capture device name. */
private final StringProperty inputDeviceName = new StringProperty();
/** The playback device name. */
private final StringProperty outputDeviceName = new StringProperty();
/** The sound system name. */
private final StringProperty soundSystem = new StringProperty();
/** The path where the recordings are stored at. */
private final StringProperty recordingPath = new StringProperty();
/** The capture device recording volume. */
private final FloatProperty recordingVolume = new FloatProperty();
/** The capture device master recording volume. */
private final FloatProperty recordingMasterVolume = new FloatProperty();
/** The playback device volume. */
private final DoubleProperty playbackVolume = new DoubleProperty();
/** The recording volumes of all used capture devices. */
private final ObservableHashMap<String, Double> recordingVolumes = new ObservableHashMap<>();
/** The audio format of the recording. */
private final ObjectProperty<AudioFormat> recordingFormat = new ObjectProperty<>();
/**
* Obtain the capture device name.
*
* @return the capture device name.
*/
public String getInputDeviceName() {
return inputDeviceName.get();
}
/**
* Set the capture device name.
*
* @param deviceName the capture device name to set.
*/
public void setInputDeviceName(String deviceName) {
this.inputDeviceName.set(deviceName);
}
/**
* Obtain the capture device name property.
*
* @return the capture device name property.
*/
public StringProperty inputDeviceNameProperty() {
return inputDeviceName;
}
/**
* Obtain the playback device name.
*
* @return the playback device name.
*/
public String getOutputDeviceName() {
return outputDeviceName.get();
}
/**
* Set the playback device name.
*
* @param deviceName the playback device name to set.
*/
public void setOutputDeviceName(String deviceName) {
this.outputDeviceName.set(deviceName);
}
/**
* Obtain the playback device name property.
*
* @return the playback device name property.
*/
public StringProperty outputDeviceNameProperty() {
return outputDeviceName;
}
/**
* Obtain the sound system name.
*
* @return the sound system name.
*/
public String getSoundSystem() {
return soundSystem.get();
}
/**
* Set the sound system name.
*
* @param soundSystem sound system name to set.
*/
public void setSoundSystem(String soundSystem) {
this.soundSystem.set(soundSystem);
}
/**
* Obtain the sound system property.
*
* @return the sound system property.
*/
public StringProperty soundSystemProperty() {
return soundSystem;
}
/**
* Obtain the recording path.
*
* @return the recording path.
*/
public String getRecordingPath() {
return recordingPath.get();
}
/**
* Set the new path where to store the recordings.
*
* @param recordingPath the recording path to set.
*/
public void setRecordingPath(String recordingPath) {
this.recordingPath.set(recordingPath);
}
/**
* Obtain the recording path property.
*
* @return the recording path property.
*/
public StringProperty recordingPathProperty() {
return recordingPath;
}
/**
* Obtain the capture device recording volume in the range of [0, 1].
*
* @return the capture device recording volume.
*/
public float getDefaultRecordingVolume() {
return recordingVolume.get();
}
/**
* Set the capture device recording volume. The volume value must be in the
* range of [0, 1].
*
* @param volume the recording volume to set.
*/
public void setDefaultRecordingVolume(float volume) {
this.recordingVolume.set(volume);
}
/**
* Obtain the recording volume property.
*
* @return the recording volume property.
*/
public FloatProperty recordingVolumeProperty() {
return recordingVolume;
}
/**
* Obtain the capture device recording volume in the range of [0, 1].
*
* @return the capture device recording volume.
*/
public float getMasterRecordingVolume() {
return recordingMasterVolume.get();
}
/**
* Set the capture device recording volume. The volume value must be in the
* range of [0, 1].
*
* @param volume the recording volume to set.
*/
public void setMasterRecordingVolume(float volume) {
this.recordingMasterVolume.set(volume);
}
/**
* Obtain the master recording volume property.
*
* @return the recording volume property.
*/
public FloatProperty recordingMasterVolumeProperty() {
return recordingMasterVolume;
}
/**
* Obtain the playback device volume in the range of [0, 1].
*
* @return the playback device volume.
*/
public double getPlaybackVolume() {
return playbackVolume.get();
}
/**
* Set the playback device volume. The volume value must be in the
* range of [0, 1].
*
* @param volume the playback volume to set.
*/
public void setPlaybackVolume(double volume) {
this.playbackVolume.set(volume);
}
/**
* Obtain the playback volume property.
*
* @return the playback volume property.
*/
public DoubleProperty playbackVolumeProperty() {
return playbackVolume;
}
/**
* Obtain the recording volumes of all used capture devices. The key of the
* map refers to the capture device name and the value stores the recording
* volume configured for this device.
*
* @return recording volumes of all used capture devices.
*/
public ObservableMap<String, Double> getRecordingVolumes() {
return recordingVolumes;
}
/**
* Set the recording volume for the specified capture device name. The
* volume value must be in the range of [0, 1].
*
* @param deviceName The capture device name.
* @param volume The recording volume.
*/
public void setRecordingVolume(String deviceName, double volume) {
recordingVolumes.put(deviceName, volume);
}
/**
* Obtain the recording volume for the specified capture device name.
*
* @param deviceName The capture device name.
*
* @return the recording volume in the range of [0, 1].
*/
public Double getRecordingVolume(String deviceName) {
return recordingVolumes.get(deviceName);
}
/**
* Obtain the audio format of the recording.
*
* @return the audio format of the recording.
*/
public AudioFormat getRecordingFormat() {
return recordingFormat.get();
}
/**
* Set the audio format of the recording.
*
* @param format The new audio format to set.
*/
public void setRecordingFormat(AudioFormat format) {
this.recordingFormat.set(format);
}
/**
* Obtain the audio format property.
*
* @return the audio format property.
*/
public ObjectProperty<AudioFormat> recordingFormatProperty() {
return recordingFormat;
}
}

View file

@ -0,0 +1,524 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.lecturestudio.core.app.Theme;
import org.lecturestudio.core.beans.BooleanProperty;
import org.lecturestudio.core.beans.DoubleProperty;
import org.lecturestudio.core.beans.ObjectProperty;
import org.lecturestudio.core.beans.StringProperty;
import org.lecturestudio.core.geometry.Dimension2D;
import org.lecturestudio.core.io.ResourceLoader;
import org.lecturestudio.core.model.RecentDocument;
import org.lecturestudio.core.util.ObservableArrayList;
import org.lecturestudio.core.util.ObservableHashSet;
import org.lecturestudio.core.util.ObservableList;
import org.lecturestudio.core.util.ObservableSet;
/**
* The Configuration specifies application wide properties. Context specific
* properties are encapsulated in the respective separate configuration classes.
*
* @author Alex Andres
*/
public class Configuration {
/** The version of the application. */
private String version;
/** The build date of the application. */
private Calendar buildDate;
/** The name of the application. */
private final StringProperty applicationName = new StringProperty();
/** The theme of the UI of the application. */
private final ObjectProperty<Theme> theme = new ObjectProperty<>();
/** The locale of the application. */
private final ObjectProperty<Locale> locale = new ObjectProperty<>();
/** The UI control size of the application. */
private final DoubleProperty uiControlSize = new DoubleProperty();
/** Indicates whether to open the application window maximized. */
private final BooleanProperty startMaximized = new BooleanProperty();
/** Indicates whether to enable a virtual keyboard on tablet devices. */
private final BooleanProperty tabletMode = new BooleanProperty();
/** Enables/disables advanced settings visible in the settings UI view. */
private final BooleanProperty advancedUIMode = new BooleanProperty();
/** Hides/shows UI elements, like the menu, in fullscreen mode. */
private final BooleanProperty extendedFullscreen = new BooleanProperty();
/** Defines the extended drawing area of a page. */
private final ObjectProperty<Dimension2D> extendPageDimension = new ObjectProperty<>();
/** The list of recently opened slide documents. */
private final ObservableList<RecentDocument> recentDocuments = new ObservableArrayList<>();
/**
* The set of window configurations that describe the properties of all
* opened windows and dialogs.
*/
private final ObservableSet<WindowConfiguration> windowConfigs = new ObservableHashSet<>();
/** The grid configuration containing all grid related properties. */
private final GridConfiguration gridConfig = new GridConfiguration();
/** The whiteboard configuration containing all whiteboard related properties. */
private final WhiteboardConfiguration whiteboardConfig = new WhiteboardConfiguration();
/** The grid display containing all display related properties. */
private final DisplayConfiguration displayConfig = new DisplayConfiguration();
/** The tool configuration containing all tool related properties. */
private final ToolConfiguration toolConfig = new ToolConfiguration();
/** The audio configuration containing all audio related properties. */
private final AudioConfiguration audioConfig = new AudioConfiguration();
/**
* Creates a new Configuration instance.
*/
public Configuration() {
String version = null;
Calendar date = Calendar.getInstance();
try {
Manifest manifest = new Manifest(ResourceLoader.getResourceAsStream(JarFile.MANIFEST_NAME));
Attributes attr = manifest.getMainAttributes();
String buildDate = attr.getValue("Build-Date");
version = attr.getValue("Package-Version");
if (version == null) {
// Set default version.
version = "1.0";
}
if (buildDate != null) {
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
date.setTime(formatter.parse(buildDate));
}
}
catch (Exception e) {
// Ignore
}
setVersion(version);
setBuildDate(date);
}
/**
* Obtain the current version of the application.
*
* @return the version of the application.
*/
public String getVersion() {
return version;
}
/**
* Set the current version of the application.
*
* @param version The new version to set.
*/
public void setVersion(String version) {
this.version = version;
}
/**
* Obtain the build date of the application.
*
* @return the build date of the application.
*/
public Calendar getBuildDate() {
return buildDate;
}
/**
* Set the build date of the application.
*
* @param date The build date of the application.
*/
public void setBuildDate(Calendar date) {
this.buildDate = date;
}
/**
* Obtain the name of the application.
*
* @return the application name.
*/
public String getApplicationName() {
return applicationName.get();
}
/**
* Set the name of the application.
*
* @param name The application name.
*/
public void setApplicationName(String name) {
this.applicationName.set(name);
}
/**
* Obtain the application name property.
*
* @return the application name property.
*/
public ObjectProperty<String> applicationNameProperty() {
return applicationName;
}
/**
* Obtain the current theme of the UI of the application.
*
* @return the UI theme.
*/
public Theme getTheme() {
return theme.get();
}
/**
* Set the new UI theme.
*
* @param theme The UI theme to set.
*/
public void setTheme(Theme theme) {
this.theme.set(theme);
}
/**
* Obtain the theme property.
*
* @return the theme property.
*/
public ObjectProperty<Theme> themeProperty() {
return theme;
}
/**
* Obtain the current locale of the application.
*
* @return the current locale.
*/
public Locale getLocale() {
return locale.get();
}
/**
* Set the new locale of the application.
*
* @param locale The new locale to set.
*/
public void setLocale(Locale locale) {
this.locale.set(locale);
}
/**
* Obtain the locale property.
*
* @return the locale property.
*/
public ObjectProperty<Locale> localeProperty() {
return locale;
}
/**
* Obtain the UI control size of the application.
*
* @return the UI control size.
*/
public double getUIControlSize() {
return uiControlSize.get();
}
/**
* Set the new UI control size of the application.
*
* @param size The new UI control size.
*/
public void setUIControlSize(double size) {
this.uiControlSize.set(size);
}
/**
* Obtain the UI control size property.
*
* @return the UI control size property.
*/
public DoubleProperty uiControlSizeProperty() {
return uiControlSize;
}
/**
* Check whether to open the application window maximized.
*
* @return true if the application window should be opened maximized, false
* otherwise.
*/
public Boolean getStartMaximized() {
return startMaximized.get();
}
/**
* Set whether to open the application window maximized.
*
* @param maximized True to open the application window maximized, false
* otherwise.
*/
public void setStartMaximized(boolean maximized) {
this.startMaximized.set(maximized);
}
/**
* Obtain the start maximized property.
*
* @return the start maximized property.
*/
public BooleanProperty startMaximizedProperty() {
return startMaximized;
}
/**
* Check whether to enable a virtual keyboard on tablet devices.
*
* @return true to enable a virtual keyboard, false otherwise.
*/
public Boolean getTabletMode() {
return tabletMode.get();
}
/**
* Set whether to enable a virtual keyboard on tablet devices.
*
* @param enable True to enable a virtual keyboard, false otherwise.
*/
public void setTabletMode(boolean enable) {
this.tabletMode.set(enable);
}
/**
* Obtain the tablet mode property.
*
* @return the tablet mode property.
*/
public BooleanProperty tabletModeProperty() {
return tabletMode;
}
/**
* Check whether to hide/show UI elements, like the menu, in fullscreen
* mode.
*
* @return true if the extended fullscreen mode is enabled, false otherwise.
*
* @see #setExtendedFullscreen(boolean)
*/
public Boolean getExtendedFullscreen() {
return extendedFullscreen.get();
}
/**
* Set whether to hide/show UI elements, like the menu, in fullscreen mode.
* <p>
* If the value is set to {@code true}, then the related UI elements must be
* hidden when fullscreen is activated, and shown again when leaving the
* fullscreen mode.
*
* @param enabled True to enable the extended fullscreen mode, false
* otherwise.
*/
public void setExtendedFullscreen(boolean enabled) {
this.extendedFullscreen.set(enabled);
}
/**
* Obtain the extended fullscreen mode property.
*
* @return the extended fullscreen mode property.
*/
public BooleanProperty extendedFullscreenProperty() {
return extendedFullscreen;
}
/**
* Check whether to enable/disable advanced settings visible in the settings
* UI view.
*
* @return true if the advanced settings mode is enabled, false otherwise.
*
* @see #setAdvancedUIMode(Boolean)
*/
public Boolean getAdvancedUIMode() {
return advancedUIMode.get();
}
/**
* Set whether to enable/disable advanced settings visible in the settings
* UI view.
* <p>
* If the value is set to {@code true}, then the advanced settings UI
* elements must be visible in the settings UI view, and hidden again when
* the advanced settings mode is disabled.
*
* @param enabled True to enable the advanced settings mode, false
* otherwise.
*/
public void setAdvancedUIMode(Boolean enabled) {
this.advancedUIMode.set(enabled);
}
/**
* Obtain the advanced settings mode property.
*
* @return the advanced settings mode property.
*/
public BooleanProperty advancedUIModeProperty() {
return advancedUIMode;
}
/**
* Obtain the extended drawing area of a page.
*
* @return the new extended drawing area.
*
* @see #setExtendPageDimension(Dimension2D)
*/
public Dimension2D getExtendPageDimension() {
return extendPageDimension.get();
}
/**
* Set the new extended drawing area of a page. The specified dimension
* defines how the page is scaled down in order to provide additional blank
* drawing area. The width and height of the specified dimension must be in
* the range of [0,1].
*
* @param dimension The new extended page dimension to set.
*/
public void setExtendPageDimension(Dimension2D dimension) {
this.extendPageDimension.set(dimension);
}
/**
* Obtain the extended page dimension property.
*
* @return the extended page dimension property.
*/
public ObjectProperty<Dimension2D> extendPageDimensionProperty() {
return extendPageDimension;
}
/**
* Obtain the list of recently opened slide documents.
*
* @return the list of recently opened slide documents.
*
* @see RecentDocument
*/
public ObservableList<RecentDocument> getRecentDocuments() {
return recentDocuments;
}
/**
* Obtain the set of window configurations that describe the properties of
* all opened windows and dialogs. Each time a window or dialog is opened
* the set should be updated in order to show the windows and dialogs with
* the recently used properties.
*
* @return the set of window configurations.
*
* @see #addWindowConfiguration(WindowConfiguration)
*/
public Set<WindowConfiguration> getWindowConfigurations() {
return windowConfigs;
}
/**
* Add a new {@link WindowConfiguration} to the set.
*
* @param config The new WindowConfiguration to add.
*/
public void addWindowConfiguration(WindowConfiguration config) {
if (!windowConfigs.add(config)) {
windowConfigs.remove(config);
windowConfigs.add(config);
}
}
/**
* Obtain the grid configuration containing all grid related properties.
*
* @return the grid configuration.
*/
public GridConfiguration getGridConfig() {
return gridConfig;
}
/**
* Obtain the whiteboard configuration containing all whiteboard related
* properties.
*
* @return the whiteboard configuration.
*/
public WhiteboardConfiguration getWhiteboardConfig() {
return whiteboardConfig;
}
/**
* Obtain the grid display containing all display related properties.
*
* @return the display configuration.
*/
public DisplayConfiguration getDisplayConfig() {
return displayConfig;
}
/**
* Obtain the tool configuration containing all tool related properties.
*
* @return the tool configuration.
*/
public ToolConfiguration getToolConfig() {
return toolConfig;
}
/**
* Obtain the audio configuration containing all audio related properties.
*
* @return the audio configuration.
*/
public AudioConfiguration getAudioConfig() {
return audioConfig;
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import java.io.File;
import java.io.IOException;
/**
* Common interface to provide a consistent mechanism for loading and saving
* application configurations that are stored in the operating systems file
* system.
*
* @param <T> The type of the configuration.
*
* @author Alex Andres
*/
public interface ConfigurationService<T> {
/**
* Loads a configuration of the specified class type from the specified
* file.
*
* @param file The file containing the configuration values.
* @param cls The class of the configuration to create.
*
* @return an instance of the specified configuration type.
*
* @throws IOException If an fatal error occurred while loading the
* configuration file.
*/
T load(File file, Class<T> cls) throws IOException;
/**
* Save a configuration object of the specified type to the specified file.
*
* @param file The destination file of the configuration.
* @param config The configuration object.
*
* @throws IOException If an fatal error occurred while saving the
* configuration file.
*/
void save(File file, T config) throws IOException;
}

View file

@ -0,0 +1,155 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.beans.BooleanProperty;
import org.lecturestudio.core.beans.ObjectProperty;
import org.lecturestudio.core.geometry.Position;
import org.lecturestudio.core.graphics.Color;
import org.lecturestudio.core.util.ObservableArrayList;
import org.lecturestudio.core.util.ObservableList;
/**
* The DisplayConfiguration specifies display related properties for the
* application.
*
* @author Alex Andres
*/
public class DisplayConfiguration {
/**
* The list of screen configurations that describe the properties of all
* connected displays.
*/
private final ObservableList<ScreenConfiguration> screens = new ObservableArrayList<>();
/** The background color of windows that are shown on connected displays. */
private final ObjectProperty<Color> backgroundColor = new ObjectProperty<>();
/** The position of the IP address that is shown on connected displays. */
private final ObjectProperty<Position> ipPosition = new ObjectProperty<>();
/**
* Indicates whether to automatically enable connected displays when the
* application starts.
*/
private final BooleanProperty autostart = new BooleanProperty();
/**
* Obtain the observable list of screen configurations that describe the
* properties of all connected displays. Each time a new display is
* connected to the system this list should be updated in order to show the
* displays with the recently used properties.
*
* @return the observable list of screen configurations.
*/
public ObservableList<ScreenConfiguration> getScreens() {
return screens;
}
/**
* Obtain the position of the IP address that is shown on connected displays.
*
* @return the position of the IP address.
*
* @see #setIpPosition(Position)
*/
public Position getIpPosition() {
return ipPosition.get();
}
/**
* Set the new position of the IP address. The IP address must be the one of
* the system that runs the web services.
*
* @param position The new position to set.
*/
public void setIpPosition(Position position) {
this.ipPosition.set(position);
}
/**
* Obtain the IP position property.
*
* @return the IP position property.
*/
public ObjectProperty<Position> ipPositionProperty() {
return ipPosition;
}
/**
* Obtain the background color of windows that are shown on connected
* displays.
*
* @return the background color of the windows.
*/
public Color getBackgroundColor() {
return backgroundColor.get();
}
/**
* Set the background color of windows that are shown on connected displays.
*
* @param color The new background color to set.
*/
public void setBackgroundColor(Color color) {
this.backgroundColor.set(color);
}
/**
* Obtain the background color property.
*
* @return the background color property.
*/
public ObjectProperty<Color> backgroundColorProperty() {
return backgroundColor;
}
/**
* Check whether to automatically enable connected displays when the
* application starts.
*
* @return true to automatically enable connected displays, false otherwise.
*/
public Boolean getAutostart() {
return autostart.get();
}
/**
* Set whether to automatically enable connected displays when the
* application starts.
*
* @param enable True to automatically enable connected displays, false
* otherwise.
*/
public void setAutostart(boolean enable) {
this.autostart.set(enable);
}
/**
* Obtain the autostart property.
*
* @return the autostart property.
*/
public BooleanProperty autostartProperty() {
return autostart;
}
}

View file

@ -0,0 +1,216 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.beans.BooleanProperty;
import org.lecturestudio.core.beans.DoubleProperty;
import org.lecturestudio.core.beans.ObjectProperty;
import org.lecturestudio.core.graphics.Color;
/**
* The GridConfiguration specifies grid related properties for the application
* while rendering and annotating document pages.
*
* @author Alex Andres
*/
public class GridConfiguration {
/** Defines the vertical line spacing of the grid. */
private final DoubleProperty verticalLinesInterval = new DoubleProperty();
/** Indicates whether vertical lines of the grid are visible. */
private final BooleanProperty verticalLinesVisible = new BooleanProperty();
/** Defines the horizontal line spacing of the grid. */
private final DoubleProperty horizontalLinesInterval = new DoubleProperty();
/** Indicates whether horizontal lines of the grid are visible. */
private final BooleanProperty horizontalLinesVisible = new BooleanProperty();
/** The grid color. */
private final ObjectProperty<Color> color = new ObjectProperty<>();
/** Indicates whether to show the grid on connected displays. */
private final BooleanProperty showGridOnDisplays = new BooleanProperty();
/**
* Check whether vertical lines of the grid are visible.
*
* @return true if vertical lines of the grid should be visible, false
* otherwise.
*/
public Boolean getVerticalLinesVisible() {
return verticalLinesVisible.get();
}
/**
* Set whether vertical lines of the grid should be visible.
*
* @param visible True to enable vertical lines, false otherwise.
*/
public void setVerticalLinesVisible(boolean visible) {
this.verticalLinesVisible.set(visible);
}
/**
* Obtain the vertical lines visible property.
*
* @return the vertical lines visible property.
*/
public BooleanProperty verticalLinesVisibleProperty() {
return verticalLinesVisible;
}
/**
* Obtain the vertical line spacing of the grid.
*
* @return the vertical line spacing of the grid.
*/
public Double getVerticalLinesInterval() {
return verticalLinesInterval.get();
}
/**
* Set the vertical line spacing of the grid.
*
* @param spacing The vertical line spacing of the grid.
*/
public void setVerticalLinesInterval(double spacing) {
this.verticalLinesInterval.set(spacing);
}
/**
* Obtain the vertical line spacing property.
*
* @return the vertical line spacing property.
*/
public DoubleProperty verticalLinesIntervalProperty() {
return verticalLinesInterval;
}
/**
* Check whether horizontal lines of the grid are visible.
*
* @return true if horizontal lines of the grid should be visible, false
* otherwise.
*/
public Boolean getHorizontalLinesVisible() {
return horizontalLinesVisible.get();
}
/**
* Set whether horizontal lines of the grid should be visible.
*
* @param visible True to enable horizontal lines, false otherwise.
*/
public void setHorizontalLinesVisible(boolean visible) {
this.horizontalLinesVisible.set(visible);
}
/**
* Obtain the horizontal lines visible property.
*
* @return the horizontal lines visible property.
*/
public BooleanProperty horizontalLinesVisibleProperty() {
return horizontalLinesVisible;
}
/**
* Obtain the horizontal line spacing of the grid.
*
* @return the horizontal line spacing of the grid.
*/
public Double getHorizontalLinesInterval() {
return horizontalLinesInterval.get();
}
/**
* Set the horizontal line spacing of the grid.
*
* @param spacing The horizontal line spacing of the grid.
*/
public void setHorizontalLinesInterval(double spacing) {
this.horizontalLinesInterval.set(spacing);
}
/**
* Obtain the horizontal line spacing property.
*
* @return the horizontal line spacing property.
*/
public DoubleProperty horizontalLinesIntervalProperty() {
return horizontalLinesInterval;
}
/**
* Obtain the grid color.
*
* @return the grid color.
*/
public Color getColor() {
return color.get();
}
/**
* Set the new grid color.
*
* @param color The new color to set.
*/
public void setColor(Color color) {
this.color.set(color);
}
/**
* Obtain the grid color property.
*
* @return the grid color property.
*/
public ObjectProperty<Color> colorProperty() {
return color;
}
/**
* Check whether to show the grid on connected displays.
*
* @return true to show the grid, false otherwise.
*/
public Boolean getShowGridOnDisplays() {
return showGridOnDisplays.get();
}
/**
* Set whether to show the grid on connected displays.
*
* @param show True to show the grid, false otherwise.
*/
public void setShowGridOnDisplays(boolean show) {
this.showGridOnDisplays.set(show);
}
/**
* Obtain the show grid property.
*
* @return the show grid property.
*/
public BooleanProperty showGridOnDisplaysProperty() {
return showGridOnDisplays;
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import static java.util.Objects.nonNull;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import org.lecturestudio.core.app.configuration.bind.AudioFormatDeserializer;
import org.lecturestudio.core.app.configuration.bind.AudioFormatSerializer;
import org.lecturestudio.core.app.configuration.bind.CalendarDeserializer;
import org.lecturestudio.core.app.configuration.bind.ColorDeserializer;
import org.lecturestudio.core.app.configuration.bind.ColorSerializer;
import org.lecturestudio.core.app.configuration.bind.Rectangle2DMixin;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.geometry.Rectangle2D;
import org.lecturestudio.core.graphics.Color;
import org.lecturestudio.core.util.ObservableArrayList;
import org.lecturestudio.core.util.ObservableHashSet;
import org.lecturestudio.core.util.ObservableList;
import org.lecturestudio.core.util.ObservableSet;
/**
* ConfigurationService implementation for loading and saving configuration
* files in the JSON format.
*
* @param <T> The type of the configuration.
*
* @author Alex Andres
*/
public class JsonConfigurationService<T> implements ConfigurationService<T> {
/**
* The object mapper that configures the conversion to and from the JSON
* format.
*/
private ObjectMapper mapper;
/**
* Create a new JsonConfigurationService instance.
*/
public JsonConfigurationService() {
SimpleModule module = new SimpleModule();
module.addAbstractTypeMapping(ObservableList.class, ObservableArrayList.class);
module.addAbstractTypeMapping(ObservableSet.class, ObservableHashSet.class);
module.addSerializer(Color.class, new ColorSerializer());
module.addSerializer(AudioFormat.class, new AudioFormatSerializer());
module.addDeserializer(Color.class, new ColorDeserializer());
module.addDeserializer(AudioFormat.class, new AudioFormatDeserializer());
module.addDeserializer(Calendar.class, new CalendarDeserializer());
module.setMixInAnnotation(Rectangle2D.class, Rectangle2DMixin.class);
mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.registerModule(module);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public T load(File file, Class<T> cls) throws IOException {
T config;
InputStream input = null;
try {
if (file.exists()) {
input = new FileInputStream(file);
}
else {
input = getClass().getResourceAsStream(file.getPath().replace("\\", "/"));
}
if (input == null) {
throw new IOException("Unable to load configuration file. File does not exist.");
}
config = mapper.readValue(input, cls);
}
catch (IOException e) {
throw e;
}
finally {
if (input != null) {
try {
input.close();
}
catch (Exception e) {
throw e;
}
}
}
return config;
}
@Override
public void save(File file, T config) throws IOException {
File parent = file.getParentFile();
if (nonNull(parent) && !parent.exists()) {
parent.mkdirs();
}
mapper.writeValue(file, config);
}
/**
* Meant to be overridden by sub-classes to add custom modules to the object
* mapper.
*
* @param mapper The JSON object mapper.
*/
protected void initModules(ObjectMapper mapper) {
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.beans.BooleanProperty;
import org.lecturestudio.core.beans.ObjectProperty;
import org.lecturestudio.core.view.Screen;
/**
* The ScreenConfiguration specifies screen related properties of connected
* displays.
*
* @author Alex Andres
*/
public class ScreenConfiguration {
/** The screen object defining the screen bounds of the connected display. */
private final ObjectProperty<Screen> screen = new ObjectProperty<>();
/** Indicates whether the connected display should be activated or not. */
private final BooleanProperty enabled = new BooleanProperty();
/**
* Obtain screen property.
*
* @return screen property.
*/
public ObjectProperty<Screen> screenProperty() {
return screen;
}
/**
* Obtain the associated screen information.
*
* @return the screen information.
*/
public Screen getScreen() {
return screen.get();
}
/**
* Set the new Screen information.
*
* @param screen The new Screen to set.
*/
public void setScreen(Screen screen) {
this.screen.set(screen);
}
/**
* Obtain enabled property.
*
* @return enabled property.
*/
public BooleanProperty enabledProperty() {
return enabled;
}
/**
* Check whether the connected display should be activated or not.
*
* @return true to activate the display, false otherwise.
*/
public Boolean getEnabled() {
return enabled.get();
}
/**
* Set whether the connected display should be activated or not.
*
* @param enabled True to activate the display, false otherwise.
*/
public void setEnabled(Boolean enabled) {
this.enabled.set(enabled);
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.graphics.Color;
import org.lecturestudio.core.tool.LatexToolSettings;
import org.lecturestudio.core.tool.StrokeSettings;
import org.lecturestudio.core.tool.TextSelectionSettings;
import org.lecturestudio.core.tool.TextSettings;
import org.lecturestudio.core.util.ObservableArrayList;
import org.lecturestudio.core.util.ObservableList;
/**
* The ToolConfiguration specifies tool related properties and settings. Tool
* specific properties are encapsulated in the respective separate configuration
* classes.
*
* @author Alex Andres
*/
public class ToolConfiguration {
/** The list of pre-defined tool colors. */
private final ObservableList<Color> presetColors = new ObservableArrayList<>();
/** The pen tool settings. */
private final StrokeSettings penSettings = new StrokeSettings();
/** The highlighter tool settings. */
private final StrokeSettings highlighterSettings = new StrokeSettings();
/** The pointer tool settings. */
private final StrokeSettings pointerSettings = new StrokeSettings();
/** The arrow tool settings. */
private final StrokeSettings arrowSettings = new StrokeSettings();
/** The line tool settings. */
private final StrokeSettings lineSettings = new StrokeSettings();
/** The rectangle tool settings. */
private final StrokeSettings rectangleSettings = new StrokeSettings();
/** The ellipse tool settings. */
private final StrokeSettings ellipseSettings = new StrokeSettings();
/** The text selection tool settings. */
private final TextSelectionSettings textSelectionSettings = new TextSelectionSettings();
/** The text tool settings. */
private final TextSettings textSettings = new TextSettings();
/** The LaTeX tool settings. */
private final LatexToolSettings latexSettings = new LatexToolSettings();
/**
* Obtain the observable list of pre-defined tool colors.
*
* @return the observable list of pre-defined tool colors.
*/
public ObservableList<Color> getPresetColors() {
return presetColors;
}
/**
* Obtain the pen tool settings.
*
* @return the pen tool settings.
*/
public StrokeSettings getPenSettings() {
return penSettings;
}
/**
* Obtain the highlighter tool settings.
*
* @return the highlighter tool settings.
*/
public StrokeSettings getHighlighterSettings() {
return highlighterSettings;
}
/**
* Obtain the pointer tool settings.
*
* @return the pointer tool settings.
*/
public StrokeSettings getPointerSettings() {
return pointerSettings;
}
/**
* Obtain the arrow tool settings.
*
* @return the arrow tool settings.
*/
public StrokeSettings getArrowSettings() {
return arrowSettings;
}
/**
* Obtain the line tool settings.
*
* @return the line tool settings.
*/
public StrokeSettings getLineSettings() {
return lineSettings;
}
/**
* Obtain the rectangle tool settings.
*
* @return the rectangle tool settings.
*/
public StrokeSettings getRectangleSettings() {
return rectangleSettings;
}
/**
* Obtain the ellipse tool settings.
*
* @return the ellipse tool settings.
*/
public StrokeSettings getEllipseSettings() {
return ellipseSettings;
}
/**
* Obtain the text selection tool settings.
*
* @return the text selection tool settings.
*/
public TextSelectionSettings getTextSelectionSettings() {
return textSelectionSettings;
}
/**
* Obtain the text tool settings.
*
* @return the text tool settings.
*/
public TextSettings getTextSettings() {
return textSettings;
}
/**
* Obtain the LaTeX tool settings.
*
* @return the LaTeX tool settings.
*/
public LatexToolSettings getLatexSettings() {
return latexSettings;
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.beans.ObjectProperty;
import org.lecturestudio.core.graphics.Color;
/**
* The WhiteboardConfiguration specifies whiteboard related properties.
*
* @author Alex Andres
*/
public class WhiteboardConfiguration {
/** The background color of the whiteboard. */
private final ObjectProperty<Color> backgroundColor = new ObjectProperty<>();
/**
* Obtain background color of the whiteboard.
*
* @return the background color.
*/
public Color getBackgroundColor() {
return backgroundColor.get();
}
/**
* Set the new background color of the whiteboard.
*
* @param color The new background color to set.
*/
public void setBackgroundColor(Color color) {
this.backgroundColor.set(color);
}
/**
* Obtain background color property.
*
* @return background color property.
*/
public ObjectProperty<Color> backgroundColorProperty() {
return backgroundColor;
}
}

View file

@ -0,0 +1,130 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import org.lecturestudio.core.geometry.Rectangle2D;
/**
* The WindowConfiguration specifies window related properties of all opened
* windows and dialogs.
*
* @author Alex Andres
*/
public class WindowConfiguration {
/** The class of the window object. */
private Class<?> windowClass;
/**
* The device ID of the display device on which the window was recently
* shown.
*/
private String deviceId;
/** The window bounds. */
private Rectangle2D windowBounds;
/**
* Obtain the class of the window object.
*
* @return the class of the window object.
*/
public Class<?> getWindowClass() {
return windowClass;
}
/**
* Set the class of the window object.
*
* @param windowClass The class of the window object.
*/
public void setWindowClass(Class<?> windowClass) {
this.windowClass = windowClass;
}
/**
* Obtain the device ID of the display device on which the window was
* recently shown.
*
* @return the device ID of the display device.
*/
public String getDeviceId() {
return deviceId;
}
/**
* Set the device ID of the display device on which the window was recently
* shown.
*
* @param deviceId The device ID of the display device.
*/
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
/**
* Obtain the window bounds.
*
* @return the window bounds.
*/
public Rectangle2D getWindowBounds() {
return windowBounds;
}
/**
* Set the new window bounds.
*
* @param bounds The new window bounds.
*/
public void setWindowBounds(Rectangle2D bounds) {
this.windowBounds = bounds;
}
@Override
public int hashCode() {
return 31 + ((windowClass == null) ? 0 : windowClass.hashCode());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
WindowConfiguration other = (WindowConfiguration) obj;
if (windowClass == null) {
if (other.windowClass != null) {
return false;
}
}
else if (!windowClass.equals(other.windowClass)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* ConfigurationService implementation for loading and saving configuration
* files in the YAML format.
*
* @param <T> The type of the configuration.
*
* @author Alex Andres
*/
public class YamlConfigurationService<T> implements ConfigurationService<T> {
/**
* The object mapper that configures the conversion to and from the YAML
* format.
*/
private final ObjectMapper mapper;
/**
* Create a new YamlConfigurationService instance.
*/
public YamlConfigurationService() {
mapper = new ObjectMapper(new YAMLFactory());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public T load(File file, Class<T> cls) throws IOException {
T config = null;
InputStream input = null;
try {
if (file.exists()) {
input = new FileInputStream(file);
}
else {
input = getClass().getResourceAsStream(file.getPath().replace("\\", "/"));
}
if (input == null) {
throw new IOException("Unable to load configuration file. File '" + file.getAbsolutePath() + "' does not exist.");
}
config = mapper.readValue(input, cls);
}
catch (IOException e) {
throw e;
}
finally {
if (input != null) {
try {
input.close();
}
catch (Exception e) {
throw e;
}
}
}
return config;
}
@Override
public void save(File file, T config) throws IOException {
mapper.writeValue(file, config);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration.bind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.audio.AudioFormat.Encoding;
/**
* Implementation of an {@link AudioFormat} JSON deserializer.
*
* @author Alex Andres
*/
public class AudioFormatDeserializer extends JsonDeserializer<AudioFormat> {
@Override
public AudioFormat deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
AudioFormat format;
try {
int samplerate = node.get("samplerate").intValue();
int channels = node.get("channels").intValue();
Encoding encoding = Encoding.valueOf(node.get("encoding").textValue());
format = new AudioFormat(encoding, samplerate, channels);
}
catch (Exception e) {
throw new IOException("Deserialize audio format failed.", e);
}
return format;
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration.bind;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.lecturestudio.core.audio.AudioFormat;
/**
* Implementation of an {@link AudioFormat} JSON serializer.
*
* @author Alex Andres
*/
public class AudioFormatSerializer extends JsonSerializer<AudioFormat> {
@Override
public void serialize(AudioFormat format, JsonGenerator generator, SerializerProvider serializers) throws IOException {
generator.writeStartObject();
generator.writeNumberField("samplerate", format.getSampleRate());
generator.writeNumberField("channels", format.getChannels());
generator.writeStringField("encoding", format.getEncoding().toString());
generator.writeEndObject();
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration.bind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.util.Calendar;
/**
* Implementation of an {@link Calendar} JSON deserializer.
*
* @author Alex Andres
*/
public class CalendarDeserializer extends JsonDeserializer<Calendar> {
@Override
public Calendar deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (!node.canConvertToLong()) {
throw new IOException("Deserialize calendar failed.");
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(node.longValue());
return calendar;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration.bind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import org.lecturestudio.core.graphics.Color;
/**
* Implementation of an {@link Color} JSON deserializer.
*
* @author Alex Andres
*/
public class ColorDeserializer extends JsonDeserializer<Color> {
@Override
public Color deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (!node.canConvertToInt()) {
throw new IOException("Deserialize color failed.");
}
return new Color(node.asInt());
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration.bind;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import org.lecturestudio.core.graphics.Color;
/**
* Implementation of an {@link Color} JSON serializer.
*
* @author Alex Andres
*/
public class ColorSerializer extends JsonSerializer<Color> {
@Override
public void serialize(Color color, JsonGenerator generator, SerializerProvider serializers) throws IOException {
generator.writeNumber(color.getRGBA());
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.configuration.bind;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.lecturestudio.core.geometry.Point2D;
import org.lecturestudio.core.geometry.Rectangle2D;
/**
* A {@link Rectangle2D} mixin-class to be registered with the FasterXML/jackson
* ObjectMapper. This annotation class defines methods to be ignored while
* serializing and deserializing Rectangle2D objects.
*
* @author Alex Andres
*/
public abstract class Rectangle2DMixin {
@JsonIgnore
abstract Point2D getLocation();
@JsonIgnore
abstract boolean isEmpty();
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.dictionary;
/**
* The Dictionary provides an interface to a string translation dictionary. The
* dictionary format and storage method depends on the implementation.
*
* @author Alex Andres
*/
public interface Dictionary {
/**
* Returns the value to which the key is mapped in this dictionary. If the
* dictionary contains an entry for the specified key, the associated value
* is returned, otherwise {@code null} is returned.
*
* @param key A key in the dictionary.
*
* @return the value associated with the specified key.
*
* @throws NullPointerException If the key is {@code null}.
*/
String get(String key) throws NullPointerException;
/**
* Checks the dictionary for an existing key.
*
* @param key A key in the dictionary.
*
* @return true if the dictionary contains an entry for the specified key,
* false otherwise.
*/
boolean contains(String key);
}

View file

@ -0,0 +1,204 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.dictionary;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Dictionary implementation for loading dictionary translation files in the
* properties format.
*
* @see ResourceBundle
*
* @author Alex Andres
*/
public class PropertyDictionary implements Dictionary {
private static final Logger LOG = LogManager.getLogger(PropertyDictionary.class);
/** The resource bundle containing locale-specific translations. */
private ResourceBundle bundle;
/** The locale of this dictionary. */
private Locale locale;
/** The dictionary file paths. */
private String[] dictPaths;
/**
* Creates a new instance of {@code PropertyDictionary} and loads the
* dictionary file for the specified locale.
*
* @param locale The dictionary localization.
* @param dictionaryPath One or more paths to dictionary files.
*
* @throws NullPointerException If the dictionary path is {@code null}.
*/
public PropertyDictionary(Locale locale, String... dictionaryPath) {
if (dictionaryPath == null) {
throw new NullPointerException("Dictionary path must be set.");
}
this.dictPaths = dictionaryPath;
setLocale(locale);
}
/**
* Returns the value to which the key is mapped in this dictionary. If the
* dictionary contains an entry for the specified key, the associated value
* is returned, otherwise the string {@code [*]} is returned.
*/
@Override
public String get(String key) throws NullPointerException {
try {
return bundle.getString(key);
}
catch (MissingResourceException e) {
LOG.warn("Missing resource translation", e);
return "[*]";
}
}
/**
* Returns the current locale of the dictionary.
*
* @return the current locale.
*/
public Locale getLocale() {
return locale;
}
/**
* Sets the locale of the dictionary. If the new locale differs from the
* current one, the new dictionary file is loaded.
*
* @param locale The new locale.
*/
public void setLocale(Locale locale) {
if (this.locale != null && this.locale.equals(locale)) {
return;
}
List<ResourceBundle> bundles = new ArrayList<>();
for (String path : dictPaths) {
ResourceBundle bundle = ResourceBundle.getBundle(path, locale);
if (bundle != null) {
bundles.add(bundle);
}
}
this.locale = locale;
this.bundle = new AggregateBundle(bundles);
}
@Override
public boolean contains(String key) {
return bundle.containsKey(key);
}
/**
* A ResourceBundle whose content is aggregated from multiple bundles.
*/
private static class AggregateBundle extends ResourceBundle {
private final Map<String, Object> contents = new HashMap<>();
/**
* Creates a new AggregateBundle with the contents of the specified
* resource bundles.
*
* @param bundles A list of bundles which shall be merged into this
* bundle.
*/
AggregateBundle(List<ResourceBundle> bundles) {
for (ResourceBundle bundle : bundles) {
Enumeration<String> keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
if (!contents.containsKey(key)) {
contents.put(key, bundle.getObject(key));
}
}
}
}
@Override
public Enumeration<String> getKeys() {
return new IteratorEnumeration<>(contents.keySet().iterator());
}
@Override
protected Object handleGetObject(String key) {
return contents.get(key);
}
}
/**
* An Enumeration implementation that wraps an Iterator.
*
* @param <T> The enumerated type.
*/
private static class IteratorEnumeration<T> implements Enumeration<T> {
private final Iterator<T> source;
/**
* Creates a new IterationEnumeration.
*
* @param source The source iterator.
*/
IteratorEnumeration(Iterator<T> source) {
this.source = source;
}
@Override
public boolean hasMoreElements() {
return source.hasNext();
}
@Override
public T nextElement() {
return source.next();
}
}
}

View file

@ -0,0 +1,165 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.app.view;
import java.awt.Container;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import org.lecturestudio.core.geometry.Rectangle2D;
import org.lecturestudio.core.view.Screen;
/**
* Screen helper class to retrieve information about connected graphic output
* devices.
*
* @author Alex Andres
*/
public final class Screens {
/** Local Graphics Environment. */
private static final GraphicsEnvironment GE = GraphicsEnvironment.getLocalGraphicsEnvironment();
/**
* Get all connected screens including the primary screen.
*
* @return an array of all connected screens.
*/
public static Screen[] getAllScreens() {
GraphicsDevice[] devices = getScreenDevices();
Screen[] screens = new Screen[devices.length];
for (int i = 0; i < screens.length; i++) {
GraphicsDevice device = devices[i];
Rectangle bounds = device.getDefaultConfiguration().getBounds();
screens[i] = new Screen(bounds.x, bounds.y, bounds.width, bounds.height);
}
return screens;
}
/**
* Get all connected screens without the primary screen.
*
* @return an array of all connected screens.
*/
public static Screen[] getConnectedScreens() {
Rectangle defaultBounds = GE.getDefaultScreenDevice().getDefaultConfiguration().getBounds();
GraphicsDevice[] devices = getScreenDevices();
Screen[] screens = new Screen[devices.length - 1];
for (int i = 0, c = 0; i < devices.length; i++) {
GraphicsDevice device = devices[i];
Rectangle bounds = device.getDefaultConfiguration().getBounds();
if (!bounds.equals(defaultBounds)) {
screens[c++] = new Screen(bounds.x, bounds.y, bounds.width, bounds.height);
}
}
return screens;
}
/**
* Get all connected screen devices.
*
* @return an array of screen devices.
*/
public static GraphicsDevice[] getScreenDevices() {
return GE.getScreenDevices();
}
/**
* Get the default (primary) screen device.
*
* @return default screen device.
*/
public static GraphicsDevice getDefaultScreenDevice() {
return GE.getDefaultScreenDevice();
}
/**
* Get a screen device which contains the specified container. If the
* container has not been placed yet on any screen the default screen device
* is returned. In case the container cannot be assigned to any screen, then
* {@code null} is returned. The decision to choose the screen is based on
* the area that the container covers on the corresponding screen.
*
* @param container A component container.
*
* @return a screen device or {@code null}.
*/
public static GraphicsDevice getScreenDevice(Container container) {
Rectangle bounds = container.getBounds();
if (bounds.isEmpty()) {
return getDefaultScreenDevice();
}
// Covered area of the container on the screen.
int area = 0;
GraphicsDevice result = null;
GraphicsDevice[] devices = getScreenDevices();
for (GraphicsDevice device : devices) {
Rectangle screenSize = device.getDefaultConfiguration().getBounds();
Rectangle coveredSize = screenSize.intersection(bounds);
int coveredArea = coveredSize.width * coveredSize.height;
if (coveredArea > area) {
area = coveredArea;
result = device;
}
}
return result;
}
/**
* Get {@code GraphicsConfiguration} associated with a graphics device which
* has the bounds defined by the provided screen.
*
* @param screen The screen for which to obtain the GraphicsConfiguration.
*
* @return a GraphicsConfiguration or {@code null}.
*/
public static GraphicsConfiguration getGraphicsConfiguration(Screen screen) {
GraphicsDevice[] devices = GE.getScreenDevices();
for (GraphicsDevice device : devices) {
Rectangle bounds = device.getDefaultConfiguration().getBounds();
Rectangle screenBounds = toAwtRectangle(screen.getBounds());
if (bounds.equals(screenBounds)) {
return device.getDefaultConfiguration();
}
}
return null;
}
private static Rectangle toAwtRectangle(Rectangle2D r) {
return new Rectangle((int) r.getX(), (int) r.getY(), (int) r.getWidth(), (int) r.getHeight());
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import org.lecturestudio.core.ExecutableException;
/**
* This exception is thrown to indicate a problem while operating with an audio
* device. Throwing this exception should be considered fatal to the operation
* of the application.
*
* @author Alex Andres
*/
public class AudioDeviceNotConnectedException extends ExecutableException {
private final String deviceName;
/**
* Construct a new AudioDeviceNotConnectedException with no other
* information.
*
* @param deviceName The name of the device.
*/
public AudioDeviceNotConnectedException(String deviceName) {
super();
this.deviceName = deviceName;
}
/**
* Construct a new AudioDeviceNotConnectedException with the specified
* message.
*
* @param message A Message describing this exception.
* @param deviceName The name of the device.
*/
public AudioDeviceNotConnectedException(String message, String deviceName) {
super(message);
this.deviceName = deviceName;
}
/**
* Construct a new AudioDeviceNotConnectedException with the specified
* formatted message.
*
* @param message A Message describing this exception.
* @param deviceName The name of the device.
* @param args Arguments used in the formatted message.
*/
public AudioDeviceNotConnectedException(String message, String deviceName,
Object... args) {
super(String.format(message, args));
this.deviceName = deviceName;
}
/**
* Construct a new AudioDeviceNotConnectedException with the specified
* throwable.
*
* @param throwable A Throwable that caused this exception.
* @param deviceName The name of the device.
*/
public AudioDeviceNotConnectedException(Throwable throwable,
String deviceName) {
super(throwable);
this.deviceName = deviceName;
}
/**
* Construct a new AudioDeviceNotConnectedException with the specified
* message and throwable.
*
* @param message A Message describing this exception.
* @param throwable A Throwable that caused this exception.
* @param deviceName The name of the device.
*/
public AudioDeviceNotConnectedException(String message, Throwable throwable,
String deviceName) {
super(message, throwable);
this.deviceName = deviceName;
}
/**
* Construct a new AudioDeviceNotConnectedException with the specified
* formatted message and provided throwable.
*
* @param message A Message describing this exception.
* @param throwable A Throwable that caused this exception.
* @param deviceName The name of the device.
* @param args Arguments used in the formatted message.
*/
public AudioDeviceNotConnectedException(String message, Throwable throwable,
String deviceName, Object... args) {
super(String.format(message, args), throwable);
this.deviceName = deviceName;
}
/**
* @return The name of the device that is not connected.
*/
public String getDeviceName() {
return deviceName;
}
}

View file

@ -0,0 +1,181 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import java.io.Serializable;
import java.util.Objects;
/**
* The AudioFormat specifies a particular arrangement of audio data in a audio
* source or in a audio sink. Having an audio format gives the ability to
* interpret the bits in the binary audio data stream.
*
* @author Alex Andres
*/
public class AudioFormat implements Serializable {
/**
* The encoding specifies the convention used to represent the audio data.
*/
public enum Encoding {
/** Signed 16 Bit PCM, little endian. */
S16LE,
/** Signed 16 Bit PCM, big endian. */
S16BE,
/** Signed 24 Bit PCM, little endian. */
S24LE,
/** Signed 24 Bit PCM, big endian. */
S24BE,
/** Signed 32 Bit PCM, little endian. */
S32LE,
/** Signed 32 Bit PCM, big endian. */
S32BE,
/** 32 Bit IEEE floating point, little endian, range -1.0 to 1.0 */
FLOAT32LE,
/** 32 Bit IEEE floating point, big endian, range -1.0 to 1.0 */
FLOAT32BE
};
/** The encoding of this audio format. */
private Encoding encoding;
/** The sample rate of this audio format. */
private int sampleRate;
/** The number of channels of this audio format. */
private int channels;
/**
* Creates an uninitialized AudioFormat.
*/
public AudioFormat() {
}
/**
* Creates an AudioFormat with the given parameters.
*
* @param encoding The audio encoding.
* @param sampleRate The number of samples per second.
* @param channels The number of channels (1 for mono, 2 for stereo, and
* so on).
*/
public AudioFormat(Encoding encoding, int sampleRate, int channels) {
this.encoding = encoding;
this.sampleRate = sampleRate;
this.channels = channels;
}
/**
* Returns the type of encoding for audio in this format.
*
* @return the audio encoding.
*/
public Encoding getEncoding() {
return encoding;
}
/**
* Returns the number of samples per second.
*
* @return the sample rate.
*/
public int getSampleRate() {
return sampleRate;
}
/**
* Returns the number of channels (1 for mono, 2 for stereo, etc.).
*
* @return the number of channels.
*/
public int getChannels() {
return channels;
}
/**
* Returns the number of bytes in each sample.
*
* @return the size of a sample in bytes.
*/
public int getBytesPerSample() {
return getBitsPerSample() / 8;
}
/**
* Returns the number of bits of each sample.
*
* @return the size of a sample in bits.
*/
public int getBitsPerSample() {
switch (encoding) {
case S16LE:
case S16BE:
return 16;
case S24LE:
case S24BE:
return 24;
case S32LE:
case S32BE:
case FLOAT32LE:
case FLOAT32BE:
return 32;
default:
return 0;
}
}
@Override
public int hashCode() {
return Objects.hash(sampleRate, sampleRate, encoding);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AudioFormat other = (AudioFormat) obj;
if (channels != other.channels) {
return false;
}
if (encoding != other.encoding) {
return false;
}
return sampleRate == other.sampleRate;
}
@Override
public String toString() {
return encoding.toString() + ", " + sampleRate + ", " + channels;
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import org.lecturestudio.core.model.Time;
/**
* The AudioPlaybackProgressListener is notified each time an audio player has
* processed and pushed audio samples to a audio playback device.
*
* @author Alex Andres
*/
@FunctionalInterface
public interface AudioPlaybackProgressListener {
/**
* Called when progress of the current playing audio changes.
*
* @param progressMs The current progress in milliseconds.
* @param durationMs The total duration of audio in milliseconds.
*/
void onAudioProgress(Time progressMs, Time durationMs);
}

View file

@ -0,0 +1,273 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import org.lecturestudio.core.ExecutableBase;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.ExecutableState;
import org.lecturestudio.core.ExecutableStateListener;
import org.lecturestudio.core.audio.device.AudioOutputDevice;
import org.lecturestudio.core.audio.source.AudioSource;
import org.lecturestudio.core.model.Time;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Default audio player implementation.
*
* @author Alex Andres
*/
public class AudioPlayer extends ExecutableBase implements Player {
private static final Logger LOG = LogManager.getLogger(AudioPlayer.class);
/** The sync state that is shared with other media players. */
private final SyncState syncState;
/** The audio playback device. */
private final AudioOutputDevice playbackDevice;
/** The audio source. */
private final AudioSource audioSource;
/** The audio source size. */
private final long inputSize;
/** The playback progress listener. */
private AudioPlaybackProgressListener progressListener;
/** The player state listener. */
private ExecutableStateListener stateListener;
/** The playback thread. */
private Thread thread;
/** The current audio source reading position. */
private long inputPos;
/**
* Create an AudioPlayer with the specified playback device and source. The
* sync state is shared with other media players to keep different media
* sources in sync while playing.
*
* @param device The audio playback device.
* @param source The audio source.
* @param syncState The shared sync state.
*
* @throws Exception If the audio player failed to initialize.
*/
public AudioPlayer(AudioOutputDevice device, AudioSource source, SyncState syncState) throws Exception {
if (isNull(device)) {
throw new NullPointerException("Missing audio playback device.");
}
if (isNull(source)) {
throw new NullPointerException("Missing audio source.");
}
this.playbackDevice = device;
this.audioSource = source;
this.syncState = syncState;
this.inputSize = source.getInputSize();
}
@Override
public void setVolume(float volume) {
if (volume < 0 || volume > 1) {
return;
}
playbackDevice.setVolume(volume);
}
@Override
public void seek(int timeMs) throws Exception {
AudioFormat format = audioSource.getAudioFormat();
float bytesPerSecond = AudioUtils.getBytesPerSecond(format);
int skipBytes = Math.round(bytesPerSecond * timeMs / 1000F);
audioSource.reset();
audioSource.skip(skipBytes);
inputPos = skipBytes;
syncState.setAudioTime((long) (inputPos / (bytesPerSecond / 1000f)));
}
@Override
public void setProgressListener(AudioPlaybackProgressListener listener) {
this.progressListener = listener;
}
@Override
public void setStateListener(ExecutableStateListener listener) {
this.stateListener = listener;
}
@Override
protected void initInternal() throws ExecutableException {
try {
audioSource.reset();
}
catch (Exception e) {
throw new ExecutableException("Audio device could not be initialized.", e);
}
if (!playbackDevice.supportsAudioFormat(audioSource.getAudioFormat())) {
throw new ExecutableException("Audio device does not support the needed audio format.");
}
try {
playbackDevice.setAudioFormat(audioSource.getAudioFormat());
playbackDevice.open();
playbackDevice.start();
}
catch (Exception e) {
throw new ExecutableException("Audio device could not be initialized.", e);
}
}
@Override
protected void startInternal() throws ExecutableException {
if (getPreviousState() == ExecutableState.Suspended) {
synchronized (thread) {
thread.notify();
}
}
else {
thread = new Thread(new AudioReaderTask(), getClass().getSimpleName());
thread.start();
}
}
@Override
protected void stopInternal() throws ExecutableException {
try {
audioSource.reset();
}
catch (Exception e) {
throw new ExecutableException(e);
}
inputPos = 0;
syncState.reset();
}
@Override
protected void destroyInternal() throws ExecutableException {
try {
playbackDevice.close();
audioSource.close();
}
catch (Exception e) {
throw new ExecutableException(e);
}
}
@Override
protected void fireStateChanged() {
if (nonNull(stateListener)) {
stateListener.onExecutableStateChange(getPreviousState(), getState());
}
}
private void onProgress(Time progress, Time duration, long progressMs) {
if (nonNull(syncState)) {
syncState.setAudioTime(progressMs);
}
if (nonNull(progressListener) && started()) {
progress.setMillis(progressMs);
progressListener.onAudioProgress(progress, duration);
}
}
private class AudioReaderTask implements Runnable {
@Override
public void run() {
byte[] buffer = new byte[playbackDevice.getBufferSize()];
int bytesRead;
// Calculate bytes per millisecond.
float bpms = AudioUtils.getBytesPerSecond(audioSource.getAudioFormat()) / 1000f;
Time progress = new Time(0);
Time duration = new Time((long) (inputSize / bpms));
ExecutableState state;
while (true) {
state = getState();
if (state == ExecutableState.Started) {
try {
bytesRead = audioSource.read(buffer, 0, buffer.length);
if (bytesRead > 0) {
playbackDevice.write(buffer, 0, bytesRead);
inputPos += bytesRead;
onProgress(progress, duration, (long) (inputPos / bpms));
}
else if (bytesRead == -1) {
// EOM
break;
}
}
catch (Exception e) {
LOG.error("Play audio failed.", e);
break;
}
}
else if (state == ExecutableState.Suspended) {
synchronized (thread) {
try {
thread.wait();
}
catch (Exception e) {
// Ignore
}
}
}
else if (state == ExecutableState.Stopped) {
return;
}
}
// EOM
try {
stop();
}
catch (ExecutableException e) {
LOG.error("Stop " + getClass().getName() + " failed.", e);
}
}
}
}

View file

@ -0,0 +1,225 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import java.security.InvalidParameterException;
import org.lecturestudio.core.ExecutableBase;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.ExecutableState;
import org.lecturestudio.core.ExecutableStateListener;
import org.lecturestudio.core.audio.device.AudioOutputDevice;
import org.lecturestudio.core.audio.io.AudioPlaybackBuffer;
import org.lecturestudio.core.io.PlaybackData;
import org.lecturestudio.core.net.Synchronizer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Extended audio player implementation.
*
* @author Alex Andres
*/
public class AudioPlayerExt extends ExecutableBase implements Player {
private static final Logger LOG = LogManager.getLogger(AudioPlayerExt.class);
/** The audio playback device. */
private AudioOutputDevice playbackDevice;
/** The audio source. */
private AudioPlaybackBuffer audioSource;
/** The player state listener. */
private ExecutableStateListener stateListener;
/** The playback thread. */
private Thread thread;
/**
* Create an AudioPlayerExt with the specified playback device and audio
* buffer.
*
* @param device The audio playback device.
* @param buffer The audio source.
*/
public AudioPlayerExt(AudioOutputDevice device, AudioPlaybackBuffer buffer) {
if (isNull(device)) {
throw new NullPointerException("Missing audio playback device.");
}
if (isNull(buffer)) {
throw new NullPointerException("Missing audio buffer source.");
}
this.playbackDevice = device;
this.audioSource = buffer;
}
@Override
public void setVolume(float volume) {
if (volume < 0 || volume > 1) {
throw new InvalidParameterException("Volume value should be within 0 and 1.");
}
playbackDevice.setVolume(volume);
}
@Override
public void seek(int time) {
audioSource.skip(time);
}
@Override
public void setProgressListener(AudioPlaybackProgressListener listener) {
}
@Override
public void setStateListener(ExecutableStateListener listener) {
this.stateListener = listener;
}
@Override
protected void initInternal() throws ExecutableException {
audioSource.reset();
AudioFormat format = audioSource.getAudioFormat();
if (!playbackDevice.supportsAudioFormat(format)) {
throw new ExecutableException("Audio device does not support the needed audio format.");
}
try {
playbackDevice.setAudioFormat(format);
playbackDevice.open();
playbackDevice.start();
}
catch (Exception e) {
throw new ExecutableException(e);
}
}
@Override
protected void startInternal() throws ExecutableException {
if (getPreviousState() == ExecutableState.Suspended) {
synchronized (thread) {
thread.notify();
}
}
else {
thread = new Thread(new AudioReaderTask(), getClass().getSimpleName());
thread.start();
}
}
@Override
protected void stopInternal() throws ExecutableException {
try {
synchronized (thread) {
thread.interrupt();
}
audioSource.reset();
}
catch (Exception e) {
throw new ExecutableException("Audio source could not be reset.", e);
}
}
@Override
protected void destroyInternal() throws ExecutableException {
try {
playbackDevice.close();
audioSource.reset();
}
catch (Exception e) {
throw new ExecutableException(e);
}
}
@Override
protected void fireStateChanged() {
if (nonNull(stateListener)) {
stateListener.onExecutableStateChange(getPreviousState(), getState());
}
}
private class AudioReaderTask implements Runnable {
@Override
public void run() {
int bytesRead;
ExecutableState state;
while (playbackDevice.isOpen()) {
state = getState();
if (state == ExecutableState.Started) {
try {
PlaybackData<byte[]> samples = audioSource.take();
if (nonNull(samples)) {
bytesRead = samples.getData().length;
Synchronizer.setAudioTime(samples.getTimestamp());
if (bytesRead > 0 && playbackDevice.isOpen()) {
playbackDevice.write(samples.getData(), 0, bytesRead); // TODO check deviceBufferSize
}
}
}
catch (Exception e) {
LOG.error("Play audio failed.", e);
break;
}
}
else if (state == ExecutableState.Suspended) {
synchronized (thread) {
try {
thread.wait();
}
catch (Exception e) {
// Ignore
}
}
}
else if (state == ExecutableState.Stopped) {
return;
}
}
try {
playbackDevice.stop();
playbackDevice.close();
}
catch (Exception e) {
LOG.error("Stop audio playback device failed.", e);
}
}
}
}

View file

@ -0,0 +1,215 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import static java.util.Objects.isNull;
import java.util.Arrays;
import org.lecturestudio.core.audio.codec.AudioCodecLoader;
import org.lecturestudio.core.audio.device.AudioInputDevice;
import org.lecturestudio.core.audio.device.AudioOutputDevice;
import org.lecturestudio.core.audio.system.AudioSystemLoader;
import org.lecturestudio.core.audio.system.AudioSystemProvider;
/**
* Audio-related utility methods.
*
* @author Alex Andres
*/
public class AudioUtils {
private static final AudioSystemLoader LOADER = AudioSystemLoader.getInstance();
public static AudioInputDevice getDefaultAudioCaptureDevice(String providerName) {
AudioSystemProvider provider = LOADER.getProvider(providerName);
return isNull(provider) ? null : provider.getDefaultInputDevice();
}
public static AudioOutputDevice getDefaultAudioPlaybackDevice(String providerName) {
AudioSystemProvider provider = LOADER.getProvider(providerName);
return isNull(provider) ? null : provider.getDefaultOutputDevice();
}
public static AudioInputDevice[] getAudioCaptureDevices(String providerName) {
AudioSystemProvider provider = LOADER.getProvider(providerName);
return isNull(provider) ? new AudioInputDevice[0] : provider.getInputDevices();
}
public static AudioOutputDevice[] getAudioPlaybackDevices(String providerName) {
AudioSystemProvider provider = LOADER.getProvider(providerName);
return isNull(provider) ? new AudioOutputDevice[0] : provider.getOutputDevices();
}
public static boolean hasAudioCaptureDevice(String providerName, String deviceName) {
if (isNull(deviceName)) {
return false;
}
return Arrays.stream(getAudioCaptureDevices(providerName))
.anyMatch(device -> device.getName().equals(deviceName));
}
/**
* Get an AudioInputDevice with the specified device name that is registered
* with the given audio system provider.
*
* @param providerName The audio system provider name.
* @param deviceName The audio capture device name.
*
* @return the retrieved AudioInputDevice or null, if the capture device
* could not be found.
*/
public static AudioInputDevice getAudioInputDevice(String providerName, String deviceName) {
AudioSystemProvider provider = LOADER.getProvider(providerName);
if (isNull(provider)) {
throw new NullPointerException("Audio provider is not available: " + providerName);
}
AudioInputDevice inputDevice = provider.getInputDevice(deviceName);
if (isNull(inputDevice)) {
throw new NullPointerException("Audio device is not available: " + deviceName);
}
return inputDevice;
}
/**
* Get an AudioOutputDevice with the specified device name that is
* registered with the given audio system provider.
*
* @param providerName The audio system provider name.
* @param deviceName The audio playback device name.
*
* @return the retrieved outputDeviceName or null, if the playback device
* could not be found.
*/
public static AudioOutputDevice getAudioOutputDevice(String providerName, String deviceName) {
AudioSystemProvider provider = LOADER.getProvider(providerName);
if (isNull(provider)) {
provider = LOADER.getProvider("Java Sound");
}
AudioOutputDevice outputDevice = provider.getOutputDevice(deviceName);
if (outputDevice == null) {
// Get next best device.
for (AudioOutputDevice device : provider.getOutputDevices()) {
if (device != null) {
return device;
}
}
}
return outputDevice;
}
/**
* Retrieve all supported audio codecs by the system.
*
* @param providerName The name of the audio system provider.
*
* @return an array of names of supported audio codecs.
*/
public static String[] getSupportedAudioCodecs(String providerName) {
return AudioCodecLoader.getInstance().getProviderNames();
}
/**
* Compute the number of bytes per second that the specified audio format
* will require.
*
* @param audioFormat The audio format.
*
* @return The required number of bytes per second.
*/
public static int getBytesPerSecond(AudioFormat audioFormat) {
return Math.round(audioFormat.getSampleRate() * audioFormat
.getBytesPerSample() * audioFormat.getChannels());
}
/**
* Pack two sequential bytes into a {@code short} value according to the
* specified endianness.
*
* @param bytes The bytes to pack, must of size 2.
* @param bigEndian True to pack with big-endian order, false to pack with
* little-endian order.
*
* @return two packed bytes as an {@code short} value.
*/
public static int toShort(byte[] bytes, boolean bigEndian) {
if (bytes.length == 1) {
return bytes[0];
}
if (bigEndian) {
return ((bytes[0] << 8) | (0xff & bytes[1]));
}
else {
return ((bytes[1] << 8) | (0xff & bytes[0]));
}
}
/**
* Convert the specified normalized float value to an short value.
*
* @param value The normalized float value to convert.
*
* @return an short value.
*/
public static int toShort(double value) {
return (int) (value * Short.MAX_VALUE);
}
/**
* Convert the specified integer value to a normalized float value in the
* range of [0,1].
*
* @param value The integer value to convert.
* @param frameSize The sample size in bytes.
* @param signed True to respect the sign bit, false otherwise.
*
* @return a normalized float value.
*/
public static float getNormalizedSampleValue(int value, int frameSize, boolean signed) {
float relValue;
int maxValue;
if (signed) {
maxValue = (frameSize == 2) ? Short.MAX_VALUE : Byte.MAX_VALUE;
}
else {
maxValue = (frameSize == 2) ? 0xffff : 0xff;
}
relValue = (float) value / maxValue;
return relValue;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import org.lecturestudio.core.Executable;
import org.lecturestudio.core.ExecutableStateListener;
/**
* Common interface to provide a consistent mechanism for media players.
*
* @author Alex Andres
*/
public interface Player extends Executable {
/**
* Set the audio volume for playback. The volume value must be in the range
* of [0,1].
*
* @param volume The new volume value.
*/
void setVolume(float volume);
/**
* Jump to the specified time position in the audio playback stream.
*
* @param timeMs The absolute time in milliseconds to jump to.
*
* @throws Exception If the playback stream failed to read the start of the
* specified position.
*/
void seek(int timeMs) throws Exception;
/**
* Set the playback progress listener.
*
* @param listener The listener to set.
*/
void setProgressListener(AudioPlaybackProgressListener listener);
/**
* Set the state listener.
*
* @param listener The listener to set.
*/
void setStateListener(ExecutableStateListener listener);
}

View file

@ -0,0 +1,254 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.lecturestudio.core.audio.sink.AudioSink;
import org.lecturestudio.core.audio.source.AudioSource;
/**
* Audio ring-buffer implementation that acts both as an audio sink and as an
* audio source. One can write and read the buffer at the same time.
*
* @author Alex Andres
*/
public class RingBuffer implements AudioSink, AudioSource {
/** Internal data storage. **/
private final ByteBuffer buffer;
/** The audio format of audio samples in this buffer. **/
private AudioFormat audioFormat;
/** Position from where to start reading from the buffer. **/
private int readPointer;
/** Position from where to start writing into the buffer. **/
private int writePointer;
/** The number of bytes written to the buffer. */
private int bytesToWrite;
/** The number of bytes read from the buffer. */
private int bytesToRead;
/**
* Creates an RingBuffer with a default buffer size of 1024 bytes.
*/
public RingBuffer() {
this(1024);
}
/**
* Creates an RingBuffer with the specified buffer size.
*
* @param capacity buffer size in bytes.
*/
public RingBuffer(int capacity) {
if (capacity < 2) {
throw new IllegalArgumentException("Buffer should have at least the capacity of 2 bytes.");
}
this.buffer = ByteBuffer.allocateDirect(capacity);
clear();
}
/**
* Resets the read and write pointers. The internal buffer remains
* unaffected.
*/
public synchronized void clear() {
readPointer = writePointer = 0;
bytesToRead = 0;
bytesToWrite = buffer.capacity();
}
/**
* Return the readable amount of bytes in buffer. Note: It is not
* necessarily valid when data is written to the buffer or read from the
* buffer. Another thread might have filled the buffer or emptied it in the
* mean time.
*
* @return currently available bytes to read.
*/
public int available() {
return bytesToRead;
}
/**
* Write as much data as possible to the buffer.
*
* @param data The data to be written.
*
* @return the number of bytes actually written.
*/
public synchronized int write(byte[] data) {
return write(data, 0, data.length);
}
/**
* Write as much data as possible to the buffer.
*
* @param data The array holding data to be written.
* @param offset The offset where to start in the array.
* @param length The number of bytes to write, starting from the offset.
*
* @return the number of bytes actually written.
*/
public synchronized int write(byte[] data, int offset, int length) {
if (offset < 0 || length < 0 || length > data.length - offset) {
throw new IndexOutOfBoundsException();
}
if (bytesToWrite == 0) {
return 0;
}
if (bytesToWrite < length) {
length = bytesToWrite;
}
buffer.position(writePointer);
int partLength = buffer.capacity() - writePointer;
if (partLength > length) {
buffer.put(data, offset, length);
writePointer += length;
}
else {
buffer.put(data, offset, partLength);
buffer.position(0);
buffer.put(data, offset + partLength, length - partLength);
writePointer = length - partLength;
}
bytesToRead += length;
bytesToWrite -= length;
return length;
}
/**
* Read as much data as possible from the buffer.
*
* @param data Where to store the data.
*
* @return the number of bytes read.
*/
public synchronized int read(byte[] data) {
return read(data, 0, data.length);
}
/**
* Read as much data as possible from the buffer.
*
* @param data Where to store the read data.
* @param offset The offset where to start in the array.
* @param length The number of bytes to read.
*
* @return the number of bytes read.
*/
public synchronized int read(byte[] data, int offset, int length) {
if (offset < 0 || length < 0 || length > data.length - offset) {
throw new IndexOutOfBoundsException();
}
if (bytesToRead == 0) {
return 0;
}
if (bytesToRead < length) {
length = bytesToRead;
}
buffer.position(readPointer);
int partLength = buffer.capacity() - readPointer;
if (partLength > length) {
buffer.get(data, offset, length);
readPointer += length;
}
else {
buffer.get(data, offset, partLength);
buffer.position(0);
buffer.get(data, partLength, length - partLength);
readPointer = length - partLength;
}
bytesToRead -= length;
bytesToWrite += length;
return length;
}
@Override
public long skip(long n) {
return 0;
}
@Override
public long getInputSize() {
return 0;
}
@Override
public AudioFormat getAudioFormat() {
return audioFormat;
}
@Override
public void setAudioFormat(AudioFormat format) {
this.audioFormat = format;
}
@Override
public void open() {
}
@Override
public void close() throws IOException {
clear();
}
@Override
public void reset() {
clear();
}
@Override
public synchronized String toString() {
StringBuilder str = new StringBuilder();
String tail;
buffer.position(0);
for (int i = 0; i < buffer.capacity(); i++) {
tail = ((i % 79) == 0) && i != 0 ? "\n" : "";
str.append(buffer.get()).append(" ").append(tail);
}
return str.toString();
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio;
import java.util.concurrent.atomic.AtomicLong;
/**
* The synchronization state for media playback to keep different media players
* in sync. The audio stream is considered to be the master stream and all other
* media stream must be in sync with it.
*
* @author Alex Andres
*/
public final class SyncState {
/** The current audio time in milliseconds. */
private static final AtomicLong audioTime = new AtomicLong(0);
/**
* Set the current audio time in milliseconds of the audio master stream.
*
* @param time The audio time in milliseconds.
*/
public void setAudioTime(long time) {
audioTime.set(time);
}
/**
* Get current audio time in milliseconds of the audio master stream.
*
* @return the audio time in milliseconds.
*/
public long getAudioTime() {
return audioTime.get();
}
/**
* Reset the audio time to 0.
*/
public void reset() {
audioTime.set(0);
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.analysis;
import org.jtransforms.fft.FloatFFT_1D;
/**
* Digital Signal Processing (DSP) helper class used to perform Fast Fourier
* Transform (FFT) computations.
*
* @author Alex Andres
*/
public final class DSP {
/**
* Compute the forward transform of the specified complex data set. The
* input is separated in two parts. The complex input number is stored as
* two float values: the real and imaginary part. Same is applied for the
* computed output values.
*
* @param samples The number of samples.
* @param realIn The real part of data to transform.
* @param imagIn The imaginary part of data to transform.
* @param realOut The computed real part of the transform.
* @param imagOut The computed imaginary part of the transform.
*/
public static void FFT(int samples, float[] realIn, float[] imagIn, float[] realOut, float[] imagOut) {
float[] work = new float[2 * samples];
for (int i = 0; i < 2 * samples; i += 2) {
work[i] = realIn[i >> 1];
work[i + 1] = (imagIn != null) ? imagIn[i >> 1] : 0;
}
FloatFFT_1D fft = new FloatFFT_1D(samples);
fft.complexForward(work);
for (int i = 0; i < 2 * samples; i += 2) {
realOut[i >> 1] = work[i];
imagOut[i >> 1] = work[i + 1];
}
}
/**
* Compute the inverse transform of the specified complex data set. The
* input is separated in two parts. The complex input number is stored as
* two float values: the real and imaginary part. Same is applied for the
* computed output values.
*
* @param samples The number of samples.
* @param realIn The real part of data to transform.
* @param imagIn The imaginary part of data to transform.
* @param realOut The computed real part of the transform.
* @param imagOut The computed imaginary part of the transform.
*/
public static void IFFT(int samples, float[] realIn, float[] imagIn, float[] realOut, float[] imagOut) {
float[] work = new float[2 * samples];
for (int i = 0; i < 2 * samples; i += 2) {
work[i] = realIn[i >> 1];
work[i + 1] = (imagIn != null) ? imagIn[i >> 1] : 0;
}
FloatFFT_1D fft = new FloatFFT_1D(samples);
fft.complexInverse(work, false);
for (int i = 0; i < 2 * samples; i += 2) {
realOut[i >> 1] = (float) (work[i] / samples);
imagOut[i >> 1] = (float) (work[i + 1] / samples);
}
}
/**
* Compute the power spectrum of the specified real sample input data.
*
* @param samples The number of samples.
* @param in The sample input data.
* @param out The computed power spectrum data.
*/
public static void getPowerSpectrum(int samples, float[] in, float[] out) {
float[] work = new float[samples];
int i;
for (i = 0; i < samples; ++i) {
work[i] = in[i];
}
FloatFFT_1D fft = new FloatFFT_1D(samples);
fft.realForward(work);
out[0] = (float) Math.pow(work[0], 2);
for (i = 2; i < samples; i += 2) {
out[i >> 1] = (float) (Math.pow(work[i], 2) + Math.pow(work[i + 1], 2));
}
out[i >> 1] = (float) Math.pow(work[1], 2);
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.analysis;
import static java.lang.StrictMath.PI;
import static java.lang.StrictMath.cos;
/**
* Hann window function implementation.
*
* @author Alex Andres
*/
public class HannWindowFunction extends WindowFunction {
/**
* Create the HannWindowFunction with specified window length.
*
* @param length the length of the window.
*/
public HannWindowFunction(int length) {
super(length);
}
@Override
public void apply(float[] frame) {
if (frame.length != length) {
throw new IllegalArgumentException("Window and frame have different lengths!");
}
for (int i = 0; i < length; i++) {
frame[i] = (float) (frame[i] * multipliers[i]);
}
}
@Override
public void apply(double[] frame) {
if (frame.length != length) {
throw new IllegalArgumentException("Window and frame have different lengths!");
}
for (int i = 0; i < length; i++) {
frame[i] = frame[i] * multipliers[i];
}
}
@Override
protected void createWindow() {
for (int i = 0; i < length; i++) {
multipliers[i] = 0.5 * (1 - cos(2 * PI * i / length));
}
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.analysis;
import java.util.Arrays;
/**
* Abstract implementation of a window function used for signal processing.
*
* @author Alex Andres
*/
public abstract class WindowFunction {
/** The length of the window. */
protected final int length;
/** The window values. */
protected final double[] multipliers;
/**
* Create a new window function.
*
* @param length the window length.
*/
public WindowFunction(int length) {
this.length = length;
this.multipliers = new double[length];
createWindow();
}
/**
* Returns the values of this window function.
*
* @return the values of this window function.
*/
public double[] getValues() {
return Arrays.copyOf(multipliers, multipliers.length);
}
/**
* Copies the values of this window function, truncating or padding with
* zeros (if necessary) so the copy has the specified length.
*
* @param length The length of the copied values to be returned.
*
* @return a copy of the original values of this window function.
*/
public double[] getValues(int length) {
return Arrays.copyOf(multipliers, length);
}
/**
* Normalizes the values of this window function.
*/
public void normalize() {
final int len = multipliers.length;
double sum = 0;
for (double value : multipliers) {
if (!Double.isNaN(value)) {
sum += value;
}
}
for (int i = 0; i < len; i++) {
if (Double.isNaN(multipliers[i])) {
multipliers[i] = Double.NaN;
}
else {
multipliers[i] = multipliers[i] / sum;
}
}
}
/**
* Initialize the window multipliers.
*/
abstract protected void createWindow();
/**
* Apply this window function on the provided frame. The window will be
* applied in-place.
*
* @param frame The input frame.
*/
abstract public void apply(float[] frame);
/**
* Apply this window function on the provided frame. The window will be
* applied in-place.
*
* @param frame The input frame.
*/
abstract public void apply(double[] frame);
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.bus;
import org.lecturestudio.core.bus.EventBus;
import org.lecturestudio.core.bus.event.BusEvent;
/**
* The AudioBus implements the publish-subscribe paradigm. It dispatches audio
* events to the subscribers.
*
* @author Alex Andres
*/
public class AudioBus {
/** The singleton instance. */
private static AudioBus instance = null;
/** The event bus instance. */
private final EventBus bus = new EventBus();
/**
* Meant to be private as for singleton purposes.
*/
private AudioBus() {
}
/**
* Get the AudioBus singleton instance.
*
* @return the AudioBus instance.
*/
private static AudioBus getInstance() {
if (instance == null) {
instance = new AudioBus();
}
return instance;
}
/**
* Register all subscriber methods on the subscriber to receive audio
* events.
*
* @param subscriber The subscriber to register.
*/
public static void register(final Object subscriber) {
getInstance().bus.register(subscriber);
}
/**
* Unregister all subscriber methods on the registered subscriber.
*
* @param subscriber The subscriber to unregister.
*/
public static void unregister(final Object subscriber) {
getInstance().bus.unregister(subscriber);
}
/**
* Publish an audio event to all registered subscribers.
*
* @param event The event to publish.
*/
public static void post(final BusEvent event) {
getInstance().bus.post(event);
}
/**
* Get the EventBus instance used by this AudioBus.
*
* @return the EventBus instance.
*/
public static EventBus get() {
return getInstance().bus;
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.bus.event;
import org.lecturestudio.core.bus.event.BusEvent;
/**
* This event is published when an audio device has been connected or
* disconnected.
*
* @author Alex Andres
*/
public class AudioDeviceHotplugEvent extends BusEvent {
/**
* The event type indicating what kind of the audio device event occurred.
*/
public enum Type {
/** An audio device has been connected. */
Connected,
/** An audio device has been disconnected. */
Disconnected
}
/** The audio device name. */
private String name;
/** The event type. */
private Type type;
/**
* Create a new AudioDeviceHotplugEvent with the given audio device name and
* the event type.
*
* @param name The audio device name.
* @param type The event type.
*/
public AudioDeviceHotplugEvent(String name, Type type) {
this.name = name;
this.type = type;
}
/**
* Get the audio device name.
*
* @return the audio device name.
*/
public String getDeviceName() {
return name;
}
/**
* Get the event type indicating what kind of the audio device event
* occurred.
*
* @return the event type.
*/
public Type getType() {
return type;
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.bus.event;
import org.lecturestudio.core.audio.effect.AudioEffect;
import org.lecturestudio.core.bus.event.BusEvent;
import org.lecturestudio.core.io.DynamicInputStream;
/**
* This event is published when an {@link AudioEffect} state transition occurs
* or the AudioEffect has made progress while processing.
*
* @author Alex Andres
*/
public class AudioEffectEvent extends BusEvent {
/**
* The AudioEffect state type.
*/
public enum State {
/** The AudioEffect has been initialized. */
Initialized,
/** The AudioEffect has been terminated and dropped all processed data. */
Terminated,
/** The AudioEffect has successfully finished the processing. */
Finished,
/** The AudioEffect has made progress while processing. */
Progress,
/** The event contains the processed result of the AudioEffect. */
Result
}
/** The event ID. */
private String id;
/** The AudioEffect state. */
private State state;
/** The AudioEffect progress. */
private float progress;
/** The audio input stream that contains audio data to be processed. */
private DynamicInputStream stream;
/**
* Create an AudioEffectEvent instance with the specified ID and AudioEffect
* state.
*
* @param id The event ID.
* @param state The AudioEffect state.
*/
public AudioEffectEvent(String id, State state) {
this.id = id;
this.state = state;
}
/**
* Get the event ID.
*
* @return the event ID.
*/
public String getId() {
return id;
}
/**
* Set the AudioEffect state.
*
* @param state The AudioEffect state.
*/
public void setState(State state) {
this.state = state;
}
/**
* Get the AudioEffect state.
*
* @return the AudioEffect state.
*/
public State getState() {
return state;
}
/**
* Set the AudioEffect processing progress. The value must be in the range
* of [0,1].
*
* @param progress The progress of the AudioEffect.
*/
public void setProgress(float progress) {
this.progress = progress;
}
/**
* Get the AudioEffect processing progress. The value is in the range of
* [0,1].
*
* @return AudioEffect progress.
*/
public float getProgress() {
return progress;
}
/**
* Get the audio input stream that contains audio data to be processed.
*
* @return the audio input stream.
*/
public DynamicInputStream getInputStream() {
return stream;
}
/**
* Set the audio input stream that contains audio data to be processed.
*
* @param stream The audio input stream to be processed.
*/
public void setInputStream(DynamicInputStream stream) {
this.stream = stream;
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.bus.event;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.bus.event.BusEvent;
/**
* This event is published when audio data has been recorded by an audio capture
* device.
*
* @author Alex Andres
*/
public class AudioRecordedEvent extends BusEvent {
/** The audio format of the recorded audio data. */
private AudioFormat audioFormat;
/** The recorded audio data. */
private byte[] data;
/**
* Create a new AudioRecordedEvent instance with the specified audio data
* and audio format.
*
* @param data The recorded audio data.
* @param audioFormat The audio format of the recorded audio data.
*/
public AudioRecordedEvent(byte[] data, AudioFormat audioFormat) {
this.data = data;
this.audioFormat = audioFormat;
}
/**
* Get the recorded audio data.
*
* @return the recorded audio data.
*/
public byte[] getAudioData() {
return data;
}
/**
* Get the audio format.
*
* @return the audio format.
*/
public AudioFormat getAudioFormat() {
return audioFormat;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.bus.event;
import org.lecturestudio.core.bus.event.BusEvent;
/**
* This event is published when an audio capture device has received audio data
* and computed the audio signal level for the input data.
*
* @author Alex Andres
*/
public class AudioSignalEvent extends BusEvent {
/** The audio signal level in the range of [0,1]. */
private double value;
/**
* Create a AudioSignalEvent instance with the specified audio signal level
* value. The value must be in the range of [0,1].
*
* @param value The audio signal level.
*/
public AudioSignalEvent(double value) {
setSignalValue(value);
}
/**
* Set audio signal level value. The value must be in the range of [0,1].
*
* @param value The audio signal level.
*/
public void setSignalValue(double value) {
this.value = value;
}
/**
* Get the audio signal level.
*
* @return the audio signal level.
*/
public double getSignalValue() {
return value;
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.bus.event;
import org.lecturestudio.core.bus.event.BusEvent;
/**
* This event is published when the audio volume of an audio capture device or
* an audio playback device should be set.
*
* @author Alex Andres
*/
public class AudioVolumeEvent extends BusEvent {
/** The audio volume in the range of [0,1]. */
private float volume;
/**
* Create a AudioVolumeEvent instance with the specified audio volume value.
* The value must be in the range of [0,1].
*
* @param volume The audio volume.
*/
public AudioVolumeEvent(float volume) {
this.volume = volume;
}
/**
* Get the audio volume.
*
* @return the audio volume.
*/
public float getVolume() {
return volume;
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
import org.lecturestudio.core.ExecutableBase;
import org.lecturestudio.core.audio.AudioFormat;
/**
* Base audio codec implementation to provide a consistent mechanism to encode
* or decode audio data.
*
* @author Alex Andres
*/
public abstract class AudioCodec extends ExecutableBase {
/** The audio format of the audio codec. */
private AudioFormat format;
/**
* Process the audio input data with the implemented audio codec.
*
* @param input The audio input data to process.
* @param length The length of the audio input data.
* @param timestamp The current timestamp the specified audio data.
*
* @throws Exception If an fatal error occurred preventing the processed
* audio data to be used.
*/
abstract public void process(byte[] input, int length, long timestamp) throws Exception;
/**
* Set the audio format of the audio data this codec has to process.
*
* @param format The audio format.
*/
public void setFormat(AudioFormat format) {
this.format = format;
}
/**
* Get the codec's audio format.
*
* @return the audio format.
*/
public AudioFormat getFormat() {
return format;
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* SPI audio codec loader. To provide audio codecs the codec provider must
* implement the {@link org.lecturestudio.core.audio.codec.AudioCodecProvider} interface and
* be registered via the SPI.
*
* @author Alex Andres
*
* @link https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
*/
public class AudioCodecLoader {
private static final Logger LOG = LogManager.getLogger(AudioCodecLoader.class);
/** The AudioCodecLoader singleton instance. */
private static AudioCodecLoader service;
/** The audio codec service loader. */
private ServiceLoader<AudioCodecProvider> loader;
/**
* Retrieve the singleton instance of AudioCodecLoader.
*/
public static synchronized AudioCodecLoader getInstance() {
if (service == null) {
service = new AudioCodecLoader();
}
return service;
}
/**
* Get a AudioCodecProvider with the specified name.
*
* @param providerName The name of the AudioCodecProvider to retrieve.
*
* @return the AudioCodecProvider or null, if no such provider exists.
*/
public AudioCodecProvider getProvider(String providerName) {
try {
for (AudioCodecProvider audioCodecProvider : loader) {
if (audioCodecProvider.getProviderName().equals(providerName)) {
return audioCodecProvider;
}
}
}
catch (ServiceConfigurationError error) {
LOG.error("Get audio codec provider failed", error);
}
return null;
}
/**
* Retrieve all registered audio codec providers.
*
* @return an array of all audio codec providers.
*/
public AudioCodecProvider[] getProviders() {
List<AudioCodecProvider> list = new ArrayList<>();
try {
for (AudioCodecProvider audioCodecProvider : loader) {
list.add(audioCodecProvider);
}
}
catch (ServiceConfigurationError error) {
LOG.error("Get audio codec providers failed", error);
}
return list.toArray(new AudioCodecProvider[0]);
}
/**
* Retrieve the names of all registered audio codec providers.
*
* @return an array of names of all registered audio codec providers.
*/
public String[] getProviderNames() {
List<String> names = new ArrayList<>();
try {
for (AudioCodecProvider audioCodecProvider : loader) {
names.add(audioCodecProvider.getProviderName());
}
}
catch (ServiceConfigurationError error) {
LOG.error("Get audio codec provider names failed", error);
}
return names.toArray(new String[0]);
}
private AudioCodecLoader() {
File[] dirs = new File[] { new File("lib") };
List<URL> urls = new ArrayList<>();
for (File dir : dirs) {
if (dir.exists() && dir.isDirectory()) {
List<URL> acc = new ArrayList<>();
getLibs(dir, acc);
urls.addAll(acc);
}
}
URL[] libs = urls.toArray(new URL[0]);
loader = ServiceLoader.load(AudioCodecProvider.class, getClassLoader(libs));
}
private ClassLoader getClassLoader(URL[] libs) {
ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
URLClassLoader urlClassLoader = new URLClassLoader(libs, currentThreadClassLoader);
Thread.currentThread().setContextClassLoader(urlClassLoader);
return Thread.currentThread().getContextClassLoader();
}
private void getLibs(File path, List<URL> libs) {
File[] files = path.listFiles();
if (files == null) {
return;
}
File file;
for (File value : files) {
file = value;
if (file.isFile() && file.canRead() && file.getName().endsWith(".jar")) {
try {
libs.add(file.toURI().toURL());
}
catch (MalformedURLException e) {
e.printStackTrace();
}
}
if (value.isDirectory()) {
getLibs(value, libs);
}
}
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
import org.lecturestudio.core.net.rtp.RtpDepacketizer;
import org.lecturestudio.core.net.rtp.RtpPacketizer;
import org.lecturestudio.core.spi.ServiceProvider;
/**
* Common interface to provide a consistent mechanism for audio codec providers.
* As the name already implies, an audio codec provider provides audio codecs
* and packetizers that form audio data into packets according to a specific
* protocol.
*
* @author Alex Andres
*/
public interface AudioCodecProvider extends ServiceProvider {
/**
* Get the audio encoder.
*
* @return the audio encoder.
*/
AudioEncoder getAudioEncoder();
/**
* Get the audio decoder.
*
* @return the audio decoder.
*/
AudioDecoder getAudioDecoder();
/**
* Get the RTP packetizer.
*
* @return the RTP packetizer.
*/
RtpPacketizer getRtpPacketizer();
/**
* Get the RTP depacketizer.
*
* @return the RTP depacketizer.
*/
RtpDepacketizer getRtpDepacketizer();
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
import java.util.ArrayList;
import java.util.List;
/**
* Base audio decoder implementation to provide a consistent mechanism to decode
* encoded audio. To get the decoded audio the receiver has to register an
* {@link AudioDecoderListener}.
*
* @author Alex Andres
*/
public abstract class AudioDecoder extends AudioCodec {
/** Registered receivers of decoded audio. */
private List<AudioDecoderListener> listeners = new ArrayList<>();
/**
* Register an listener to receive the decoded audio.
*
* @param listener The listener that receives decoded audio.
*/
public void addListener(AudioDecoderListener listener) {
listeners.add(listener);
}
/**
* Remove an listener from the listener list.
*
* @param listener The listener to remove.
*/
public void removeListener(AudioDecoderListener listener) {
listeners.remove(listener);
}
/**
* Notify all registered listeners that an audio chunk has been decoded.
*
* @param data The decoded audio data.
* @param length The length of the decoded audio data.
* @param timestamp The timestamp of the decoded audio.
*/
protected void fireAudioDecoded(byte[] data, int length, long timestamp) {
for (AudioDecoderListener listener : listeners) {
listener.audioDecoded(data, length, timestamp);
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
/**
* This listener receives decoded audio data from an {@link AudioDecoder}.
*
* @author Alex Andres
*/
public interface AudioDecoderListener {
/**
* Receive an decoded audio chunk from the decoder.
*
* @param data The decoded audio data.
* @param length The length of the decoded audio data.
* @param timestamp The timestamp of the decoded audio.
*/
void audioDecoded(byte[] data, int length, long timestamp);
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
import java.util.ArrayList;
import java.util.List;
import org.lecturestudio.core.audio.AudioFormat;
/**
* Base audio encoder implementation to provide a consistent mechanism to encode
* audio. To get the encoded audio the receiver has to register an {@link
* AudioEncoderListener}.
*
* @author Alex Andres
*/
public abstract class AudioEncoder extends AudioCodec {
/** Registered receivers of encoded audio. */
private List<AudioEncoderListener> listeners = new ArrayList<>();
/** The bitrate of the encoded audio. */
private int bitrate;
/**
* Get all supported audio formats of this encoder.
*
* @return an array of supported audio formats.
*/
abstract public AudioFormat[] getSupportedFormats();
/**
* Set the bitrate of the encoded audio.
*
* @param bitrate the bitrate of the encoded audio.
*/
public void setBitrate(int bitrate) {
this.bitrate = bitrate;
}
/**
* Get the bitrate of the encoded audio.
*
* @return the bitrate of the encoded audio.
*/
public int getBitrate() {
return bitrate;
}
/**
* Register an listener to receive the encoded audio.
*
* @param listener The listener that receives encoded audio.
*/
public void addListener(AudioEncoderListener listener) {
listeners.add(listener);
}
/**
* Remove an listener from the listener list.
*
* @param listener The listener to remove.
*/
public void removeListener(AudioEncoderListener listener) {
listeners.remove(listener);
}
/**
* Notify all registered listeners that an audio chunk has been encoded.
*
* @param data The encoded audio data.
* @param length The length of the encoded audio data.
* @param timestamp The timestamp of the encoded audio.
*/
protected void fireAudioEncoded(byte[] data, int length, long timestamp) {
for (AudioEncoderListener listener : listeners) {
listener.audioEncoded(data, length, timestamp);
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
/**
* This listener receives encoded audio data from an {@link AudioEncoder}.
*
* @author Alex Andres
*/
public interface AudioEncoderListener {
/**
* Receive an encoded audio chunk from the encoder.
*
* @param data The encoded audio data.
* @param length The length of the encoded audio data.
* @param timestamp The timestamp of the encoded audio.
*/
void audioEncoded(byte[] data, int length, long timestamp);
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec;
import com.github.javaffmpeg.CodecID;
import org.lecturestudio.core.audio.codec.ffmpeg.FFmpegAudioDecoder;
import org.lecturestudio.core.audio.codec.ffmpeg.FFmpegAudioEncoder;
import org.lecturestudio.core.audio.codec.ffmpeg.FFmpegRtpDepacketizer;
import org.lecturestudio.core.audio.codec.ffmpeg.FFmpegRtpPacketizer;
import org.lecturestudio.core.net.rtp.RtpDepacketizer;
import org.lecturestudio.core.net.rtp.RtpPacketizer;
/**
* OPUS audio codec provider implementation.
*
* @link http://opus-codec.org
*
* @author Alex Andres
*/
public class OpusCodecProvider implements AudioCodecProvider {
@Override
public AudioEncoder getAudioEncoder() {
return new FFmpegAudioEncoder(CodecID.OPUS);
}
@Override
public AudioDecoder getAudioDecoder() {
return new FFmpegAudioDecoder(CodecID.OPUS);
}
@Override
public RtpPacketizer getRtpPacketizer() {
return new FFmpegRtpPacketizer();
}
@Override
public RtpDepacketizer getRtpDepacketizer() {
return new FFmpegRtpDepacketizer();
}
@Override
public String getProviderName() {
return "OPUS";
}
}

View file

@ -0,0 +1,183 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec.ffmpeg;
import com.github.javaffmpeg.Audio;
import com.github.javaffmpeg.AudioFrame;
import com.github.javaffmpeg.AudioResampler;
import com.github.javaffmpeg.Codec;
import com.github.javaffmpeg.CodecID;
import com.github.javaffmpeg.Decoder;
import com.github.javaffmpeg.JavaFFmpegException;
import com.github.javaffmpeg.MediaPacket;
import com.github.javaffmpeg.MediaType;
import com.github.javaffmpeg.SampleFormat;
import java.nio.ByteBuffer;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.audio.codec.AudioDecoder;
import org.bytedeco.javacpp.BytePointer;
/**
* FFmpeg audio decoder implementation.
*
* @link https://ffmpeg.org
*
* @author Alex Andres
*/
public class FFmpegAudioDecoder extends AudioDecoder {
private static final int SAMPLE_SIZE = 2;
private Decoder decoder;
private com.github.javaffmpeg.AudioResampler resampler;
/**
* Create a FFmpegAudioDecoder with the specified codec ID. Based on the ID
* the corresponding FFmpeg decoder will be created.
*
* @param codecID The ID of the codec to use.
*/
public FFmpegAudioDecoder(CodecID codecID) {
try {
decoder = new Decoder(Codec.getDecoderById(codecID));
decoder.setMediaType(MediaType.AUDIO);
}
catch (JavaFFmpegException e) {
e.printStackTrace();
}
}
@Override
public void process(byte[] input, int length, long timestamp) throws Exception {
ByteBuffer buffer = ByteBuffer.wrap(input, 0, length);
MediaPacket packet = new MediaPacket(buffer);
AudioFrame frame = decoder.decodeAudio(packet);
if (frame != null) {
if (resampler != null) {
AudioFrame[] frames = resampler.resample(frame);
for (AudioFrame resFrame : frames) {
processAudioFrame(resFrame, timestamp);
resFrame.clear();
}
}
else {
processAudioFrame(frame, timestamp);
}
frame.clear();
}
packet.clear();
}
@Override
protected void initInternal() throws ExecutableException {
}
@Override
protected void startInternal() throws ExecutableException {
AudioFormat inputFormat = getFormat();
int sampleRate = inputFormat.getSampleRate();
int channels = inputFormat.getChannels();
try {
decoder.setSampleRate(sampleRate);
decoder.setSampleFormat(SampleFormat.S16);
decoder.setAudioChannels(channels);
decoder.open(null);
}
catch (Exception e) {
throw new ExecutableException(e);
}
// requested format
com.github.javaffmpeg.AudioFormat reqFormat = new com.github.javaffmpeg.AudioFormat();
reqFormat.setChannelLayout(Audio.getChannelLayout(channels));
reqFormat.setChannels(channels);
reqFormat.setSampleFormat(SampleFormat.S16);
reqFormat.setSampleRate(sampleRate);
// decoder format
com.github.javaffmpeg.AudioFormat decFormat = new com.github.javaffmpeg.AudioFormat();
decFormat.setChannelLayout(decoder.getChannelLayout());
decFormat.setChannels(decoder.getAudioChannels());
decFormat.setSampleFormat(decoder.getSampleFormat());
decFormat.setSampleRate(decoder.getSampleRate());
// in some cases the decoder chooses it's own parameters, e.g. OPUS
if (!reqFormat.equals(decFormat)) {
int samples = sampleRate / 50; // 20 ms audio
resampler = new AudioResampler();
try {
resampler.open(decFormat, reqFormat, samples);
}
catch (Exception e) {
throw new ExecutableException(e);
}
}
}
@Override
protected void stopInternal() throws ExecutableException {
decoder.close();
resampler.close();
}
@Override
protected void destroyInternal() throws ExecutableException {
}
private void processAudioFrame(AudioFrame frame, long timestamp) {
int planes = frame.getPlaneCount();
int size = planes * frame.getPlane(0).limit();
byte[] samples = new byte[size];
// interleave planes
for (int i = 0; i < planes; i++) {
BytePointer plane = frame.getPlane(i);
ByteBuffer pBuffer = plane.asByteBuffer();
int pLength = plane.limit();
int offset = i * planes;
for (int j = 0, k = offset; j < pLength; j += SAMPLE_SIZE) {
samples[k++] = (byte) (pBuffer.get() & 0xFF);
samples[k++] = (byte) (pBuffer.get() & 0xFF);
k += SAMPLE_SIZE * (planes - 1);
}
}
fireAudioDecoded(samples, samples.length, timestamp);
}
}

View file

@ -0,0 +1,185 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec.ffmpeg;
import com.github.javaffmpeg.Audio;
import com.github.javaffmpeg.AudioFrame;
import com.github.javaffmpeg.Codec;
import com.github.javaffmpeg.CodecID;
import com.github.javaffmpeg.Encoder;
import com.github.javaffmpeg.JavaFFmpegException;
import com.github.javaffmpeg.MediaPacket;
import com.github.javaffmpeg.MediaType;
import com.github.javaffmpeg.SampleFormat;
import org.lecturestudio.core.ExecutableException;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.audio.codec.AudioEncoder;
/**
* FFmpeg audio encoder implementation.
*
* @link https://ffmpeg.org
*
* @author Alex Andres
*/
public class FFmpegAudioEncoder extends AudioEncoder {
/** An array of supported audio formats. */
private AudioFormat[] supportedFormats;
/** The internal FFmpeg encoder. */
private Encoder encoder;
/** The internal encoding format. */
private com.github.javaffmpeg.AudioFormat format;
/** The sample size in bytes. */
private int sampleSize;
/**
* Create a FFmpegAudioEncoder with the specified codec ID. Based on the ID
* the corresponding FFmpeg encoder will be created.
*
* @param codecId The ID of the codec to use.
*/
public FFmpegAudioEncoder(CodecID codecId) {
try {
encoder = new Encoder(Codec.getEncoderById(codecId));
encoder.setMediaType(MediaType.AUDIO);
setBitrate(128000);
getInputFormats();
}
catch (JavaFFmpegException e) {
e.printStackTrace();
}
}
@Override
public AudioFormat[] getSupportedFormats() {
return supportedFormats;
}
@Override
public void process(byte[] input, int length, long timestamp) throws Exception {
int samples = input.length / sampleSize;
AudioFrame frame = new AudioFrame(format, samples);
// mono stream
if (format.getChannels() == 1) {
frame.getPlane(0).asByteBuffer().put(input);
}
else {
throw new Exception("Frame input only for mono audio implemented.");
}
MediaPacket[] packets = encoder.encodeAudio(frame);
if (packets != null) {
for (MediaPacket packet : packets) {
if (packet == null) {
continue;
}
byte[] outputData = new byte[packet.getData().limit()];
packet.getData().get(outputData);
fireAudioEncoded(outputData, outputData.length, timestamp);
packet.clear();
}
}
frame.clear();
}
@Override
protected void initInternal() throws ExecutableException {
}
@Override
protected void startInternal() throws ExecutableException {
AudioFormat inputFormat = getFormat();
int sampleRate = inputFormat.getSampleRate();
int channels = inputFormat.getChannels();
try {
encoder.setMediaType(MediaType.AUDIO);
encoder.setBitrate(getBitrate());
encoder.setSampleRate(sampleRate);
encoder.setSampleFormat(SampleFormat.S16);
encoder.setAudioChannels(channels);
encoder.setQuality(0); // Quality-based encoding not supported.
encoder.open(null);
}
catch (JavaFFmpegException e) {
throw new ExecutableException(e);
}
format = new com.github.javaffmpeg.AudioFormat();
format.setChannelLayout(Audio.getChannelLayout(channels));
format.setChannels(channels);
format.setSampleFormat(SampleFormat.S16);
format.setSampleRate(sampleRate);
sampleSize = 2 * format.getChannels();
}
@Override
protected void stopInternal() throws ExecutableException {
encoder.close();
}
@Override
protected void destroyInternal() throws ExecutableException {
}
private void getInputFormats() {
if (encoder == null) {
return;
}
Integer[] sampleRates = encoder.getCodec().getSupportedSampleRates();
if (sampleRates == null) {
return;
}
supportedFormats = new AudioFormat[sampleRates.length];
AudioFormat.Encoding encoding = AudioFormat.Encoding.S16LE;
int channels = 1;
for (int i = 0; i < sampleRates.length; i++) {
int sampleRate = sampleRates[i];
AudioFormat audioFormat = new AudioFormat(encoding, sampleRate, channels);
supportedFormats[i] = audioFormat;
}
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec.ffmpeg;
import org.lecturestudio.core.net.rtp.RtpDepacketizer;
import org.lecturestudio.core.net.rtp.RtpPacket;
/**
* FFmpeg RTP depacketizer implementation.
*
* @author Alex Andres
*/
public class FFmpegRtpDepacketizer implements RtpDepacketizer {
@Override
public byte[] processPacket(RtpPacket packet) {
return packet.getPayload();
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.codec.ffmpeg;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.lecturestudio.core.net.rtp.RtpPacket;
import org.lecturestudio.core.net.rtp.RtpPacketizer;
/**
* FFmpeg RTP packetizer implementation.
*
* @author Alex Andres
*/
public class FFmpegRtpPacketizer implements RtpPacketizer {
/** The RTP packet which should be sent. */
private RtpPacket rtpPacket;
/**
* Create a new FFmpegRtpPacketizer instance and set the default RTP packet
* header values.
*/
public FFmpegRtpPacketizer() {
Random rand = new Random();
rtpPacket = new RtpPacket();
rtpPacket.setVersion(2);
rtpPacket.setPadding(0);
rtpPacket.setExtension(0);
rtpPacket.setMarker(0);
rtpPacket.setPayloadType(97); // dynamic
rtpPacket.setSeqNumber(rand.nextInt());
rtpPacket.setTimestamp(rand.nextInt());
rtpPacket.setSsrc(rand.nextInt());
}
@Override
public List<RtpPacket> processPacket(byte[] payload, int payloadLength, long timestamp) {
List<RtpPacket> packets = new ArrayList<>();
updateRtpPacket(timestamp);
packPacket(payload, payloadLength);
packets.add(rtpPacket.clone());
return packets;
}
private void packPacket(byte[] packetBytes, int payloadSize) {
byte[] payload = new byte[payloadSize];
System.arraycopy(packetBytes, 0, payload, 0, payloadSize);
rtpPacket.setPayload(payload);
}
private void updateRtpPacket(long timestamp) {
/* Increment RTP header flags */
rtpPacket.setSeqNumber(rtpPacket.getSeqNumber() + 1);
rtpPacket.setTimestamp(timestamp);
}
}

View file

@ -0,0 +1,277 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.device;
import java.util.Arrays;
import java.util.List;
import org.lecturestudio.core.audio.AudioFormat;
/**
* Common class to provide a consistent mechanism for audio devices.
*
* @author Alex Andres
*/
public abstract class AudioDevice {
/** An array of all available sample rates to support. */
public static final int[] SUPPORTED_SAMPLE_RATES = new int[] {
8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000
};
/** The audio format to be used by the audio device. */
private AudioFormat audioFormat;
/** The audio volume for playback or recording. */
private double volume = 1;
/** The measured audio signal power level of the last processed audio chunk. */
private double signalPowerLevel = 0;
/** Indicated whether the audio is muted or not. */
private boolean mute = false;
/**
* Get the name of the audio device assigned by the operating system.
*
* @return the name of the audio device.
*/
abstract public String getName();
/**
* Open the audio device and prepare the device to capture or play audio.
*
* @throws Exception If the audio device failed to open.
*/
abstract public void open() throws Exception;
/**
* Close the audio device and release all previously assigned resources.
*
* @throws Exception If the audio device could not be closed.
*/
abstract public void close() throws Exception;
/**
* Start capturing or playing audio by the device.
*
* @throws Exception If the device failed to start.
*/
abstract public void start() throws Exception;
/**
* Stop capturing or playing audio by the device.
*
* @throws Exception If the device failed to stop.
*/
abstract public void stop() throws Exception;
/**
* Check if the audio device is opened.
*
* @return true if the device is opened, false otherwise.
*/
abstract public boolean isOpen();
/**
* Get a list of all supported audio formats by this device.
*
* @return a list of all supported audio formats.
*/
abstract public List<AudioFormat> getSupportedFormats();
/**
* Get the current audio buffer size. The buffer size reflects the latency
* of the audio signal.
*
* @return the current audio buffer size.
*/
abstract public int getBufferSize();
/**
* Check if the specified audio format is supported by the device.
*
* @param format The audio format to check.
*
* @return true if the audio format is supported, false otherwise.
*/
public boolean supportsAudioFormat(AudioFormat format) {
return getSupportedFormats().contains(format);
}
/**
* Set the audio format to be used by the audio device for playback or
* recording.
*
* @param format The audio format to be used.
*/
public void setAudioFormat(AudioFormat format) {
this.audioFormat = format;
}
/**
* Get the audio format of the device.
*
* @return the audio format.
*/
public AudioFormat getAudioFormat() {
return audioFormat;
}
/**
* Get the volume of the device with which it plays or records audio.
*
* @return the volume of audio.
*/
public double getVolume() {
return volume;
}
/**
* Set the audio volume for playback or recording. The volume value must be
* in the range of [0,1].
*
* @param volume The new volume value.
*/
public void setVolume(double volume) {
if (volume < 0 || volume > 1)
return;
this.volume = volume;
}
/**
* Check whether audio signal is muted or not.
*
* @return true if the audio signal is muted, false otherwise.
*/
public boolean isMuted() {
return mute;
}
/**
* Set whether to mute the audio signal of this device.
*
* @param mute True to mute the audio signal, false otherwise.
*/
public void setMute(boolean mute) {
this.mute = mute;
}
/**
* Get the signal power level of the last processed audio data chunk.
*
* @return the current signal power level of audio.
*/
public double getSignalPowerLevel() {
if (isMuted()) {
return 0;
}
return signalPowerLevel;
}
/**
* Set the signal power level of the last processed audio data chunk.
*
* @param level the current signal power level of audio.
*/
protected void setSignalPowerLevel(float level) {
this.signalPowerLevel = level;
}
/**
* AGC algorithm to adjust the speech level of an audio signal to a
* specified value in dBFS.
*
* @param input Audio input samples with values in range [-1, 1].
* @param output Audio output samples with values in range [-1, 1].
* @param gainLevel Output power level in dBFS.
* @param sampleCount Number of samples.
*/
protected void AGC(float[] input, float[] output, float gainLevel, int sampleCount) {
// Convert power gain level into normal power.
float power = (float) Math.pow(10, (gainLevel / 10));
// Calculate the energy of the input signal.
float energy = 0;
for (int i = 0; i < sampleCount; i++) {
energy += input[i] * input[i];
}
// Calculate the amplification factor.
float amp = (float) Math.sqrt((power * sampleCount) / energy);
// Scale the input signal to achieve the required output power.
for (int i = 0; i < sampleCount; i++) {
output[i] = input[i] * amp;
}
}
void applyGain(byte[] buffer, int offset, int length) {
if (volume == 1) {
signalPowerLevel = getSignalPowerLevel(buffer);
return;
}
if (volume == 0) {
signalPowerLevel = 0;
Arrays.fill(buffer, offset, length - offset, (byte) 0);
return;
}
float energy = 0;
for (int i = 0; i < buffer.length; i += 2) {
int value = (short) ((buffer[i + 1] << 8) | (buffer[i] & 0xFF));
value = (int) (volume * value);
if (value > Short.MAX_VALUE) {
value = Short.MAX_VALUE;
}
else if (value < Short.MIN_VALUE) {
value = Short.MIN_VALUE;
}
float norm = (float) value / Short.MAX_VALUE;
energy += norm * norm;
buffer[i] = (byte) value;
buffer[i + 1] = (byte) (value >> 8);
}
signalPowerLevel = (float) (10 * Math.log10(energy / (buffer.length / 2f)) + 96) / 96;
}
private float getSignalPowerLevel(byte[] buffer) {
float energy = 0;
for (int i = 0; i < buffer.length; i += 2) {
int value = (short) ((buffer[i + 1] << 8) | (buffer[i] & 0xFF));
float norm = (float) value / Short.MAX_VALUE;
energy += norm * norm;
}
return (float) (10 * Math.log10(energy / (buffer.length / 2f)) + 96) / 96;
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.device;
/**
* Common class to provide a consistent mechanism for audio capture devices.
*
* @author Alex Andres
*/
public abstract class AudioInputDevice extends AudioDevice {
/**
* Device specific method to be implemented to read captured audio data from
* the device into the specified audio buffer.
*
* @param buffer The audio buffer into which to write the captured audio.
* @param offset The offset at which to start to write the audio buffer.
* @param length The length of the audio buffer.
*
* @return the number of bytes written to the audio buffer.
*
* @throws Exception If captured audio could not be written to the buffer.
*/
abstract protected int readInput(byte[] buffer, int offset, int length) throws Exception;
/**
* Read captured audio data from the device into the specified audio
* buffer.
*
* @param buffer The audio buffer into which to write the captured audio.
* @param offset The offset at which to start to write the audio buffer.
* @param length The length of the audio buffer.
*
* @return the number of bytes written to the audio buffer.
*
* @throws Exception If captured audio could not be written to the buffer.
*/
public synchronized int read(byte[] buffer, int offset, int length) throws Exception {
int read = readInput(buffer, offset, length);
if (!isMuted()) {
applyGain(buffer, offset, length);
}
return read;
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.device;
/**
* Common class to provide a consistent mechanism for audio playback devices.
*
* @author Alex Andres
*/
public abstract class AudioOutputDevice extends AudioDevice {
/**
* Device specific method to be implemented to write audio data for playback
* to the device from the specified audio buffer.
*
* @param buffer The audio buffer containing the samples for playback.
* @param offset The offset from which to start to read the audio buffer.
* @param length The length of the audio buffer.
*
* @return the number of bytes written to the playback buffer of the device.
*
* @throws Exception If the playback device did not accept the audio buffer.
*/
abstract public int writeOutput(byte[] buffer, int offset, int length) throws Exception;
/**
* Write audio samples for playback.
*
* @param buffer The audio buffer containing the samples for playback.
* @param offset The offset from which to start to read the audio buffer.
* @param length The length of the audio buffer.
*
* @return the number of bytes written to the playback buffer of the device.
*
* @throws Exception If the playback device did not accept the audio buffer.
*/
public int write(byte[] buffer, int offset, int length) throws Exception {
int written = length;
if (!isMuted()) {
applyGain(buffer, offset, length);
written = writeOutput(buffer, offset, length);
}
return written;
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.device;
import com.github.javaffmpeg.Audio;
import com.github.javaffmpeg.AudioFrame;
import java.util.ArrayList;
import java.util.List;
import org.lecturestudio.core.audio.AudioFormat;
/**
* FFmpeg audio capture device implementation.
*
* @author Alex Andres
*/
public class FFmpegAudioInputDevice extends AudioInputDevice {
/** Internal capture device. */
private final com.github.javaffmpeg.FFmpegAudioInputDevice device;
/**
* Create a new FFmpegAudioInputDevice instance with the specified FFmpeg
* capture device.
*
* @param device The FFmpeg capture device.
*/
public FFmpegAudioInputDevice(com.github.javaffmpeg.FFmpegAudioInputDevice device) {
this.device = device;
}
@Override
protected synchronized int readInput(byte[] buffer, int offset, int length) {
if (isOpen()) {
AudioFrame samples = device.getSamples();
// TODO get by sample format
Audio.getAudio16(samples, buffer);
samples.clear();
return length;
}
return 0;
}
@Override
public String getName() {
return device.getName();
}
@Override
public synchronized void open() throws Exception {
if (isOpen()) {
return;
}
AudioFormat audioFormat = getAudioFormat();
com.github.javaffmpeg.AudioFormat format = new com.github.javaffmpeg.AudioFormat();
format.setChannelLayout(Audio.getChannelLayout(audioFormat.getChannels()));
format.setChannels(audioFormat.getChannels());
format.setSampleFormat(Audio.getSampleFormat(audioFormat.getBytesPerSample(), false, false));
format.setSampleRate(audioFormat.getSampleRate());
device.setBufferMilliseconds(20);
device.open(format);
}
@Override
public synchronized void close() throws Exception {
device.close();
}
@Override
public synchronized void start() {
// nothing to do
}
@Override
public synchronized void stop() {
// nothing to do
}
@Override
public synchronized boolean isOpen() {
return device.isOpen();
}
@Override
public List<AudioFormat> getSupportedFormats() {
AudioFormat.Encoding encoding = AudioFormat.Encoding.S16LE;
int channels = 1;
List<AudioFormat> formats = new ArrayList<AudioFormat>();
for (int sampleRate : AudioDevice.SUPPORTED_SAMPLE_RATES) {
AudioFormat audioFormat = new AudioFormat(encoding, sampleRate, channels);
formats.add(audioFormat);
}
return formats;
}
@Override
public int getBufferSize() {
return device.getBufferSize();
}
}

View file

@ -0,0 +1,170 @@
/*
* Copyright (C) 2020 TU Darmstadt, Department of Computer Science,
* Embedded Systems and Applications Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lecturestudio.core.audio.device;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import java.util.ArrayList;
import java.util.List;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.TargetDataLine;
import org.lecturestudio.core.audio.AudioFormat;
import org.lecturestudio.core.util.OsInfo;
/**
* Java-based audio capture device implementation.
*
* @author Alex Andres
*/
public class JavaSoundInputDevice extends AudioInputDevice {
/** Minimal audio buffer size to use with Java. */
private static final int BUFFER_SIZE = 4096;
/** Internal Mixer.Info that represents information about an audio mixer. */
private final Mixer.Info mixerInfo;
/** Internal capture source. */
private TargetDataLine line;
/**
* Create a new JavaSoundInputDevice instance with the specified {@code
* Mixer.Info} that contains information about an audio mixer.
*
* @param mixerInfo The audio mixer info.
*/
public JavaSoundInputDevice(Mixer.Info mixerInfo) {
this.mixerInfo = mixerInfo;
}
@Override
public String getName() {
return mixerInfo.getName();
}
@Override
public void open() throws Exception {
if (isNull(mixerInfo)) {
throw new Exception("Invalid audio mixer set.");
}
Mixer mixer = AudioSystem.getMixer(mixerInfo);
if (mixer == null) {
throw new Exception("Could not acquire specified mixer: " + getName());
}
AudioFormat audioFormat = getAudioFormat();
javax.sound.sampled.AudioFormat format = createAudioFormat(
audioFormat.getSampleRate(), audioFormat.getChannels());
DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
line = (TargetDataLine) mixer.getLine(info);
line.open(format, BUFFER_SIZE);
}
@Override
public void close() throws Exception {
if (nonNull(line)) {
line.stop();
line.flush();
if (!OsInfo.isMac()) {
line.close();
}
line = null;
}
}
@Override
public void start() {
if (nonNull(line)) {
line.start();
}
}
@Override
public void stop() {
if (nonNull(line)) {
line.stop();
line.flush();
}
}
@Override
public int readInput(byte[] buffer, int offset, int length) {
return line.read(buffer, offset, length);
}
@Override
public List<AudioFormat> getSupportedFormats() {
AudioFormat.Encoding encoding = AudioFormat.Encoding.S16LE;
int channels = 1;
List<AudioFormat> formats = new ArrayList<>();
Mixer mixer = AudioSystem.getMixer(mixerInfo);
DataLine.Info info;
for (int sampleRate : AudioDevice.SUPPORTED_SAMPLE_RATES) {
AudioFormat audioFormat = new AudioFormat(encoding, sampleRate, channels);
javax.sound.sampled.AudioFormat format = createAudioFormat(sampleRate, channels);
info = new DataLine.Info(TargetDataLine.class, format);
if (mixer.isLineSupported(info)) {
formats.add(audioFormat);
}
}
return formats;
}
@Override
public int getBufferSize() {
if (nonNull(line)) {
return line.getBufferSize();
}
return -1;
}
@Override
public boolean isOpen() {
return nonNull(line) && line.isOpen();
}
private javax.sound.sampled.AudioFormat createAudioFormat(int sampleRate, int channels) {
javax.sound.sampled.AudioFormat.Encoding encoding = javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED;
int sampleSizeInBits = 16;
int frameSize = (sampleSizeInBits / 8) * channels;
return new javax.sound.sampled.AudioFormat(
encoding, sampleRate, sampleSizeInBits, channels, frameSize,
sampleRate, false);
}
}

Some files were not shown because too many files have changed in this diff Show more