First import

This commit is contained in:
Andrea Bondavalli 2020-01-28 20:16:30 +01:00
commit 259e99afbc
76 changed files with 12117 additions and 0 deletions

3
3rdparty/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/ravenna-alsa-lkm
/cpp-httplib

View File

@ -0,0 +1,13 @@
diff --git a/driver/module_interface.c b/driver/module_interface.c
index 5f924f9..be0663b 100644
--- a/driver/module_interface.c
+++ b/driver/module_interface.c
@@ -94,7 +94,7 @@ unsigned int nf_hook_func(unsigned int hooknum, struct sk_buff *skb, const struc
if (ip_header->saddr == 0x0100007f) // 127.0.0.1
{
//printk(KERN_INFO "Loopback address detected\n");
- return NF_ACCEPT;
+ //return NF_ACCEPT;
}

View File

@ -0,0 +1,35 @@
diff --git a/driver/PTP.c b/driver/PTP.c
index 4b2242a..1bf593a 100644
--- a/driver/PTP.c
+++ b/driver/PTP.c
@@ -90,7 +90,7 @@ uint32_t get_FS(uint32_t ui32SamplingRate)
return 2;
default:
// TODO: should assert
- MTAL_DP("Caudio_streamer_clock::get_FS error: unknown SamplingRate = %u\n", ui32SamplingRate);
+ //MTAL_DP("Caudio_streamer_clock::get_FS error: unknown SamplingRate = %u\n", ui32SamplingRate);
case 48000:
case 44100:
return 1;
@@ -110,7 +110,7 @@ uint32_t get_samplerate_base(uint32_t ui32SamplingRate)
default:
// TODO: should assert
- MTAL_DP("Caudio_streamer_clock::get_samplerate_base error: unknown SamplingRate = %u\n", ui32SamplingRate);
+ //MTAL_DP("Caudio_streamer_clock::get_samplerate_base error: unknown SamplingRate = %u\n", ui32SamplingRate);
case 352800:
case 176400:
case 88200:
diff --git a/driver/module_netlink.c b/driver/module_netlink.c
index 48de263..dcdcce0 100644
--- a/driver/module_netlink.c
+++ b/driver/module_netlink.c
@@ -158,7 +158,7 @@ void recv_reply_from_user_land(struct sk_buff *skb)
// check if the given size if sufficient to copy the answered data
if (response_from_user_land->dataSize >= msg->dataSize)
{
- if (response_from_user_land->data == NULL)
+ if (response_from_user_land->data != NULL)
{
memcpy(response_from_user_land->data, msg->data, msg->dataSize);
}

View File

@ -0,0 +1,100 @@
diff --git a/driver/MTAL_LKernelAPI.c b/driver/MTAL_LKernelAPI.c
index 164f315..a993b41 100644
--- a/driver/MTAL_LKernelAPI.c
+++ b/driver/MTAL_LKernelAPI.c
@@ -35,7 +35,7 @@
#include <linux/spinlock.h>
#include <linux/version.h>
-#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,19,0)
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,19,0) || LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0)
#include <linux/timekeeping.h>
#else
#include <linux/time.h>
@@ -214,7 +214,10 @@ uint64_t MTAL_LK_GetCounterFreq(void)
uint64_t MTAL_LK_GetSystemTime(void)
{
uint64_t timeVal = 0ull;
-#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,19,0)
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0)
+ struct timespec64 ts64;
+ ktime_get_real_ts64(&ts64);
+#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3,19,0)
struct timespec64 ts64;
getnstimeofday64(&ts64);
#else
diff --git a/driver/module_timer.c b/driver/module_timer.c
index 5f64a8e..372afa3 100644
--- a/driver/module_timer.c
+++ b/driver/module_timer.c
@@ -35,7 +35,11 @@
#include "module_main.h"
#include "module_timer.h"
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0)
+static struct hrtimer my_hrtimer_;
+#else
static struct tasklet_hrtimer my_hrtimer_;
+#endif
static uint64_t base_period_;
static uint64_t max_period_allowed;
static uint64_t min_period_allowed;
@@ -80,8 +84,8 @@ enum hrtimer_restart timer_callback(struct hrtimer *timer)
///ret_overrun = hrtimer_forward(timer, kt_now, period);
ret_overrun = hrtimer_forward_now(timer, period);
// comment it when running in VM
- if(ret_overrun > 1)
- printk(KERN_INFO "Timer overrun ! (%d times)\n", ret_overrun);
+ /*if(ret_overrun > 1)
+ printk(KERN_INFO "Timer overrun ! (%d times)\n", ret_overrun);*/
return HRTIMER_RESTART;
}
@@ -89,9 +93,12 @@ enum hrtimer_restart timer_callback(struct hrtimer *timer)
int init_clock_timer(void)
{
stop_ = 0;
- ///hrtimer_init(&my_hrtimer_, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0)
+ hrtimer_init(&my_hrtimer_, CLOCK_MONOTONIC, HRTIMER_MODE_PINNED);
+ my_hrtimer_.function = &timer_callback;
+#else
tasklet_hrtimer_init(&my_hrtimer_, timer_callback, CLOCK_MONOTONIC/*_RAW*/, HRTIMER_MODE_PINNED/*HRTIMER_MODE_ABS*/);
- ///my_hrtimer_.function = &timer_callback;
+#endif
//base_period_ = 100 * ((unsigned long)1E6L); // 100 ms
base_period_ = 1333333; // 1.3 ms
@@ -108,8 +115,12 @@ void kill_clock_timer(void)
int start_clock_timer(void)
{
- ktime_t period = ktime_set(0, base_period_); //100 ms
+ ktime_t period = ktime_set(0, base_period_);
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0)
+ hrtimer_start(&my_hrtimer_, period, HRTIMER_MODE_REL);
+#else
tasklet_hrtimer_start(&my_hrtimer_, period, HRTIMER_MODE_ABS);
+#endif
return 0;
}
@@ -117,7 +128,11 @@ int start_clock_timer(void)
void stop_clock_timer(void)
{
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0)
+ hrtimer_cancel(&my_hrtimer_);
+#else
tasklet_hrtimer_cancel(&my_hrtimer_);
+#endif
/*int ret_cancel = 0;
while(hrtimer_callback_running(&my_hrtimer_))
++ret_cancel;
@@ -145,4 +160,4 @@ void set_base_period(uint64_t base_period)
min_period_allowed = base_period_ / 7;
max_period_allowed = (base_period_ * 10) / 6;
printk(KERN_INFO "Base period set to %lld ns\n", base_period_);
-}
\ No newline at end of file
+}

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. <https://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 <https://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
<https://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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

98
README.md Normal file
View File

@ -0,0 +1,98 @@
# AES67 Linux Daemon
## License ##
AES67 daemon and the WebUI are licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
The daemon uses the following open source:
* **Merging Technologies ALSA RAVENNA/AES67 Driver** licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
* **cpp-httplib** licensed under the [MIT License](https://github.com/yhirose/cpp-httplib/blob/master/LICENSE)
## Repository content ##
### [daemon](daemon) directory ###
This directory contains the AES67 daemon source code.
The daemon can be cross-compiled for multiple platforms and implements the following functionalities:
* communication and configuration with the ALSA RAVENNA/AES67 driver via netlink
* HTTP REST API for control and configuration
* session handling and SDP parsing and creation
* SAP discovery protocol implementation
* IGMP handling for SAP and RTP sessions
The directory also contains the daemon regression tests in the [tests](tests) subdirectory. To run daemon tests install the ALSA RAVENNA/AES67 kernel module enter the [tests](tests) subdirectory and run *./daemon-test -l all*
See the [README](daemon/README.md) file in this directory for additional information about the AES67 daemon configuration and the HTTP REST API.
### [webui](webui) directory ###
This directory contains the AES67 daemon WebUI configuration implemented using React.
With the WebUI a user can do the following operations:
* change the daemon configuration, this causes a daemon restart
* edit PTP clock slave configuration and monitor status PTP status
* add and edit RTP Sources
* add, edit and monitor RTP Sinks
### [3rdparty](3rdparty) directory ###
This directory used to download the 3rdparty open source used by the daemon.
The **patches** subdirectory contains patches applied to the ALSA RAVENNA/AES67 module to compile with the Linux Kernel 5.x and to enable operations on the network loopback device (for testing purposes).
The ALSA RAVENNA/AES67 kernel module is responsible for:
* registering as an ALSA driver
* generating and receiving RTP audio packets
* PTP slave operations and PTP driven interrupt loop
* netlink communication between user and kernel
See [ALSA RAVENNA/AES67 Driver README](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md) for additional information about the Merging Technologies module and for proper Linux Kernel configuration and tuning.
### [demo](demo) directory ###
This directory contains a the daemon configuration and status files used to run a short demo on the network loopback device. The [demo](#demo) is described below.
## Prerequisite ##
The daemon has been tested on **Ubuntu 18.04** and **19.10** using:
* Linux kernel version >= 5.0.0
* GCC version >= 7.4 / clang >= 6.0.0 (C++17 support required)
* cmake version >= 3.10.2
* node version >= 8.10.0
* mpm version >= 3.5.2
* boost libraries version >= 1.65.1
The [ubuntu-packages.sh](ubuntu-packages.sh) script can be used to install all the packages required to compile and run the AES67 daemon, the daemon tests and the [demo](#demo). See [script notes](#notes).
## How to build ##
To compile the AES67 daemon and the WebUI use the [build.sh](build.sh) script. See [script notes](#notes).
The script performs the following operations:
* checkout, patch and build the Merging Technologies ALSA RAVENNA/AES67 module
* checkout the cpp-httplib
* build and deploy the WebUI
* build the AES67 daemon
## Run the Demo ##
<a name="demo"></a>
To run a simple demo use the [run\_demo.sh](run_demo.sh) script. See [script notes](#notes).
The demo performs the following operations:
* setup system parameters
* install the ALSA RAVENNA/AES67 module
* start the ptp4l as master clock on the network loopback device
* start the AES67 daemon and creates a source and a sink according to the status file in the demo directory
* open a browser on the daemon PTP status page
* wait for the Ravenna driver PTP slave to synchronize
* start recording on the configured ALSA sink for 60 seconds to the wave file in *./demo/sink_test.wav*
* start playing a test sound on the configured ALSA source
* wait for the recording to complete and terminate ptp4l and the AES67 daemon
## Notes ##
<a name="notes"></a>
* All the scripts in this repository are provided as a reference to help setting up the system and run a simple demo.
They have been tested on **Ubuntu 18.04** and **19.10** distros only.

41
build.sh Executable file
View File

@ -0,0 +1,41 @@
#!/bin/bash
#
# Tested on Ubuntu 18.04
#
cd 3rdparty
if [ ! -d ravenna-alsa-lkm.git ]; then
git clone https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm.git
cd ravenna-alsa-lkm
git checkout 5a06f0d33c18e532eb5dac3ad90c0acd59fbabd7
cd driver
echo "Apply patches to ravenna-alsa-lkm module ..."
git apply ../../patches/ravenna-alsa-lkm-kernel-v5.patch
git apply ../../patches/ravenna-alsa-lkm-enable-loopback.patch
git apply ../../patches/ravenna-alsa-lkm-fixes.patch
echo "Building ravenna-alsa-lkm kernel module ..."
make
cd ../..
fi
if [ ! -d cpp-httplib.git ]; then
git clone https://github.com/yhirose/cpp-httplib.git
cd cpp-httplib
git checkout 301a419c0243d3ab843e5fc2bb9fa56a9daa7bcd
cd ..
fi
cd ..
cd webui
echo "Building and installing webui ..."
#npm install react-modal react-toastify react-router-dom
npm install
npm run build
cd ..
cd daemon
echo "Building aes67-daemon ..."
cmake .
make
cd ..

28
cleanup.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
rm -rf 3rdparty/ravenna-alsa-lkm
rm -rf 3rdparty/cpp-httplib
rm -rf daemon/status.json
rm -rf daemon/CMakeFiles
rm -rf daemon/Makefile
rm -rf daemon/CMakeCache.txt
rm -rf daemon/cmake_install.cmake
rm -rf daemon/CTestTestfile.cmake
rm -rf daemon/Testing
rm -rf daemon/aes67-daemon
rm -rf daemon/tests/CMakeFiles
rm -rf daemon/tests/Makefile
rm -rf daemon/tests/CMakeCache.txt
rm -rf daemon/tests/cmake_install.cmake
rm -rf daemon/tests/CTestTestfile.cmake
rm -rf daemon/tests/Testing
rm -rf daemon/tests/daemon-test
rm -rf demo/sink-test.wav
rm -rf webui/build
rm -rf webui/node_modules
rm -rf webui/*.log
rm -rf webui/package-lock.json

11
daemon/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# cmake
/CMakeFiles
/Testings
CMakeCache.txt
cmake_install.cmake
Makefile
CTestTestfile.cmake
aes67-daemon

12
daemon/CMakeLists.txt Normal file
View File

@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.7.0)
project(aes67-daemon CXX)
enable_testing()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "-g -O3 -DBOOST_LOG_DYN_LINK -DBOOST_LOG_USE_NATIVE_SYSLOG -Wall")
set(RAVENNA_ALSA_LKM "../3rdparty/ravenna-alsa-lkm/")
set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/")
find_package(Boost COMPONENTS system thread log program_options REQUIRED)
include_directories(aes67-daemon ${RAVENNA_ALSA_LKM}/common ${RAVENNA_ALSA_LKM}/driver ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR})
add_executable(aes67-daemon error_code.cpp json.cpp main.cpp driver_handler.cpp driver_manager.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp)
add_subdirectory(tests)
target_link_libraries(aes67-daemon ${Boost_LIBRARIES})

674
daemon/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. <https://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 <https://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
<https://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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

549
daemon/README.html Normal file
View File

@ -0,0 +1,549 @@
<h1>AES67 Daemon</h1>
<p>AES67 daemon uses the Merging Technologies device driver (MergingRavennaALSA.ko) to implement basic AES67 functionalities. See <a href="https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md">ALSA RAVENNA/AES67 Driver README</a> for additional information.</p>
<p>The daemon is responsible for:</p>
<ul>
<li>communication and configuration of the device driver</li>
<li>provide an HTTP REST API for the daemon control and configuration</li>
<li>SAP discovery protocol implementation </li>
<li>IGMP handling for SAP and RTP sessions</li>
</ul>
<h2>Configuration file</h2>
<p>The daemon uses a JSON file to store the configuration parameters. <br />
The config file must be specified at startup time and it gets updated when new params are set via the REST interface (WebUI).
See <a href="#config">JSON config params</a> for additional info on the configuration parameters.
If a status file is specified in the daemon's configuration the server will load it at startup ad will save it at termination.
The status file contains all the configured sources and sinks (streams).
See <a href="#rtp-stream">JSON streams</a> for additional info on the status file format and its parameters.</p>
<h2>HTTP REST API</h2>
<p>The daemon implements a REST API interface to configure and control the driver.
All operations returns an HTTP <em>200</em> status code in case of success and a HTTP <em>4xx</em> or a <em>5xx</em> status code in case of failure. <br />
In case of failure the server returns a <strong>text/plain</strong> content type with the category and a description of the error occurred. <br />
<strong><em>NOTE:</em></strong> At present the embedded HTTP server doesn't implement neither HTTPS nor user authentication.</p>
<h3>Get Daemon Configuration</h3>
<ul>
<li><strong>URL</strong> /api/config </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> none </li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#config">Config params</a></li>
</ul>
<h3>Set Daemon Configuration</h3>
<ul>
<li><strong>URL</strong> /api/config </li>
<li><strong>Method</strong> POST </li>
<li><strong>URL Params</strong> none </li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#config">Config params</a></li>
</ul>
<h3>Get PTP Configuration</h3>
<ul>
<li><strong>URL</strong> /api/ptp/config </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> none </li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#ptp-config">PTP Config params</a></li>
</ul>
<h3>Set PTP Configuration</h3>
<ul>
<li><strong>URL</strong> /api/ptp/config</li>
<li><strong>Method</strong> POST</li>
<li><strong>URL Params</strong> none</li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#ptp-config">PTP Config params</a></li>
</ul>
<h3>Add RTP Source</h3>
<ul>
<li><strong>Description</strong> add or update the RTP source specified by the <em>id</em> </li>
<li><strong>URL</strong> /api/source/:id </li>
<li><strong>Method</strong> PUT </li>
<li><strong>URL Params</strong> id=[integer in the range (0-63)] </li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#rtp-source">RTP Source params</a></li>
</ul>
<h3>Remove RTP Source</h3>
<ul>
<li><strong>Description</strong> remove the RTP sink specified by the <em>id</em> </li>
<li><strong>URL</strong> /api/source/:id </li>
<li><strong>Method</strong> DELETE </li>
<li><strong>URL Params</strong> id=[integer in the range (0-63)] </li>
<li><strong>Body</strong> none </li>
</ul>
<h3>Get RTP Source SDP file</h3>
<ul>
<li><strong>Description</strong> retrieve the SDP of the source specified by <em>id</em> </li>
<li><strong>URL</strong> /api/source/sdp/:id </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> id=[integer in the range (0-63)] </li>
<li><strong>Body Type</strong> application/sdp </li>
<li><strong>Body</strong> <a href="#rtp-source-sdp">Example SDP file for a source</a></li>
</ul>
<h3>Add RTP Sink</h3>
<ul>
<li><strong>Description</strong> add or update the RTP sink specified by the <em>id</em> </li>
<li><strong>URL</strong> /api/sink/:id </li>
<li><strong>Method</strong> PUT </li>
<li><strong>URL Params</strong> id=[integer in the range (0-63)] </li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#rtp-sink">RTP Sink params</a></li>
</ul>
<h3>Remove RTP Sink</h3>
<ul>
<li><strong>Description</strong> remove the RTP sink specified by <em>id</em> </li>
<li><strong>URL</strong> /api/sink/:id </li>
<li><strong>Method</strong> DELETE </li>
<li><strong>URL Params</strong> id=[integer in the range (0-63)] </li>
<li><strong>Body</strong> none </li>
</ul>
<h3>Get RTP Sink status</h3>
<ul>
<li><strong>Description</strong> retrieve the status of the sink specified by <em>id</em></li>
<li><strong>URL</strong> /api/sink/status/:id </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> id=[integer in the range (0-63)] </li>
<li><strong>Body Type</strong> application/json </li>
<li><strong>Body</strong> <a href="#rtp-sink-status">RTP Sink status params</a></li>
</ul>
<h3>Get all configured RTP Sources</h3>
<ul>
<li><strong>URL</strong> /api/sources </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> none </li>
<li><strong>Body type</strong> application/json </li>
<li><strong>Body</strong> <a href="#rtp-sources">RTP Sources params</a></li>
</ul>
<h3>Get all configured RTP Sinks</h3>
<ul>
<li><strong>URL</strong> /api/sinks </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> none </li>
<li><strong>Body type</strong> application/json </li>
<li><strong>Body</strong> <a href="#rtp-sinks">RTP Sinks params</a></li>
</ul>
<h3>Get all configured RTP Sources and Sinks (Streams)</h3>
<ul>
<li><strong>URL</strong> /api/streams </li>
<li><strong>Method</strong> GET </li>
<li><strong>URL Params</strong> none </li>
<li><strong>Body type</strong> application/json </li>
<li><strong>Body</strong> <a href="#rtp-streams">RTP Streams params</a></li>
</ul>
<h2>HTTP REST API structures</h2>
<h3>JSON Config<a name="config"></a></h3>
<p>Example</p>
<pre><code>{
"interface_name": "lo",
"http_port": 8080,
"log_severity": 2,
"syslog_proto": "none",
"syslog_server": "255.255.255.254:1234",
"rtp_mcast_base": "239.2.0.1",
"status_file": "./status.json",
"rtp_port": "5004",
"ptp_domain": 0,
"ptp_dscp": 46,
"playout_delay": 0,
"frame_size_at_1fs": 192,
"sample_rate": 44100,
"max_tic_frame_size": 1024,
"sap_interval": 30,
"mac_addr": "01:00:5e:01:00:01",
"ip_addr": "127.0.0.1"
}
</code></pre>
<p>where:</p>
<blockquote>
<p><strong>interface_name</strong>
JSON string specifying the network interface used by the daemon and the driver for both the RTP, PTP, SAP and HTTP traffic.</p>
<p><strong>http_port</strong>
JSON number specifying the HTTP port number used by the embedded web server in the daemon implementing the REST interface.</p>
<p><strong>log_severity</strong>
JSON integer specifying the process log severity level (0 to 5). <br />
All traces major or equal to the specified level are enabled. (0=trace, 1=debug, 2=info, 3=warning, 4=error, 5=fatal).</p>
<p><strong>syslog_proto</strong>
JSON string specifying the syslog protocol used for logging. <br />
This can be an empty string to log to the local syslog, "udp" to send syslog messages
to a remote server or "none" to disable the syslog logging.
When "none" is used the client writes the logs to the standard output.</p>
<p><strong>syslog_server</strong>
JSON string specifying the syslog server address used for logging.</p>
<p><strong>status_file</strong>
JSON string specifying the file that will contain the sessions status. <br />
The file is loaded when the daemon starts and is saved when the daemon exits.</p>
<p><strong>rtp_mcast_base</strong>
JSON string specifying the default base RTP IPv4 multicast address used by a source. <br />
The specific multicast RTP address is the base address plus the source id number. <br />
For example if the base address is 239.2.0.1 and source id is 1 the RTP source address used is 239.2.0.2.</p>
<p><strong>rtp_port</strong>
JSON number specifying the RTP port used by the sources.</p>
<p><strong>ptp_domain</strong>
JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.</p>
<p><strong>ptp_dscp</strong>
JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
Valid values are 48 (CS6) and 46 (EF).</p>
<p><strong>sample_rate</strong>
JSON number specifying the default sample rate.
Valid values are 44100Hz, 48000Hz and 96000Hz.</p>
<p><strong>playout_delay</strong>
JSON number specifying the default safety playout delay at 1FS in samples.</p>
<p><strong>tic_frame_size_at_1fs</strong>
JSON number specifying the RTP frame size at 1FS in samples.</p>
<p><strong>max_tic_frame_size</strong>
JSON number specifying the max tick frame size. <br />
In case of a high value of <em>tic_frame_size_at_1fs</em>, this must be set to 8192.</p>
<p><strong>sap_interval</strong>
JSON number specifying the SAP interval to use. Use 0 for automatic and RFC compliant interval. Default is 30secs.</p>
<p><strong>mac_addr</strong>
JSON string specifying the MAC address of the specified network device.
<strong><em>NOTE:</em></strong> This parameter is read-only and cannot be set. The server will determine the MAC address of the network device at startup time.</p>
<p><strong>ip_addr</strong>
JSON string specifying the IP address of the specified network device.
<strong><em>NOTE:</em></strong> This parameter is read-only and cannot be set. The server will determine the IP address of the network device at startup time and will monitor it periodically.</p>
</blockquote>
<h3>JSON PTP Config<a name="ptp-config"></a></h3>
<p>Example</p>
<pre><code>{
"domain": 0,
"dscp": 46
}
</code></pre>
<p>where:</p>
<blockquote>
<p><strong>domain</strong>
JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to. </p>
<p><strong>dscp</strong>
JSON number specifying the IP DSCP used in IPv4 header for PTP traffic. <br />
Valid values are 48 (CS6) and 46 (EF).</p>
</blockquote>
<h3>JSON RTP source<a name="rtp-source"></a></h3>
<p>Example:</p>
<pre><code>{
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"codec": "L16",
"max_samples_per_packet": 48,
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": true,
"map": [0, 1]
}
</code></pre>
<p>where:</p>
<blockquote>
<p><strong>enabled</strong>
JSON bool specifying whether the source is enabled and will be announced or not. </p>
<p><strong>name</strong>
JSON string specifying the source name.</p>
<p><strong>io</strong>
JSON string specifying the IO name.</p>
<p><strong>codec</strong>
JSON string specifying codec in use.
Valid values are L16 and L24.</p>
<p><strong>max_sample_per_packet</strong>
JSON number specifying the max number of samples contained in one RTP packet.
Valid values are 12, 16, 18, 96, 192.</p>
<p><strong>ttl</strong>
JSON number specifying RTP packets Time To Live.</p>
<p><strong>payload_type</strong>
JSON number specifying RTP Payload Type.</p>
<p><strong>dscp</strong>
JSON number specifying the IP DSCP used in IPv4 header for RTP traffic.
Valid values are 46 (EF), 34 (AF41), 26 (AF31), 0 (BE).</p>
<p><strong>refclk_ptp_traceable</strong>
JSON boolean specifying whether the PTP reference clock is traceable or not.
A reference clock source is traceable if it is known to be delivering traceable time.</p>
<p><strong>map</strong>
JSON array of integers specifying the mapping between the RTP source and the ALSA playback device channels used during playback. The length of this map determines number of channels of the source.</p>
</blockquote>
<h3>RTP source SDP<a name="rtp-source-sdp"></a></h3>
<p>Example:</p>
<pre><code>v=0
o=- 0 0 IN IP4 127.0.0.1
s=ALSA Source 0
c=IN IP4 239.1.0.1/15
t=0 0
a=clock-domain:PTPv2 0
m=audio 5004 RTP/AVP 98
c=IN IP4 239.1.0.1/15
a=rtpmap:98 L16/44100/2
a=sync-time:0
a=framecount:48
a=ptime:1.08843537415
a=mediaclk:direct=0
a=ts-refclk:ptp=traceable
a=recvonly
</code></pre>
<h3>JSON RTP sink<a name="rtp-sink"></a></h3>
<p>Example:</p>
<pre><code>{
"name": "ALSA Sink 0",
"io": "Audio Device",
"delay": 576,
"use_sdp": false,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
"ignore_refclk_gmid": false,
"map": [0, 1]
}
</code></pre>
<p>where:</p>
<blockquote>
<p><strong>name</strong>
JSON string specifying the source name.</p>
<p><strong>io</strong>
JSON string specifying the IO name.</p>
<p><strong>delay</strong>
JSON number specifying the playout delay of the sink in samples.
This value <em>must</em> be larger than the source frame size.</p>
<p><strong>use_sdp</strong>
JSON boolean specifying whether the source SDP file is fetched from the HTTP URL specified in the <strong>source</strong> parameter or the SDP in the <strong>sdp</strong> parameter is used.</p>
<p><strong>source</strong>
JSON string specifying the HTTP URL of the source SDP file. This parameter is mandatory if <strong>use_sdp</strong> is false.</p>
<p><strong>sdp</strong>
JSON string specifying the SDP of the source. This parameter is mandatory if <strong>use_sdp</strong> is true.
See <a href="#rtp-source-sdp">example SDP file for a source</a></p>
<p><strong>ignore_refclk_gmid</strong>
JSON boolean specifying whether the grand master reference clock ID specified in the SDP file of the source must be compared with the master reference clock to which the current PTP slave clock is syncronized.</p>
<p><strong>map</strong>
JSON array of integers specifying the mapping between the RTP sink and the ALSA capture device channels used during recording. The length of this map determines number of channels of the sink.</p>
</blockquote>
<h3>JSON RTP sink status<a name="rtp-sink-status"></a></h3>
<p>Example:</p>
<pre><code>{
"sink_flags":
{
"rtp_seq_id_error": false,
"rtp_ssrc_error": false,
"rtp_payload_type_error": false,
"rtp_sac_error": false,
"receiving_rtp_packet": false,
"some_muted": false,
"muted": true
},
"sink_min_time": 0
</code></pre>
<p>}</p>
<p>where:</p>
<blockquote>
<p><strong>sink_flags</strong>
JSON object containing a set of flags reporting the RTP sink status. </p>
<ul>
<li><p><strong>rtp_seq_id_error</strong> JSON boolean specifying whether a wrong RTP sequence was detected. </p></li>
<li><p><strong>rtp_ssrc_error</strong> JSON boolean specifying whether a wrong RTP source is contributing to the incoming stream. </p></li>
<li><p><strong>rtp_payload_type_error</strong> JSON boolean specifying whether a wrong payload type was received. </p></li>
<li><p><strong>rtp_sac_error</strong> JSON boolean specifying whether a packet with a wrong RTP timestamp was received.</p></li>
<li><p><strong>receiving_rtp_packet</strong> JSON boolean specifying whether the sink is currently receiving RTP packets from the source. </p></li>
<li><p><strong>some_muted</strong> JSON boolean (not used)</p></li>
<li><p><strong>muted</strong> JSON boolean specifying whether the sink is currently muted.</p></li>
</ul>
<p><strong>sink_min_time</strong> JSON number specifying the minimum source RTP packet arrival time. </p>
</blockquote>
<h3>JSON RTP Sources<a name="rtp-sources"></a></h3>
<p>Example:</p>
<pre><code>{
"sources": [
{
"id": 0,
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"max_samples_per_packet": 48,
"codec": "L16",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": true,
"map": [ 0, 1 ]
} ]
</code></pre>
<p>}</p>
<p>where:</p>
<blockquote>
<p><strong>sources</strong>
JSON array of the configured sources.
Every source is identified by the JSON number <strong>id</strong> (in the range 0 - 63).
See <a href="#rtp-source">RTP Source params</a> for all the other parameters.</p>
</blockquote>
<h3>JSON RTP Sinks<a name="rtp-sinks"></a></h3>
<p>Example:</p>
<pre><code>{
"sinks": [
{
"id": 0,
"name": "ALSA Sink 0",
"io": "Audio Device",
"use_sdp": true,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
"delay": 576,
"ignore_refclk_gmid": false,
"map": [ 0, 1 ]
} ]
</code></pre>
<p>}</p>
<p>where:</p>
<blockquote>
<p><strong>sinks</strong>
JSON array of the configured sinks.
Every sink is identified by the JSON number <strong>id</strong> (in the range 0 - 63).
See <a href="#rtp-sink">RTP Sink params</a> for all the other parameters.</p>
</blockquote>
<h3>JSON RTP Streams<a name="rtp-streams"></a></h3>
<p>Example:</p>
<pre><code>{
"sources": [
{
"id": 0,
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"max_samples_per_packet": 48,
"codec": "L16",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": true,
"map": [ 0, 1 ]
} ],
"sinks": [
{
"id": 0,
"name": "ALSA Sink 0",
"io": "Audio Device",
"use_sdp": true,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
"delay": 576,
"ignore_refclk_gmid": false,
"map": [ 0, 1 ]
} ]
}
</code></pre>
<p>where:</p>
<blockquote>
<p><strong>sources</strong>
JSON array of the configured sources.
Every source is identified by the JSON number <strong>id</strong> (in the range 0 - 63).
See <a href="#rtp-source">RTP Source params</a> for all the other parameters.</p>
<p><strong>sinks</strong>
JSON array of the configured sinks.
Every sink is identified by the JSON number <strong>id</strong> (in the range 0 - 63).
See <a href="#rtp-sink">RTP Sink params</a> for all the other parameters.</p>
</blockquote>

486
daemon/README.md Normal file
View File

@ -0,0 +1,486 @@
# AES67 Daemon #
AES67 daemon uses the Merging Technologies device driver (MergingRavennaALSA.ko) to implement basic AES67 functionalities. See [ALSA RAVENNA/AES67 Driver README](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md) for additional information.
The daemon is responsible for:
* communication and configuration of the device driver
* provide an HTTP REST API for the daemon control and configuration
* session handling and SDP parsing and creation
* SAP discovery protocol implementation
* IGMP handling for SAP and RTP sessions
## Configuration file ##
The daemon uses a JSON file to store the configuration parameters.
The config file must be specified at startup time and it gets updated when new params are set via the REST interface.
See [JSON config params](#config) for additional info on the configuration parameters.
If a status file is specified in the daemon's configuration the server will load it at startup ad will save it at termination.
The status file contains all the configured sources and sinks (streams).
See [JSON streams](#rtp-streams) for additional info on the status file format and its parameters.
## HTTP REST API ##
The daemon implements a REST API interface to configure and control the driver.
All operations returns HTTP *200* status code in case of success and HTTP *4xx* or *5xx* status code in case of failure.
In case of failure the server returns a **text/plain** content type with the category and a description of the error occurred.
**_NOTE:_** At present the embedded HTTP server doesn't implement neither HTTPS nor user authentication.
### Get Daemon Configuration ###
* **URL** /api/config
* **Method** GET
* **URL Params** none
* **Body Type** application/json
* **Body** [Config params](#config)
### Set Daemon Configuration ###
* **URL** /api/config
* **Method** POST
* **URL Params** none
* **Body Type** application/json
* **Body** [Config params](#config)
### Get PTP Configuration ###
* **URL** /api/ptp/config
* **Method** GET
* **URL Params** none
* **Body Type** application/json
* **Body** [PTP Config params](#ptp-config)
### Set PTP Configuration ###
* **URL** /api/ptp/config
* **Method** POST
* **URL Params** none
* **Body Type** application/json
* **Body** [PTP Config params](#ptp-config)
### Add RTP Source ###
* **Description** add or update the RTP source specified by the *id*
* **URL** /api/source/:id
* **Method** PUT
* **URL Params** id=[integer in the range (0-63)]
* **Body Type** application/json
* **Body** [RTP Source params](#rtp-source)
### Remove RTP Source ###
* **Description** remove the RTP sink specified by the *id*
* **URL** /api/source/:id
* **Method** DELETE
* **URL Params** id=[integer in the range (0-63)]
* **Body** none
### Get RTP Source SDP file ###
* **Description** retrieve the SDP of the source specified by *id*
* **URL** /api/source/sdp/:id
* **Method** GET
* **URL Params** id=[integer in the range (0-63)]
* **Body Type** application/sdp
* **Body** [Example SDP file for a source](#rtp-source-sdp)
### Add RTP Sink ###
* **Description** add or update the RTP sink specified by the *id*
* **URL** /api/sink/:id
* **Method** PUT
* **URL Params** id=[integer in the range (0-63)]
* **Body Type** application/json
* **Body** [RTP Sink params](#rtp-sink)
### Remove RTP Sink ###
* **Description** remove the RTP sink specified by *id*
* **URL** /api/sink/:id
* **Method** DELETE
* **URL Params** id=[integer in the range (0-63)]
* **Body** none
### Get RTP Sink status ###
* **Description** retrieve the status of the sink specified by *id*
* **URL** /api/sink/status/:id
* **Method** GET
* **URL Params** id=[integer in the range (0-63)]
* **Body Type** application/json
* **Body** [RTP Sink status params](#rtp-sink-status)
### Get all configured RTP Sources ###
* **URL** /api/sources
* **Method** GET
* **URL Params** none
* **Body type** application/json
* **Body** [RTP Sources params](#rtp-sources)
### Get all configured RTP Sinks ###
* **URL** /api/sinks
* **Method** GET
* **URL Params** none
* **Body type** application/json
* **Body** [RTP Sinks params](#rtp-sinks)
### Get all configured RTP Sources and Sinks (Streams) ###
* **URL** /api/streams
* **Method** GET
* **URL Params** none
* **Body type** application/json
* **Body** [RTP Streams params](#rtp-streams)
## HTTP REST API structures ##
### JSON Config<a name="config"></a> ###
Example
{
"interface_name": "lo",
"http_port": 8080,
"log_severity": 2,
"syslog_proto": "none",
"syslog_server": "255.255.255.254:1234",
"rtp_mcast_base": "239.2.0.1",
"status_file": "./status.json",
"rtp_port": "5004",
"ptp_domain": 0,
"ptp_dscp": 46,
"playout_delay": 0,
"frame_size_at_1fs": 192,
"sample_rate": 44100,
"max_tic_frame_size": 1024,
"sap_interval": 30,
"mac_addr": "01:00:5e:01:00:01",
"ip_addr": "127.0.0.1"
}
where:
> **interface\_name**
> JSON string specifying the network interface used by the daemon and the driver for both the RTP, PTP, SAP and HTTP traffic.
> **http\_port**
> JSON number specifying the HTTP port number used by the embedded web server in the daemon implementing the REST interface.
> **log\_severity**
> JSON integer specifying the process log severity level (0 to 5).
> All traces major or equal to the specified level are enabled. (0=trace, 1=debug, 2=info, 3=warning, 4=error, 5=fatal).
> **syslog\_proto**
> JSON string specifying the syslog protocol used for logging.
> This can be an empty string to log to the local syslog, "udp" to send syslog messages
> to a remote server or "none" to disable the syslog logging.
> When "none" is used the client writes the logs to the standard output.
> **syslog\_server**
> JSON string specifying the syslog server address used for logging.
> **status\_file**
> JSON string specifying the file that will contain the sessions status.
> The file is loaded when the daemon starts and is saved when the daemon exits.
> **rtp\_mcast\_base**
> JSON string specifying the default base RTP IPv4 multicast address used by a source.
> The specific multicast RTP address is the base address plus the source id number.
> For example if the base address is 239.2.0.1 and source id is 1 the RTP source address used is 239.2.0.2.
> **rtp_port**
> JSON number specifying the RTP port used by the sources.
> **ptp\_domain**
> JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.
> **ptp\_dscp**
> JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
> Valid values are 48 (CS6) and 46 (EF).
> **sample\_rate**
> JSON number specifying the default sample rate.
> Valid values are 44100Hz, 48000Hz and 96000Hz.
> **playout\_delay**
> JSON number specifying the default safety playout delay at 1FS in samples.
> **tic\_frame\_size\_at\_1fs**
> JSON number specifying the RTP frame size at 1FS in samples.
> **max\_tic\_frame\_size**
> JSON number specifying the max tick frame size.
> In case of a high value of *tic_frame_size_at_1fs*, this must be set to 8192.
> **sap\_interval**
> JSON number specifying the SAP interval to use. Use 0 for automatic and RFC compliant interval. Default is 30secs.
> **mac\_addr**
> JSON string specifying the MAC address of the specified network device.
> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the MAC address of the network device at startup time.
> **ip\_addr**
> JSON string specifying the IP address of the specified network device.
> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the IP address of the network device at startup time and will monitor it periodically.
### JSON PTP Config<a name="ptp-config"></a> ###
Example
{
"domain": 0,
"dscp": 46
}
where:
> **domain**
> JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.
> **dscp**
> JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
> Valid values are 48 (CS6) and 46 (EF).
### JSON RTP source<a name="rtp-source"></a> ###
Example:
{
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"codec": "L16",
"max_samples_per_packet": 48,
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": true,
"map": [0, 1]
}
where:
> **enabled**
> JSON bool specifying whether the source is enabled and will be announced or not.
> **name**
> JSON string specifying the source name.
> **io**
> JSON string specifying the IO name.
> **codec**
> JSON string specifying codec in use.
> Valid values are L16 and L24.
> **max\_sample\_per\_packet**
> JSON number specifying the max number of samples contained in one RTP packet.
> Valid values are 12, 16, 18, 96, 192.
> **ttl**
> JSON number specifying RTP packets Time To Live.
> **payload\_type**
> JSON number specifying RTP Payload Type.
> **dscp**
> JSON number specifying the IP DSCP used in IPv4 header for RTP traffic.
> Valid values are 46 (EF), 34 (AF41), 26 (AF31), 0 (BE).
> **refclk\_ptp\_traceable**
> JSON boolean specifying whether the PTP reference clock is traceable or not.
> A reference clock source is traceable if it is known to be delivering traceable time.
> **map**
> JSON array of integers specifying the mapping between the RTP source and the ALSA playback device channels used during playback. The length of this map determines number of channels of the source.
### RTP source SDP<a name="rtp-source-sdp"></a> ###
Example:
v=0
o=- 0 0 IN IP4 127.0.0.1
s=ALSA Source 0
c=IN IP4 239.1.0.1/15
t=0 0
a=clock-domain:PTPv2 0
m=audio 5004 RTP/AVP 98
c=IN IP4 239.1.0.1/15
a=rtpmap:98 L16/44100/2
a=sync-time:0
a=framecount:48
a=ptime:1.08843537415
a=mediaclk:direct=0
a=ts-refclk:ptp=traceable
a=recvonly
### JSON RTP sink<a name="rtp-sink"></a> ###
Example:
{
"name": "ALSA Sink 0",
"io": "Audio Device",
"delay": 576,
"use_sdp": false,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
"ignore_refclk_gmid": false,
"map": [0, 1]
}
where:
> **name**
> JSON string specifying the source name.
> **io**
> JSON string specifying the IO name.
> **delay**
> JSON number specifying the playout delay of the sink in samples.
> This value *must* be larger than the source frame size.
> **use\_sdp**
> JSON boolean specifying whether the source SDP file is fetched from the HTTP URL specified in the **source** parameter or the SDP in the **sdp** parameter is used.
> **source**
> JSON string specifying the HTTP URL of the source SDP file. This parameter is mandatory if **use\_sdp** is false.
> **sdp**
> JSON string specifying the SDP of the source. This parameter is mandatory if **use\_sdp** is true.
> See [example SDP file for a source](#rtp-source-sdp)
> **ignore\_refclk\_gmid**
> JSON boolean specifying whether the grand master reference clock ID specified in the SDP file of the source must be compared with the master reference clock to which the current PTP slave clock is syncronized.
> **map**
> JSON array of integers specifying the mapping between the RTP sink and the ALSA capture device channels used during recording. The length of this map determines number of channels of the sink.
### JSON RTP sink status<a name="rtp-sink-status"></a> ###
Example:
{
"sink_flags":
{
"rtp_seq_id_error": false,
"rtp_ssrc_error": false,
"rtp_payload_type_error": false,
"rtp_sac_error": false,
"receiving_rtp_packet": false,
"some_muted": false,
"muted": true
},
"sink_min_time": 0
}
where:
> **sink\_flags**
> JSON object containing a set of flags reporting the RTP sink status.
> - **rtp\_seq\_id\_error** JSON boolean specifying whether a wrong RTP sequence was detected.
> - **rtp\_ssrc\_error** JSON boolean specifying whether a wrong RTP source is contributing to the incoming stream.
> - **rtp\_payload\_type\_error** JSON boolean specifying whether a wrong payload type was received.
> - **rtp\_sac\_error** JSON boolean specifying whether a packet with a wrong RTP timestamp was received.
> - **receiving\_rtp\_packet** JSON boolean specifying whether the sink is currently receiving RTP packets from the source.
> - **some\_muted** JSON boolean (not used)
> - **muted** JSON boolean specifying whether the sink is currently muted.
> **sink\_min\_time** JSON number specifying the minimum source RTP packet arrival time.
### JSON RTP Sources<a name="rtp-sources"></a> ###
Example:
{
"sources": [
{
"id": 0,
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"max_samples_per_packet": 48,
"codec": "L16",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": true,
"map": [ 0, 1 ]
} ]
}
where:
> **sources**
> JSON array of the configured sources.
> Every source is identified by the JSON number **id** (in the range 0 - 63).
> See [RTP Source params](#rtp-source) for all the other parameters.
### JSON RTP Sinks<a name="rtp-sinks"></a> ###
Example:
{
"sinks": [
{
"id": 0,
"name": "ALSA Sink 0",
"io": "Audio Device",
"use_sdp": true,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
"delay": 576,
"ignore_refclk_gmid": false,
"map": [ 0, 1 ]
} ]
}
where:
> **sinks**
> JSON array of the configured sinks.
> Every sink is identified by the JSON number **id** (in the range 0 - 63).
> See [RTP Sink params](#rtp-sink) for all the other parameters.
### JSON RTP Streams<a name="rtp-streams"></a> ###
Example:
{
"sources": [
{
"id": 0,
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"max_samples_per_packet": 48,
"codec": "L16",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": true,
"map": [ 0, 1 ]
} ],
"sinks": [
{
"id": 0,
"name": "ALSA Sink 0",
"io": "Audio Device",
"use_sdp": true,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
"delay": 576,
"ignore_refclk_gmid": false,
"map": [ 0, 1 ]
} ]
}
where:
> **sources**
> JSON array of the configured sources.
> Every source is identified by the JSON number **id** (in the range 0 - 63).
> See [RTP Source params](#rtp-source) for all the other parameters.
> **sinks**
> JSON array of the configured sinks.
> Every sink is identified by the JSON number **id** (in the range 0 - 63).
> See [RTP Sink params](#rtp-sink) for all the other parameters.

106
daemon/config.cpp Normal file
View File

@ -0,0 +1,106 @@
//
// config.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <boost/asio.hpp>
#include <boost/foreach.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include "config.hpp"
#include "interface.hpp"
#include "json.hpp"
using namespace boost::asio;
std::shared_ptr<Config> Config::parse(const std::string& filename) {
Config config;
std::ifstream jsonstream(filename);
if (!jsonstream) {
std::cerr << "Fatal: error opening config file " << filename << std::endl;
return nullptr;
}
try {
config = json_to_config(jsonstream);
} catch (std::exception const& e) {
std::cerr << "Configuration file " << filename << " : " << e.what()
<< std::endl;
return nullptr;
}
if (config.log_severity_ < 0 || config.log_severity_ > 5)
config.log_severity_ = 2;
if (config.playout_delay_ > 4000)
config.playout_delay_ = 4000;
if (config.tic_frame_size_at_1fs_ == 0 ||
config.tic_frame_size_at_1fs_ > 8192)
config.tic_frame_size_at_1fs_ = 192;
if (config.max_tic_frame_size_ == 0 || config.max_tic_frame_size_ > 8192)
config.max_tic_frame_size_ = 1024;
if (config.sample_rate_ == 0)
config.sample_rate_ = 44100;
if (ip::address_v4::from_string(config.rtp_mcast_base_.c_str()).to_ulong() ==
INADDR_NONE)
config.rtp_mcast_base_ = "239.1.0.1";
auto [mac_addr, mac_str] = get_interface_mac(config.interface_name_);
if (mac_str.empty()) {
std::cerr << "Cannot retrieve MAC address for interface "
<< config.interface_name_ << std::endl;
return nullptr;
}
config.mac_addr_ = mac_addr;
config.mac_str_ = mac_str;
auto [ip_addr, ip_str] = get_interface_ip(config.interface_name_);
if (ip_str.empty()) {
std::cerr << "Cannot retrieve IPv4 address for interface "
<< config.interface_name_ << std::endl;
return nullptr;
}
config.ip_addr_ = ip_addr;
config.ip_str_ = ip_str;
config.config_filename_ = filename;
config.need_restart_ = false;
return std::make_shared<Config>(config);
}
bool Config::save(const Config& config, bool need_restart) {
std::ofstream js(config_filename_);
if (!js) {
BOOST_LOG_TRIVIAL(fatal)
<< "Config:: cannot save to file " << config_filename_;
return false;
}
js << config_to_json(config);
BOOST_LOG_TRIVIAL(info) << "Config:: file saved";
need_restart_ = need_restart;
return true;
}

131
daemon/config.hpp Normal file
View File

@ -0,0 +1,131 @@
//
// config.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _CONFIG_HPP_
#define _CONFIG_HPP_
#include <cstdint>
#include <memory>
#include <string>
class Config {
public:
/* save new config to json file */
bool save(const Config& config, bool need_restart = true);
/* build config from json file */
static std::shared_ptr<Config> parse(const std::string& filename);
/* attributes retrieved from config json */
uint16_t get_http_port() const { return http_port_; };
const std::string get_http_base_dir() const { return http_base_dir_; };
int get_log_severity() const { return log_severity_; };
uint32_t get_playout_delay() const { return playout_delay_; };
uint32_t get_tic_frame_size_at_1fs() const { return tic_frame_size_at_1fs_; };
uint32_t get_max_tic_frame_size() const { return max_tic_frame_size_; };
uint32_t get_sample_rate() const { return sample_rate_; };
const std::string& get_rtp_mcast_base() const { return rtp_mcast_base_; };
uint16_t get_rtp_port() const { return rtp_port_; };
uint8_t get_ptp_domain() const { return ptp_domain_; };
uint8_t get_ptp_dscp() const { return ptp_dscp_; };
uint16_t get_sap_interval() const { return sap_interval_; };
const std::string& get_syslog_proto() const { return syslog_proto_; };
const std::string& get_syslog_server() const { return syslog_server_; };
const std::string& get_status_file() const { return status_file_; };
const std::string& get_interface_name() const { return interface_name_; };
const std::string& get_config_filename() const { return config_filename_; };
/* attributes set during init */
const std::array<uint8_t, 6>& get_mac_addr() const { return mac_addr_; };
const std::string& get_mac_addr_str() const { return mac_str_; };
uint32_t get_ip_addr() const { return ip_addr_; };
const std::string& get_ip_addr_str() const { return ip_str_; };
bool get_need_restart() const { return need_restart_; };
void set_http_port(uint16_t http_port) { http_port_ = http_port; };
void set_http_base_dir(const std::string& http_base_dir) { http_base_dir_ = http_base_dir; };
void set_log_severity(int log_severity) { log_severity_ = log_severity; };
void set_playout_delay(uint32_t playout_delay) {
playout_delay_ = playout_delay;
};
void set_tic_frame_size_at_1fs(uint32_t tic_frame_size_at_1fs) {
tic_frame_size_at_1fs_ = tic_frame_size_at_1fs;
};
void set_max_tic_frame_size(uint32_t max_tic_frame_size) {
max_tic_frame_size_ = max_tic_frame_size;
};
void set_sample_rate(uint32_t sample_rate) { sample_rate_ = sample_rate; };
void set_rtp_mcast_base(const std::string& rtp_mcast_base) {
rtp_mcast_base_ = rtp_mcast_base;
};
void set_rtp_port(uint16_t rtp_port) { rtp_port_ = rtp_port; };
void set_ptp_domain(uint8_t ptp_domain) { ptp_domain_ = ptp_domain; };
void set_ptp_dscp(uint8_t ptp_dscp) { ptp_dscp_ = ptp_dscp; };
void set_sap_interval(uint16_t sap_interval) {
sap_interval_ = sap_interval;
};
void set_syslog_proto(const std::string& syslog_proto) {
syslog_proto_ = syslog_proto;
};
void set_syslog_server(const std::string& syslog_server) {
syslog_server_ = syslog_server;
};
void set_status_file(const std::string& status_file) {
status_file_ = status_file;
};
void set_interface_name(const std::string& interface_name) {
interface_name_ = interface_name;
};
void set_ip_addr_str(const std::string& ip_str) { ip_str_ = ip_str; };
void set_ip_addr(uint32_t ip_addr) { ip_addr_ = ip_addr; };
void set_mac_addr_str(const std::string& mac_str) { mac_str_ = mac_str; };
void set_mac_addr(const std::array<uint8_t, 6>& mac_addr) {
mac_addr_ = mac_addr;
};
private:
/* from json */
uint16_t http_port_{8080};
std::string http_base_dir_{"../webui/build"};
int log_severity_{2};
uint32_t playout_delay_{0};
uint32_t tic_frame_size_at_1fs_{512};
uint32_t max_tic_frame_size_{1024};
uint32_t sample_rate_{44100};
std::string rtp_mcast_base_{"239.1.0.1"};
uint16_t rtp_port_{5004};
uint8_t ptp_domain_{0};
uint8_t ptp_dscp_{46};
uint16_t sap_interval_{300};
std::string syslog_proto_{""};
std::string syslog_server_{""};
std::string status_file_{"./status.json"};
std::string interface_name_{"eth0"};
/* set during init */
std::array<uint8_t, 6> mac_addr_{0, 0, 0, 0, 0, 0};
std::string mac_str_;
uint32_t ip_addr_{0};
std::string ip_str_;
std::string config_filename_;
/* recofing needs daemon need_restart */
bool need_restart_{false};
};
#endif

18
daemon/daemon.conf Normal file
View File

@ -0,0 +1,18 @@
{
"http_port": 8080,
"http_base_dir": "../webui/build",
"log_severity": 2,
"playout_delay": 0,
"tic_frame_size_at_1fs": 192,
"max_tic_frame_size": 1024,
"sample_rate": 44100,
"rtp_mcast_base": "239.1.0.1",
"rtp_port": 5004,
"ptp_domain": 0,
"ptp_dscp": 48,
"sap_interval": 30,
"syslog_proto": "none",
"syslog_server": "255.255.255.254:1234",
"status_file": "./status.json",
"interface_name": "lo"
}

201
daemon/driver_handler.cpp Normal file
View File

@ -0,0 +1,201 @@
//
// driver_handler.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <iostream>
#include <thread>
#include "driver_handler.hpp"
#include "log.hpp"
/*
void dump(const void* mem, unsigned int n) {
const char* p = reinterpret_cast<const char*>(mem);
for (unsigned int i = 0; i < n; i++) {
std::cout << std::hex << int(p[i]) << " ";
}
std::cout << std::endl;
}
*/
bool DriverHandler::init(const Config& /* config */) {
if (running_) {
return true;
}
try {
client_u2k_.init(nl_endpoint<nl_protocol>(0), nl_protocol(NETLINK_U2K_ID));
client_k2u_.init(nl_endpoint<nl_protocol>(0), nl_protocol(NETLINK_K2U_ID));
running_ = true;
res_ = std::async(std::launch::async, &DriverHandler::event_receiver, this);
return true;
} catch (const boost::system::system_error& se) {
BOOST_LOG_TRIVIAL(fatal) << "driver_handler:: init " << se.what();
BOOST_LOG_TRIVIAL(fatal) << "Kernel module not loaded ?";
return false;
}
}
void DriverHandler::send(enum MT_ALSA_msg_id id,
NetlinkClient& client,
uint8_t* buffer,
size_t data_size,
const uint8_t* data) {
struct MT_ALSA_msg alsa_msg;
memset(&alsa_msg, 0, sizeof(alsa_msg));
alsa_msg.id = id;
alsa_msg.errCode = 0;
alsa_msg.dataSize = data_size;
struct nlmsghdr* nlh = (struct nlmsghdr*)buffer;
nlh->nlmsg_len =
sizeof(struct nlmsghdr) + sizeof(struct MT_ALSA_msg) + data_size;
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = 0;
nlh->nlmsg_type = NLMSG_DONE;
memcpy(NLMSG_DATA(nlh), &alsa_msg, sizeof(struct MT_ALSA_msg));
if (data != nullptr && data_size > 0) {
memcpy(reinterpret_cast<uint8_t*>(NLMSG_DATA(nlh)) +
sizeof(struct MT_ALSA_msg),
data, data_size);
}
nl_endpoint<nl_protocol> kernel_endpoint(0, 0); /* For Linux Kernel */
client.get_socket().send_to(boost::asio::buffer(nlh, nlh->nlmsg_len),
kernel_endpoint);
}
bool DriverHandler::event_receiver() {
while (running_) {
boost::system::error_code ec;
auto bytes =
client_k2u_.receive(boost::asio::buffer(event_buffer_, max_payload),
boost::posix_time::seconds(reply_timeout_secs), ec);
if (ec) {
if (ec != boost::asio::error::operation_aborted) {
BOOST_LOG_TRIVIAL(fatal)
<< "driver_handler::k2u_receive " << ec.message();
return false;
}
}
for (struct nlmsghdr* nlh = (nlmsghdr*)event_buffer_;
NLMSG_OK(nlh, (size_t)bytes); nlh = NLMSG_NEXT(nlh, bytes)) {
if (nlh->nlmsg_type == NLMSG_DONE) {
struct MT_ALSA_msg* palsa_msg =
reinterpret_cast<struct MT_ALSA_msg*> NLMSG_DATA(nlh);
BOOST_LOG_TRIVIAL(debug)
<< "driver_handler:: received event code " << palsa_msg->id
<< " error " << palsa_msg->errCode << " data len "
<< palsa_msg->dataSize;
if (palsa_msg->errCode == 0) {
size_t res_size = sizeof(int32_t);
uint8_t res[sizeof(int32_t)];
memset(res, 0, res_size);
on_event(palsa_msg->id, res_size, res, palsa_msg->dataSize,
reinterpret_cast<const uint8_t*>(palsa_msg) + data_offset);
BOOST_LOG_TRIVIAL(debug) << "driver_handler::sending event response "
<< palsa_msg->id << " data len " << res_size;
memset(response_buffer_, 0, sizeof(response_buffer_));
try {
send(palsa_msg->id, client_k2u_, response_buffer_, res_size, res);
} catch (boost::system::error_code& ec) {
BOOST_LOG_TRIVIAL(error)
<< "driver_handler::k2u_send_to " << ec.message();
on_event_error(palsa_msg->id, DaemonErrc::send_u2k_failed);
}
} else {
on_event_error(palsa_msg->id, get_driver_error(palsa_msg->errCode));
}
}
}
}
return true;
}
bool DriverHandler::terminate() {
if (running_) {
running_ = false;
client_u2k_.terminate();
client_k2u_.terminate();
return res_.get();
}
return true;
}
void DriverHandler::send_command(enum MT_ALSA_msg_id id,
size_t data_size,
const uint8_t* data) {
std::lock_guard<std::mutex> lock(mutex_);
if (data_size > max_payload) {
on_command_error(id, DaemonErrc::send_invalid_size);
return;
}
BOOST_LOG_TRIVIAL(debug) << "driver_handler:: sending command code " << id
<< " data len " << data_size;
memset(command_buffer_, 0, sizeof(command_buffer_));
try {
send(id, client_u2k_, command_buffer_, data_size, data);
} catch (boost::system::error_code& ec) {
BOOST_LOG_TRIVIAL(error) << "driver_handler:: u2k_send_to " << ec.message();
on_command_error(id, DaemonErrc::send_u2k_failed);
return;
}
BOOST_LOG_TRIVIAL(debug) << "driver_handler:: command code " << id
<< " data len " << data_size << " sent";
boost::system::error_code ec;
auto bytes =
client_u2k_.receive(boost::asio::buffer(command_buffer_, max_payload),
boost::posix_time::seconds(reply_timeout_secs), ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "driver_handler:: u2k_receive " << ec.message();
on_command_error(id, DaemonErrc::receive_u2k_failed);
return;
}
for (struct nlmsghdr* nlh = (nlmsghdr*)command_buffer_;
NLMSG_OK(nlh, (size_t)bytes); nlh = NLMSG_NEXT(nlh, bytes)) {
if (nlh->nlmsg_type == NLMSG_DONE) {
struct MT_ALSA_msg* palsa_msg =
reinterpret_cast<struct MT_ALSA_msg*> NLMSG_DATA(nlh);
BOOST_LOG_TRIVIAL(debug)
<< "driver_handler:: received cmd code " << palsa_msg->id << " error "
<< palsa_msg->errCode << " data len " << palsa_msg->dataSize;
if (id != palsa_msg->id) {
BOOST_LOG_TRIVIAL(warning) << "driver_handler:: unexpected cmd response:"
<< "sent " << id << " received " << palsa_msg->id;
on_command_error(palsa_msg->id, DaemonErrc::invalid_driver_response);
} else {
if (palsa_msg->errCode == 0) {
// dump((uint8_t*)palsa_msg + data_offset, palsa_msg->dataSize);
on_command_done(
palsa_msg->id, palsa_msg->dataSize,
reinterpret_cast<const uint8_t*>(palsa_msg) + data_offset);
} else {
on_command_error(palsa_msg->id, get_driver_error(palsa_msg->errCode));
}
}
}
}
}

81
daemon/driver_handler.hpp Normal file
View File

@ -0,0 +1,81 @@
//
// driver_handler.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _DRIVER_HANDLER_HPP_
#define _DRIVER_HANDLER_HPP_
#include <future>
#include "MT_ALSA_message_defs.h"
#include "config.hpp"
#include "log.hpp"
#include "netlink_client.hpp"
#include "error_code.hpp"
class DriverHandler {
public:
static constexpr size_t max_payload = MAX_PAYLOAD;
static constexpr off_t data_offset =
sizeof(struct MT_ALSA_msg) /*+ sizeof(int)*/;
static constexpr int reply_timeout_secs = 1; // 1sec in driver
static constexpr size_t buffer_size =
NLMSG_SPACE(max_payload) + sizeof(struct MT_ALSA_msg);
DriverHandler(){};
DriverHandler(const DriverHandler&) = delete;
DriverHandler& operator=(const DriverHandler&) = delete;
virtual ~DriverHandler(){ terminate(); };
virtual bool init(const Config& config);
virtual bool terminate();
virtual void send_command(enum MT_ALSA_msg_id id,
size_t size = 0,
const uint8_t* data = nullptr);
virtual void on_command_done(enum MT_ALSA_msg_id id,
size_t size = 0,
const uint8_t* data = nullptr) = 0;
virtual void on_command_error(enum MT_ALSA_msg_id id,
std::error_code error) = 0;
virtual void on_event(enum MT_ALSA_msg_id id,
size_t& res_size,
uint8_t* res,
size_t req_size = 0,
const uint8_t* req = nullptr) = 0;
virtual void on_event_error(enum MT_ALSA_msg_id id,
std::error_code error) = 0;
private:
void send(enum MT_ALSA_msg_id id,
NetlinkClient& client,
uint8_t* buffer,
size_t data_size,
const uint8_t* data);
bool event_receiver();
std::future<bool> res_;
std::atomic_bool running_{false};
uint8_t command_buffer_[buffer_size];
uint8_t event_buffer_[buffer_size];
uint8_t response_buffer_[buffer_size];
NetlinkClient client_u2k_{"commands"}; /* u2k for commands */
NetlinkClient client_k2u_{"events"}; /* k2u for events */
std::mutex mutex_; /* one command at a time */
};
#endif

335
daemon/driver_manager.cpp Normal file
View File

@ -0,0 +1,335 @@
//
// driver_manager.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <thread>
#include "driver_manager.hpp"
#include "log.hpp"
static const std::vector<std::string> alsa_msg_str = {
"Start",
"Stop",
"Reset",
"StartIO",
"StopIO",
"SetSampleRate",
"GetSampleRate",
"GetAudioMode",
"SetDSDAudioMode",
"SetTICFrameSizeAt1FS",
"SetMaxTICFrameSize",
"SetNumberOfInputs",
"SetNumberOfOutputs",
"GetNumberOfInputs",
"GetNumberOfOutputs",
"SetInterfaceName",
"Add_RTPStream",
"Remove_RTPStream",
"Update_RTPStream_Name",
"GetPTPInfo",
"Hello",
"Bye",
"Ping",
"SetMasterOutputVolume",
"SetMasterOutputSwitch",
"GetMasterOutputVolume",
"GetMasterOutputSwitch",
"SetPlayoutDelay",
"SetCaptureDelay",
"GetRTPStreamStatus",
"SetPTPConfig",
"GetPTPConfig",
"GetPTPStatus"
};
static const std::vector<std::string> ptp_status_str = {
"unlocked",
"locking",
"locked"
};
std::shared_ptr<DriverManager> DriverManager::create() {
// no need to be thread-safe here
static std::weak_ptr<DriverManager> instance;
if (auto ptr = instance.lock()) {
return ptr;
}
auto ptr = std::shared_ptr<DriverManager>(new DriverManager());
instance = ptr;
return ptr;
}
bool DriverManager::init(const Config& config) {
if (!DriverHandler::init(config)) {
return false;
}
sample_rate = config.get_sample_rate();
TPTPConfig ptp_config;
ptp_config.ui8Domain = config.get_ptp_domain();
ptp_config.ui8DSCP = config.get_ptp_dscp();
bool res = hello() ||
start() ||
reset() ||
set_interface_name(config.get_interface_name()) ||
set_ptp_config(ptp_config) ||
set_tic_frame_size_at_1fs(config.get_tic_frame_size_at_1fs()) ||
set_playout_delay(config.get_playout_delay()) ||
set_max_tic_frame_size(config.get_max_tic_frame_size());
return !res;
}
bool DriverManager::terminate() {
stop();
bye();
return DriverHandler::terminate();
}
std::error_code DriverManager::hello() {
this->send_command(MT_ALSA_Msg_Hello, 0, nullptr);
return retcode_;
}
std::error_code DriverManager::bye() {
this->send_command(MT_ALSA_Msg_Bye, 0, nullptr);
return retcode_;
}
std::error_code DriverManager::start() {
this->send_command(MT_ALSA_Msg_Start, 0, nullptr);
return retcode_;
}
std::error_code DriverManager::stop() {
this->send_command(MT_ALSA_Msg_Stop, 0, nullptr);
return retcode_;
}
std::error_code DriverManager::reset() {
this->send_command(MT_ALSA_Msg_Reset, 0, nullptr);
return retcode_;
}
std::error_code DriverManager::set_ptp_config(const TPTPConfig& config) {
BOOST_LOG_TRIVIAL(info) << "driver_manager:: setting PTP Domain "
<< (int)config.ui8Domain << " DSCP "
<< (int)config.ui8DSCP;
this->send_command(MT_ALSA_Msg_SetPTPConfig, sizeof(TPTPConfig),
reinterpret_cast<const uint8_t*>(&config));
return retcode_;
}
std::error_code DriverManager::get_ptp_config(TPTPConfig& config) {
this->send_command(MT_ALSA_Msg_GetPTPConfig);
if (!retcode_) {
memcpy(&config, recv_data_, sizeof(TPTPConfig));
BOOST_LOG_TRIVIAL(debug)
<< "driver_manager:: PTP Domain " << (int)config.ui8Domain << " DSCP "
<< (int)config.ui8DSCP;
}
return retcode_;
}
std::error_code DriverManager::get_ptp_status(TPTPStatus& status) {
this->send_command(MT_ALSA_Msg_GetPTPStatus);
if (!retcode_) {
memcpy(&status, recv_data_, sizeof(TPTPStatus));
BOOST_LOG_TRIVIAL(debug)
<< "driver_manager:: PTP Status "
<< ptp_status_str[status.nPTPLockStatus] << " GMID " << status.ui64GMID
<< " Jitter " << status.i32Jitter;
}
return retcode_;
}
std::error_code DriverManager::set_interface_name(const std::string& ifname) {
BOOST_LOG_TRIVIAL(info) << "driver_manager:: setting interface " << ifname;
this->send_command(MT_ALSA_Msg_SetInterfaceName, ifname.length() + 1,
reinterpret_cast<const uint8_t*>(ifname.c_str()));
return retcode_;
}
std::error_code DriverManager::add_rtp_stream(
const TRTP_stream_info& stream_info,
uint64_t& stream_handle) {
this->send_command(MT_ALSA_Msg_Add_RTPStream, sizeof(TRTP_stream_info),
reinterpret_cast<const uint8_t*>(&stream_info));
if (!retcode_) {
memcpy(&stream_handle, recv_data_, sizeof(stream_handle));
BOOST_LOG_TRIVIAL(info)
<< "driver_manager:: add RTP stream success handle " << stream_handle;
}
return retcode_;
}
std::error_code DriverManager::get_rtp_stream_status(
uint64_t stream_handle,
TRTP_stream_status& stream_status) {
this->send_command(MT_ALSA_Msg_GetRTPStreamStatus, sizeof(uint64_t),
reinterpret_cast<const uint8_t*>(&stream_handle));
if (!retcode_) {
memcpy(&stream_status, recv_data_, sizeof(stream_status));
}
return retcode_;
}
std::error_code DriverManager::remove_rtp_stream(uint64_t stream_handle) {
this->send_command(MT_ALSA_Msg_Remove_RTPStream, sizeof(uint64_t),
reinterpret_cast<const uint8_t*>(&stream_handle));
return retcode_;
}
std::error_code DriverManager::ping() {
this->send_command(MT_ALSA_Msg_Ping);
return retcode_;
}
std::error_code DriverManager::set_sample_rate(uint32_t sample_rate) {
this->send_command(MT_ALSA_Msg_SetSampleRate, sizeof(uint32_t),
reinterpret_cast<const uint8_t*>(&sample_rate));
return retcode_;
}
std::error_code DriverManager::set_tic_frame_size_at_1fs(uint64_t frame_size) {
this->send_command(MT_ALSA_Msg_SetTICFrameSizeAt1FS, sizeof(uint64_t),
reinterpret_cast<const uint8_t*>(&frame_size));
return retcode_;
}
std::error_code DriverManager::set_max_tic_frame_size(uint64_t frame_size) {
this->send_command(MT_ALSA_Msg_SetMaxTICFrameSize, sizeof(uint64_t),
reinterpret_cast<const uint8_t*>(&frame_size));
return retcode_;
}
std::error_code DriverManager::set_playout_delay(int32_t delay) {
this->send_command(MT_ALSA_Msg_SetPlayoutDelay, sizeof(uint32_t),
reinterpret_cast<const uint8_t*>(&delay));
return retcode_;
}
std::error_code DriverManager::get_sample_rate(uint32_t& sample_rate) {
this->send_command(MT_ALSA_Msg_GetSampleRate);
if (!retcode_) {
memcpy(&sample_rate, recv_data_, sizeof(uint32_t));
BOOST_LOG_TRIVIAL(info) << "driver_manager:: sample rate " << sample_rate;
}
return retcode_;
}
std::error_code DriverManager::get_number_of_inputs(int32_t& inputs) {
this->send_command(MT_ALSA_Msg_GetNumberOfInputs);
if (!retcode_) {
memcpy(&inputs, recv_data_, sizeof(uint32_t));
BOOST_LOG_TRIVIAL(info) << "driver_manager:: number of inputs " << inputs;
}
return retcode_;
}
std::error_code DriverManager::get_number_of_outputs(int32_t& outputs) {
this->send_command(MT_ALSA_Msg_GetNumberOfOutputs);
if (!retcode_) {
memcpy(&outputs, recv_data_, sizeof(uint32_t));
BOOST_LOG_TRIVIAL(info) << "driver_manager:: number of outputs " << outputs;
}
return retcode_;
}
void DriverManager::on_command_done(enum MT_ALSA_msg_id id,
size_t size,
const uint8_t* data) {
BOOST_LOG_TRIVIAL(info) << "driver_manager:: cmd " << alsa_msg_str[id]
<< " done data len " << size;
memcpy(recv_data_, data, size);
retcode_ = std::error_code{};
}
void DriverManager::on_command_error(enum MT_ALSA_msg_id id,
std::error_code error) {
BOOST_LOG_TRIVIAL(error) << "driver_manager:: cmd " << alsa_msg_str[id]
<< " failed with error " << error.message();
retcode_ = error;
}
void DriverManager::on_event(enum MT_ALSA_msg_id id,
size_t& resp_size,
uint8_t* resp,
size_t req_size,
const uint8_t* req) {
BOOST_LOG_TRIVIAL(debug) << "driver_manager:: event " << alsa_msg_str[id]
<< " data len " << req_size;
switch (id) {
case MT_ALSA_Msg_Hello:
resp_size = 0;
break;
case MT_ALSA_Msg_Bye:
resp_size = 0;
break;
case MT_ALSA_Msg_SetMasterOutputVolume:
if (req_size == sizeof(int32_t)) {
memcpy(&output_volume, req, req_size);
BOOST_LOG_TRIVIAL(info)
<< "driver_manager:: event SetMasterOutputVolume "
<< output_volume;
}
resp_size = 0;
break;
case MT_ALSA_Msg_SetMasterOutputSwitch:
if (req_size == sizeof(int32_t)) {
memcpy(&output_switch, req, req_size);
BOOST_LOG_TRIVIAL(info)
<< "driver_manager:: event SetMasterOutputSwitch "
<< output_switch;
}
resp_size = 0;
break;
case MT_ALSA_Msg_SetSampleRate:
if (req_size == sizeof(uint32_t)) {
memcpy(&sample_rate, req, req_size);
BOOST_LOG_TRIVIAL(info) << "driver_manager:: event SetSampleRate " << sample_rate;
}
resp_size = 0;
break;
case MT_ALSA_Msg_GetMasterOutputVolume:
resp_size = sizeof(int32_t);
memcpy(resp, &output_volume, resp_size);
BOOST_LOG_TRIVIAL(info)
<< "driver_manager:: event GetMasterOutputVolume " << output_volume;
break;
case MT_ALSA_Msg_GetMasterOutputSwitch:
resp_size = sizeof(int32_t);
memcpy(resp, &output_switch, resp_size);
BOOST_LOG_TRIVIAL(info)
<< "driver_manager:: event GetMasterOutputSwitch " << output_switch;
break;
default:
BOOST_LOG_TRIVIAL(error) << "driver_manager:: unknown event "
<< alsa_msg_str[id] << " data len " << req_size;
break;
}
}
void DriverManager::on_event_error(enum MT_ALSA_msg_id id,
std::error_code error) {
BOOST_LOG_TRIVIAL(error) << "driver_manager:: event " << alsa_msg_str[id]
<< " error " << error;
}

91
daemon/driver_manager.hpp Normal file
View File

@ -0,0 +1,91 @@
//
// driver_manager.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _DRIVER_MANAGER_HPP_
#define _DRIVER_MANAGER_HPP_
#include <boost/asio.hpp>
#include <mutex>
#include "driver_handler.hpp"
#include "RTP_stream_info.h"
#include "audio_streamer_clock_PTP_defs.h"
class DriverManager : public DriverHandler {
public:
static std::shared_ptr<DriverManager> create();
// driver interface
bool init(const Config& config) override;
bool terminate() override;
std::error_code ping(); // unused, return error
std::error_code set_ptp_config(const TPTPConfig& config);
std::error_code get_ptp_config(TPTPConfig& config);
std::error_code get_ptp_status(TPTPStatus& status);
std::error_code set_interface_name(const std::string& ifname);
std::error_code add_rtp_stream(const TRTP_stream_info& stream_info,
uint64_t& stream_handle);
std::error_code get_rtp_stream_status(uint64_t stream_handle,
TRTP_stream_status& stream_status);
std::error_code remove_rtp_stream(uint64_t stream_handle);
std::error_code get_sample_rate(uint32_t& sample_rate);
std::error_code set_sample_rate(uint32_t sample_rate);
std::error_code set_tic_frame_size_at_1fs(uint64_t frame_size);
std::error_code set_max_tic_frame_size(uint64_t frame_size);
std::error_code set_playout_delay(int32_t delay);
std::error_code get_number_of_inputs(int32_t& inputs);
std::error_code get_number_of_outputs(int32_t& outputs);
int32_t get_current_output_volume() { return output_volume; };
int32_t get_current_output_switch() { return output_switch; };
uint32_t get_current_sample_rate() { return sample_rate; };
protected:
// singleton, use create to build
DriverManager(){};
// these are used in init/terminate
std::error_code hello();
std::error_code start();
std::error_code stop();
std::error_code reset();
std::error_code bye();
void on_command_done(enum MT_ALSA_msg_id id,
size_t size = 0,
const uint8_t* data = nullptr) override;
void on_command_error(enum MT_ALSA_msg_id id, std::error_code error) override;
void on_event(enum MT_ALSA_msg_id id,
size_t& res_size,
uint8_t* res,
size_t req_size = 0,
const uint8_t* req = nullptr) override;
void on_event_error(enum MT_ALSA_msg_id id, std::error_code error) override;
std::error_code retcode_;
uint8_t recv_data_[NLMSG_SPACE(max_payload)]{0};
int32_t output_volume{-20};
int32_t output_switch{0};
uint32_t sample_rate{0};
};
#endif

125
daemon/error_code.cpp Normal file
View File

@ -0,0 +1,125 @@
//
// error_code.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include "error_code.hpp"
struct DriverErrCategory : std::error_category {
const char* name() const noexcept override;
std::string message(int ev) const override;
};
const char* DriverErrCategory::name() const noexcept {
return "driver";
}
std::string DriverErrCategory::message(int ev) const {
switch (static_cast<DriverErrc>(ev)) {
case DriverErrc::unknown:
return "unhandled error code";
case DriverErrc::invalid_data_size:
return "invalid data size";
case DriverErrc::invalid_value:
return "invalid value specified";
case DriverErrc::command_failed:
return "command failed";
case DriverErrc::command_not_found:
return "command not found";
case DriverErrc::unknown_command:
return "unknown command";
case DriverErrc::invalid_daemon_response:
return "invalid daemon response";
case DriverErrc::invalid_daemon_response_size:
return "invalid daemon response";
default:
return "(unrecognized driver error)";
}
}
std::error_code get_driver_error(int code) {
switch (code) {
case -302:
return DriverErrc::invalid_daemon_response_size;
case -303:
return DriverErrc::invalid_daemon_response;
case -314:
return DriverErrc::unknown_command;
case -315:
return DriverErrc::invalid_data_size;
case -401:
return DriverErrc::command_failed;
case -404:
return DriverErrc::command_not_found;
case -805:
return DriverErrc::invalid_value;
default:
return DriverErrc::unknown;
}
}
const DriverErrCategory theDriverErrCategory{};
std::error_code make_error_code(DriverErrc e) {
return {static_cast<int>(e), theDriverErrCategory};
}
struct DaemonErrCategory : std::error_category {
const char* name() const noexcept override;
std::string message(int ev) const override;
};
const char* DaemonErrCategory::name() const noexcept {
return "daemon";
}
std::string DaemonErrCategory::message(int ev) const {
switch (static_cast<DaemonErrc>(ev)) {
case DaemonErrc::invalid_stream_id:
return "invalid stream id";
case DaemonErrc::stream_id_in_use:
return "stream id is in use";
case DaemonErrc::stream_id_not_in_use:
return "stream not in use";
case DaemonErrc::invalid_url:
return "invalid URL";
case DaemonErrc::cannot_retrieve_sdp:
return "cannot retrieve SDP";
case DaemonErrc::cannot_parse_sdp:
return "cannot parse SDP";
case DaemonErrc::send_invalid_size:
return "send data size too big";
case DaemonErrc::send_u2k_failed:
return "failed to send command to driver";
case DaemonErrc::send_k2u_failed:
return "failed to send event response to driver";
case DaemonErrc::receive_u2k_failed:
return "failed to receive response from driver";
case DaemonErrc::receive_k2u_failed:
return "failed to receive event from driver";
case DaemonErrc::invalid_driver_response:
return "unexpected driver command response code";
default:
return "(unrecognized daemon error)";
}
}
const DaemonErrCategory theDaemonErrCategory{};
std::error_code make_error_code(DaemonErrc e) {
return {static_cast<int>(e), theDaemonErrCategory};
}

69
daemon/error_code.hpp Normal file
View File

@ -0,0 +1,69 @@
//
// error_code.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _ERROR_CODE_HPP_
#define _ERROR_CODE_HPP_
#include <system_error>
// Driver errors
enum class DriverErrc {
unknown = 10, // unhandled code from driver
invalid_data_size = 11, // driver -315 invalid data size
invalid_value = 12, // driver -815 invalid value specified
command_failed = 13, // driver -401 command failed
command_not_found = 14, // driver -404 command not found
unknown_command = 15, // driver -314 unknown command
invalid_daemon_response = 16, // driver -303 invalid daemon response
invalid_daemon_response_size = 17 // driver -302 invalid daemon response
};
namespace std {
template <>
struct is_error_code_enum<DriverErrc> : true_type {};
} // namespace std
std::error_code make_error_code(DriverErrc);
std::error_code get_driver_error(int code);
// Daemon errors
enum class DaemonErrc {
invalid_stream_id = 40, // daemon invalid stream id
stream_id_in_use = 41, // daemon stream id is in use
stream_id_not_in_use = 42, // daemon stream not in use
invalid_url = 43, // daemon invalid URL
cannot_retrieve_sdp = 44, // daemon cannot retrieve SDP
cannot_parse_sdp = 45, // daemon cannot parse SDP
send_invalid_size = 50, // daemon data size too big for buffer
send_u2k_failed = 51, // daemon failed to send command to driver
send_k2u_failed = 52, // daemon failed to send event response to driver
receive_u2k_failed = 53, // daemon failed to receive response from driver
receive_k2u_failed = 54, // daemon failed to receive event from driver
invalid_driver_response = 55 // unexpected driver command response code
};
namespace std {
template <>
struct is_error_code_enum<DaemonErrc> : true_type {};
} // namespace std
std::error_code make_error_code(DaemonErrc);
#endif

325
daemon/http_server.cpp Normal file
View File

@ -0,0 +1,325 @@
//
// http_server.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <boost/foreach.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <iostream>
#include <string>
#include "http_server.hpp"
#include "log.hpp"
#include "json.hpp"
using namespace httplib;
static inline void set_headers(Response& res, const std::string content_type = "") {
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Headers", "x-user-id");
if (!content_type.empty()) {
res.set_header("Content-Type", content_type);
}
}
static inline int get_http_error_status(const std::error_code& code) {
if (std::string(code.category().name()) == "daemon") {
if (code.value() < static_cast<int>(DaemonErrc::send_invalid_size)) {
return 400;
}
}
if (std::string(code.category().name()) == "driver") {
if (code.value() == static_cast<int>(DriverErrc::command_failed)) {
return 400;
}
}
return 500;
}
static inline std::string get_http_error_message(
const std::error_code& code) {
std::stringstream ss;;
ss << "(" << code.category().name() << ") " << code.message();
return ss.str();
}
static inline void set_error(
const std::error_code& code,
const std::string& message,
Response& res) {
res.status = get_http_error_status(code);
set_headers(res, "text/plain");
res.body = message + " : " + get_http_error_message(code);
}
static inline void set_error(
int status,
const std::string& message,
Response& res) {
res.status = status;
set_headers(res, "text/plain");
res.body = message;
}
bool HttpServer::start() {
/* setup http operations */
if (!svr_.is_valid()) {
return false;
}
svr_.set_base_dir(config_->get_http_base_dir().c_str());
svr_.Get("(/|/Config|/PTP|/Sources|/Sinks)", [&](const Request& req, Response& res) {
std::ifstream file(config_->get_http_base_dir() + "/index.html");
std::stringstream buffer;
buffer << file.rdbuf();
res.set_content(buffer.str(), "text/html");
});
/* allows cross-origin */
svr_.Options("/api/(.*?)", [&](const Request & /*req*/, Response &res) {
set_headers(res);
});
/* get config */
svr_.Get("/api/config", [&](const Request& req, Response& res) {
set_headers(res, "application/json");
res.body = config_to_json(*config_);
});
/* set config */
svr_.Post("/api/config", [this](const Request& req, Response& res) {
try {
Config config = json_to_config(req.body, *config_);
if (!config_->save(config)) {
set_error(500, "failed to save config", res);
return;
}
set_headers(res);
} catch (const std::runtime_error& e) {
set_error(400, e.what(), res);
}
});
/* get ptp status */
svr_.Get("/api/ptp/status", [this](const Request& req, Response& res) {
PTPStatus status;
session_manager_->get_ptp_status(status);
set_headers(res, "application/json");
res.body = ptp_status_to_json(status);
});
/* get ptp config */
svr_.Get("/api/ptp/config", [this](const Request& req, Response& res) {
PTPConfig ptpConfig;
session_manager_->get_ptp_config(ptpConfig);
set_headers(res, "application/json");
res.body = ptp_config_to_json(ptpConfig);
});
/* set ptp config */
svr_.Post("/api/ptp/config", [this](const Request& req, Response& res) {
try {
PTPConfig ptpConfig = json_to_ptp_config(req.body);
Config config(*config_);
config.set_ptp_domain(ptpConfig.domain);
config.set_ptp_dscp(ptpConfig.dscp);
if (!config_->save(config, false)) {
set_error(500, "failed to save config", res);
return;
}
auto ret = session_manager_->set_ptp_config(ptpConfig);
if (ret) {
set_error(ret, "failed to set ptp config", res);
return;
}
set_headers(res);
} catch (const std::runtime_error& e) {
set_error(400, e.what(), res);
}
});
/* get all sources */
svr_.Get("/api/sources", [this](const Request& req, Response& res) {
auto const sources = session_manager_->get_sources();
set_headers(res, "application/json");
res.body = sources_to_json(sources);
});
/* get all sinks */
svr_.Get("/api/sinks", [this](const Request& req, Response& res) {
auto const sinks = session_manager_->get_sinks();
set_headers(res, "application/json");
res.body = sinks_to_json(sinks);
});
/* get all sources and sinks */
svr_.Get("/api/streams", [this](const Request& req, Response& res) {
auto const sources = session_manager_->get_sources();
auto const sinks = session_manager_->get_sinks();
set_headers(res, "application/json");
res.body = streams_to_json(sources, sinks);
});
/* get a source SDP */
svr_.Get("/api/source/sdp/([0-9]+)", [this](const Request& req, Response& res) {
uint32_t id;
try {
id = std::stoi(req.matches[1]);
} catch (...) {
set_error(400, "failed to convert id", res);
return;
}
auto ret = session_manager_->get_source_sdp(id, res.body);
if (ret) {
set_error(ret, "get source " + std::to_string(id) + " failed", res);
} else {
set_headers(res, "application/sdp");
}
});
/* get stream status */
svr_.Get("/api/sink/status/([0-9]+)", [this](const Request& req,
Response& res) {
uint32_t id;
try {
id = std::stoi(req.matches[1]);
} catch (...) {
set_error(400, "failed to convert id", res);
return;
}
SinkStreamStatus status;
auto ret = session_manager_->get_sink_status(id, status);
if (ret) {
set_error(ret, "failed to get sink " + std::to_string(id) +
" status", res);
} else {
set_headers(res, "application/json");
res.body = sink_status_to_json(status);
}
});
/* add a source */
svr_.Put("/api/source/([0-9]+)", [this](const Request& req, Response& res) {
try {
StreamSource source = json_to_source(req.matches[1], req.body);
auto ret = session_manager_->add_source(source);
if (ret) {
set_error(ret, "failed to add source " + std::to_string(source.id), res);
} else {
set_headers(res);
}
} catch (const std::runtime_error& e) {
set_error(400, e.what(), res);
}
});
/* remove a source */
svr_.Delete("/api/source/([0-9]+)", [this](const Request& req, Response& res) {
uint32_t id;
try {
id = std::stoi(req.matches[1]);
} catch (...) {
set_error(400, "failed to convert id", res);
return;
}
auto ret = session_manager_->remove_source(id);
if (ret) {
set_error(ret, "failed to remove source " + std::to_string(id), res);
} else {
set_headers(res);
}
});
/* add a sink */
svr_.Put("/api/sink/([0-9]+)", [this](const Request& req, Response& res) {
try {
StreamSink sink = json_to_sink(req.matches[1], req.body);
auto ret = session_manager_->add_sink(sink);
if (ret) {
set_error(ret, "failed to add sink " + std::to_string(sink.id), res);
} else {
set_headers(res);
}
} catch (const std::runtime_error& e) {
set_error(400, e.what(), res);
}
});
/* remove a sink */
svr_.Delete("/api/sink/([0-9]+)", [this](const Request& req, Response& res) {
uint32_t id;
try {
id = std::stoi(req.matches[1]);
} catch (...) {
set_error(400, "failed to convert id", res);
return;
}
auto ret = session_manager_->remove_sink(id);
if (ret) {
set_error(ret, "failed to remove sink " + std::to_string(id), res);
} else {
set_headers(res);
}
});
svr_.set_logger([](const Request& req, const Response& res) {
if (res.status == 200) {
BOOST_LOG_TRIVIAL(info) << "http_server:: " << req.method << " "
<< req.path << " response " << res.status;
} else {
BOOST_LOG_TRIVIAL(error)
<< "http_server:: " << req.method << " " << req.path << " response "
<< res.status << " " << res.body;
}
});
/* start http server on a separate thread */
res_ = std::async(std::launch::async, [&]() {
try {
svr_.listen(config_->get_ip_addr_str().c_str(), config_->get_http_port());
} catch (...) {
BOOST_LOG_TRIVIAL(fatal)
<< "http_server:: "
<< "failed to listen to " << config_->get_ip_addr_str() << ":"
<< config_->get_http_port();
return false;
}
return true;
});
/* wait for HTTP server to show up */
httplib::Client cli(config_->get_ip_addr_str().c_str(), config_->get_http_port());
int retry = 3;
while (retry--) {
auto res = cli.Get("/api/config");
if (res && res->status == 200) {
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return retry;
}
bool HttpServer::stop() {
BOOST_LOG_TRIVIAL(info) << "http_server: stopping ... ";
svr_.stop();
return res_.get();
}

47
daemon/http_server.hpp Normal file
View File

@ -0,0 +1,47 @@
//
// http_server.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _HTTP_SERVER_HPP_
#define _HTTP_SERVER_HPP_
#include <httplib.h>
#include "config.hpp"
#include "session_manager.hpp"
class HttpServer {
public:
HttpServer() = delete;
HttpServer(std::shared_ptr<SessionManager> session_manager,
std::shared_ptr<Config> config)
: session_manager_(session_manager),
config_(config) {};
bool start();
bool stop();
int get_http_status(const std::error_code& code) const;
std::string get_http_error(const std::error_code& code) const;
private:
std::shared_ptr<SessionManager> session_manager_;
std::shared_ptr<Config> config_;
httplib::Server svr_;
std::future<bool> res_;
};
#endif

109
daemon/igmp.hpp Normal file
View File

@ -0,0 +1,109 @@
//
// igmp.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _IGMP_HPP_
#define _IGMP_HPP_
#include <boost/asio.hpp>
#include <mutex>
#include <map>
#include "log.hpp"
using namespace boost::asio;
using namespace boost::asio::ip;
using namespace boost::system;
class IGMP {
public:
IGMP() {
socket_.open(listen_endpoint_.protocol());
socket_.set_option(udp::socket::reuse_address(true));
socket_.bind(listen_endpoint_);
};
bool join(const std::string& interface_ip, const std::string& mcast_ip) {
std::lock_guard<std::mutex> lock(mutex);
auto it = mcast_ref.find(mcast_ip);
if (it != mcast_ref.end() && (*it).second > 0) {
mcast_ref[mcast_ip]++;
return true;
}
error_code ec;
ip::multicast::join_group option(
ip::address::from_string(mcast_ip).to_v4(),
ip::address::from_string(interface_ip).to_v4());
socket_.set_option(option, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "igmp:: failed to joined multicast group "
<< mcast_ip << " " << ec.message();
return false;
}
ip::multicast::enable_loopback el_option(true);
socket_.set_option(el_option, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "igmp:: enable loopback option "
<< ec.message();
}
BOOST_LOG_TRIVIAL(info) << "igmp:: joined multicast group "
<< mcast_ip << " on " << interface_ip;
mcast_ref[mcast_ip] = 1;
return true;
}
bool leave(const std::string& interface_ip, const std::string& mcast_ip) {
std::lock_guard<std::mutex> lock(mutex);
auto it = mcast_ref.find(mcast_ip);
if (it == mcast_ref.end() || (*it).second == 0) {
return false;
}
if (--mcast_ref[mcast_ip] > 0) {
return true;
}
error_code ec;
ip::multicast::leave_group option(
ip::address::from_string(mcast_ip).to_v4(),
ip::address::from_string(interface_ip).to_v4());
socket_.set_option(option, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "igmp:: failed to leave multicast group "
<< mcast_ip << " " << ec.message();
return false;
}
BOOST_LOG_TRIVIAL(info) << "igmp:: left multicast group "
<< mcast_ip << " on " << interface_ip;
return true;
}
private:
io_service io_service_;
ip::udp::socket socket_{io_service_};
udp::endpoint listen_endpoint_{udp::endpoint(address_v4::any(), 0)};
std::map<std::string, int> mcast_ref;
std::mutex mutex;
};
#endif

90
daemon/interface.cpp Normal file
View File

@ -0,0 +1,90 @@
//
// interface.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
// MIT License
//
#include <utility>
#include <boost/asio.hpp>
#include "log.hpp"
using namespace boost::asio;
using namespace boost::asio::ip;
std::pair<uint32_t, std::string> get_interface_ip(
const std::string& interface_name) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
BOOST_LOG_TRIVIAL(error)
<< "Cannot retrieve IP address for interface " << interface_name;
return std::make_pair(0, "");
}
struct ifreq ifr;
ifr.ifr_addr.sa_family = AF_INET;
strncpy(ifr.ifr_name, interface_name.c_str(), IFNAMSIZ - 1);
if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) {
close(fd);
BOOST_LOG_TRIVIAL(error)
<< "Cannot retrieve IP address for interface " << interface_name;
return std::make_pair(0, "");
}
close(fd);
struct sockaddr_in* sockaddr =
reinterpret_cast<struct sockaddr_in*>(&ifr.ifr_addr);
uint32_t addr = ntohl(sockaddr->sin_addr.s_addr);
std::string str_addr(ip::address_v4(addr).to_string());
/*BOOST_LOG_TRIVIAL(debug) << "interface " << interface_name << " IP address "
<< str_addr;*/
return std::make_pair(addr, str_addr);
}
std::pair<std::array<uint8_t, 6>, std::string> get_interface_mac(
const std::string& interface_name) {
std::array<uint8_t, 6> mac{0x01, 0x00, 0x5e, 0x01, 0x00, 0x01};
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
BOOST_LOG_TRIVIAL(error)
<< "Cannot retrieve MAC address for interface " << interface_name;
return std::make_pair(mac, "");
}
struct ifreq ifr;
ifr.ifr_addr.sa_family = AF_INET;
strncpy(ifr.ifr_name, interface_name.c_str(), IFNAMSIZ - 1);
if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) {
close(fd);
BOOST_LOG_TRIVIAL(error)
<< "Cannot retrieve MAC address for interface " << interface_name;
return std::make_pair(mac, "");
}
close(fd);
if (*ifr.ifr_hwaddr.sa_data != 0) {
uint8_t* sa = reinterpret_cast<uint8_t*>(ifr.ifr_hwaddr.sa_data);
std::copy(sa, sa + 8, std::begin(mac));
}
char str_mac[18];
sprintf(str_mac, "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5]);
/*BOOST_LOG_TRIVIAL(debug) << "interface " << interface_name << " MAC address "
<< str_mac;*/
return std::make_pair(mac, str_mac);
}

28
daemon/interface.hpp Normal file
View File

@ -0,0 +1,28 @@
//
// interface.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _INTERFACE_HPP_
#define _INTERFACE_HPP_
std::pair<uint32_t, std::string> get_interface_ip(
const std::string& interface_name);
std::pair<std::array<uint8_t, 6>, std::string> get_interface_mac(
const std::string& interface_name);
#endif

506
daemon/json.cpp Normal file
View File

@ -0,0 +1,506 @@
//
// json.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <boost/foreach.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <regex>
#include <iostream>
#include <string>
#include "json.hpp"
#include "log.hpp"
static inline std::string remove_undesired_chars(const std::string& s) {
std::regex html_regex("[^ A-Za-z0-9:~._/=%\()\\r\\n\\t\?#-]?");
std::string r = std::regex_replace(s, html_regex, "");
return r;
}
static std::string escape_json(const std::string& js) {
std::string s(remove_undesired_chars(js));
std::ostringstream ss;
for (auto c = s.cbegin(); c != s.cend(); c++) {
switch (*c) {
case '"':
ss << "\\\"";
break;
case '\\':
ss << "\\\\";
break;
case '\b':
ss << "\\b";
break;
case '\f':
ss << "\\f";
break;
case '\n':
ss << "\\n";
break;
case '\r':
ss << "\\r";
break;
case '\t':
ss << "\\t";
break;
default:
if ('\x00' <= *c && *c <= '\x1f') {
ss << "\\u" << std::hex << std::setw(4) << std::setfill('0')
<< (int)*c;
} else {
ss << *c;
}
}
}
return ss.str();
}
std::string config_to_json(const Config& config) {
std::stringstream ss;
ss << "{"
<< "\n \"http_port\": " << config.get_http_port()
<< ",\n \"http_base_dir\": \"" << config.get_http_base_dir() << "\""
<< ",\n \"log_severity\": " << config.get_log_severity()
<< ",\n \"playout_delay\": " << config.get_playout_delay()
<< ",\n \"tic_frame_size_at_1fs\": " << config.get_tic_frame_size_at_1fs()
<< ",\n \"max_tic_frame_size\": " << config.get_max_tic_frame_size()
<< ",\n \"sample_rate\": " << config.get_sample_rate()
<< ",\n \"rtp_mcast_base\": \"" << escape_json(config.get_rtp_mcast_base()) << "\""
<< ",\n \"rtp_port\": " << config.get_rtp_port()
<< ",\n \"ptp_domain\": " << unsigned(config.get_ptp_domain())
<< ",\n \"ptp_dscp\": " << unsigned(config.get_ptp_dscp())
<< ",\n \"sap_interval\": " << config.get_sap_interval()
<< ",\n \"syslog_proto\": \"" << escape_json(config.get_syslog_proto()) << "\""
<< ",\n \"syslog_server\": \"" << escape_json(config.get_syslog_server()) << "\""
<< ",\n \"status_file\": \"" << escape_json(config.get_status_file()) << "\""
<< ",\n \"interface_name\": \"" << escape_json(config.get_interface_name()) << "\""
<< ",\n \"mac_addr\": \"" << escape_json(config.get_mac_addr_str()) << "\""
<< ",\n \"ip_addr\": \"" << escape_json(config.get_ip_addr_str()) << "\""
<< "\n}\n";
return ss.str();
}
std::string source_to_json(const StreamSource& source) {
std::stringstream ss;
ss << "\n {"
<< "\n \"id\": " << unsigned(source.id)
<< ",\n \"enabled\": " << std::boolalpha << source.enabled
<< ",\n \"name\": \"" << escape_json(source.name) << "\""
<< ",\n \"io\": \"" << escape_json(source.io) << "\""
<< ",\n \"max_samples_per_packet\": " << source.max_samples_per_packet
<< ",\n \"codec\": \"" << escape_json(source.codec) << "\""
<< ",\n \"ttl\": " << unsigned(source.ttl)
<< ",\n \"payload_type\": " << unsigned(source.payload_type)
<< ",\n \"dscp\": " << +unsigned(source.dscp)
<< ",\n \"refclk_ptp_traceable\": " << std::boolalpha
<< source.refclk_ptp_traceable << ",\n \"map\": [ ";
int i = 0;
for (int value : source.map) {
if (i++ > 0)
ss << ", ";
ss << value;
}
ss << " ]\n }";
return ss.str();
}
std::string sink_to_json(const StreamSink& sink) {
std::stringstream ss;
ss << "\n {"
<< "\n \"id\": " << unsigned(sink.id) << ",\n \"name\": \""
<< escape_json(sink.name) << "\""
<< ",\n \"io\": \"" << escape_json(sink.io) << "\""
<< ",\n \"use_sdp\": " << std::boolalpha << sink.use_sdp
<< ",\n \"source\": \"" << escape_json(sink.source) << "\""
<< ",\n \"sdp\": \"" << escape_json(sink.sdp) << "\""
<< ",\n \"delay\": " << sink.delay
<< ",\n \"ignore_refclk_gmid\": " << std::boolalpha
<< sink.ignore_refclk_gmid << ",\n \"map\": [ ";
int i = 0;
for (int value : sink.map) {
if (i++ > 0)
ss << ", ";
ss << value;
}
ss << " ]\n }";
return ss.str();
}
std::string sink_status_to_json(const SinkStreamStatus& status) {
std::stringstream ss;
ss << "{";
ss << " \n \"sink_flags\":\n {" << std::boolalpha
<< " \n \"rtp_seq_id_error\": " << status.is_rtp_seq_id_error
<< ", \n \"rtp_ssrc_error\": " << status.is_rtp_ssrc_error
<< ", \n \"rtp_payload_type_error\": "
<< status.is_rtp_payload_type_error
<< ", \n \"rtp_sac_error\": " << status.is_rtp_sac_error
<< ", \n \"receiving_rtp_packet\": " << status.is_receiving_rtp_packet
<< ", \n \"some_muted\": " << status.is_some_muted
<< ", \n \"muted\": " << status.is_muted << "\n },"
<< "\n \"sink_min_time\": " << status.min_time << "\n}\n";
return ss.str();
}
std::string ptp_config_to_json(const PTPConfig& ptp_config) {
std::stringstream ss;
ss << "{"
<< " \"domain\": " << unsigned(ptp_config.domain)
<< ", \"dscp\": " << unsigned(ptp_config.dscp) << " }\n";
return ss.str();
}
std::string ptp_status_to_json(const PTPStatus& status) {
std::stringstream ss;
ss << "{"
<< " \"status\": \"" << escape_json(status.status) << "\""
<< ", \"gmid\": \"" << escape_json(status.gmid) << "\""
<< ", \"jitter\": " << status.jitter << " }\n";
return ss.str();
}
std::string sources_to_json(const std::list<StreamSource>& sources) {
int count = 0;
std::stringstream ss;
ss << "{\n \"sources\": [";
for (auto const& source: sources) {
if (count++) {
ss << ", ";
}
ss << source_to_json(source);
}
ss << " ]\n}\n";
return ss.str();
}
std::string sinks_to_json(const std::list<StreamSink>& sinks) {
int count = 0;
std::stringstream ss;
ss << "{\n \"sinks\": [";
for (auto const& sink: sinks) {
if (count++) {
ss << ", ";
}
ss << sink_to_json(sink);
}
ss << " ]\n}\n";
return ss.str();
}
std::string streams_to_json(const std::list<StreamSource>& sources,
const std::list<StreamSink>& sinks) {
int count = 0;
std::stringstream ss;
ss << "{\n \"sources\": [";
for (auto const& source: sources) {
if (count++) {
ss << ", ";
}
ss << source_to_json(source);
}
count = 0;
ss << " ],\n \"sinks\": [";
for (auto const& sink: sinks) {
if (count++) {
ss << ", ";
}
ss << sink_to_json(sink);
}
ss << " ]\n}\n";
return ss.str();
}
Config json_to_config_(std::istream& js, Config& config) {
try {
boost::property_tree::ptree pt;
boost::property_tree::read_json(js, pt);
for (auto const& [key, val] : pt) {
if (key == "http_port") {
config.set_http_port(val.get_value<int>());
} else if (key == "http_base_dir") {
config.set_http_base_dir(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "log_severity") {
config.set_log_severity(val.get_value<int>());
} else if (key == "interface_name") {
config.set_interface_name(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "playout_delay") {
config.set_playout_delay(val.get_value<uint32_t>());
} else if (key == "tic_frame_size_at_1fs") {
config.set_tic_frame_size_at_1fs(val.get_value<uint32_t>());
} else if (key == "max_tic_frame_size") {
config.set_max_tic_frame_size(val.get_value<uint32_t>());
} else if (key == "sample_rate") {
config.set_sample_rate(val.get_value<uint32_t>());
} else if (key == "rtp_mcast_base") {
config.set_rtp_mcast_base(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "rtp_port") {
config.set_rtp_port(val.get_value<uint16_t>());
} else if (key == "ptp_domain") {
config.set_ptp_domain(val.get_value<uint8_t>());
} else if (key == "ptp_dscp") {
config.set_ptp_dscp(val.get_value<uint8_t>());
} else if (key == "sap_interval") {
config.set_sap_interval(val.get_value<uint16_t>());
} else if (key == "status_file") {
config.set_status_file(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "syslog_proto") {
config.set_syslog_proto(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "syslog_server") {
config.set_syslog_server(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "mac_addr" || key == "ip_addr") {
/* ignored */
} else {
std::cerr << "Warning: unkown configuration option " << key
<< std::endl;
}
}
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
} catch (...) {
throw std::runtime_error("failed to convert a number");
}
return config;
}
Config json_to_config(std::istream& js, const Config& curConfig) {
Config config(curConfig);
return json_to_config_(js, config);
}
Config json_to_config(std::istream& js) {
Config config;
return json_to_config_(js, config);
}
Config json_to_config(const std::string & json, const Config& curConfig) {
std::stringstream ss(json);
return json_to_config(ss, curConfig);
}
Config json_to_config(const std::string & json) {
std::stringstream ss(json);
return json_to_config(ss);
}
StreamSource json_to_source(const std::string& id, const std::string& json) {
/* JSON request
"enabled": true,
"name": "ALSA (on ubuntu)_1",
"io": "Audio Device",
"map": [ 0, 1, 2, 3, 4, 5, 6, 7 ],
"max_samples_per_packet": 48,
"codec": "L24",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": false
*/
StreamSource source;
try {
boost::property_tree::ptree pt;
std::stringstream ss(json);
boost::property_tree::read_json(ss, pt);
source.id = std::stoi(id);
source.enabled = pt.get<bool>("enabled");
source.name = remove_undesired_chars(pt.get<std::string>("name"));
source.io = remove_undesired_chars(pt.get<std::string>("io"));
/* source map determite the association with
ALSA output channels used to playing */
BOOST_FOREACH (boost::property_tree::ptree::value_type& v,
pt.get_child("map")) {
source.map.push_back(std::stoi(v.second.data()));
}
source.max_samples_per_packet = pt.get<uint32_t>("max_samples_per_packet");
source.codec = remove_undesired_chars(pt.get<std::string>("codec"));
source.ttl = pt.get<uint8_t>("ttl");
source.payload_type = pt.get<uint8_t>("payload_type");
source.dscp = pt.get<uint8_t>("dscp");
source.refclk_ptp_traceable = pt.get<bool>("refclk_ptp_traceable");
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
} catch (...) {
throw std::runtime_error("failed to convert a number");
}
return source;
}
StreamSink json_to_sink(const std::string& id, const std::string& json) {
/* JSON request
"name": "ALSA (on ubuntu)_1",
"io": "Audio Device",
"use_sdp": true,
"source": "...",
"sdp": "...",
"delay": 384,
"ignore_refclk_gmid": false,
"map": [ 0, 1, 2, 3, 4, 5, 6, 7 ]
*/
StreamSink sink;
try {
boost::property_tree::ptree pt;
std::stringstream ss(json);
boost::property_tree::read_json(ss, pt);
sink.id = std::stoi(id);
sink.name = remove_undesired_chars(pt.get<std::string>("name"));
sink.io = remove_undesired_chars(pt.get<std::string>("io"));
sink.source = remove_undesired_chars(pt.get<std::string>("source"));
sink.use_sdp = pt.get<bool>("use_sdp");
sink.sdp = remove_undesired_chars(pt.get<std::string>("sdp"));
sink.delay = pt.get<uint32_t>("delay");
sink.ignore_refclk_gmid = pt.get<bool>("ignore_refclk_gmid");
/* source map determite the association with
ALSA input channels used to recording */
BOOST_FOREACH (boost::property_tree::ptree::value_type& v,
pt.get_child("map")) {
sink.map.push_back(std::stoi(v.second.data()));
}
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
} catch (...) {
throw std::runtime_error("failed to convert a number");
}
return sink;
}
PTPConfig json_to_ptp_config(const std::string& json) {
PTPConfig ptpConfig;
try {
boost::property_tree::ptree pt;
std::stringstream ss(json);
boost::property_tree::read_json(ss, pt);
ptpConfig.domain = pt.get<int>("domain");
ptpConfig.dscp = pt.get<int>("dscp");
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
}
return ptpConfig;
}
void json_to_sources(const std::string & json,
std::list<StreamSource>& sources) {
std::stringstream ss(json);
return json_to_sources(ss, sources);
}
static void parse_json_sources(boost::property_tree::ptree& pt,
std::list<StreamSource>& sources) {
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
StreamSource source;
source.id = v.second.get<uint8_t>("id");
source.enabled = v.second.get<bool>("enabled");
source.name = v.second.get<std::string>("name");
source.io = v.second.get<std::string>("io");
source.max_samples_per_packet = v.second.get<uint32_t>("max_samples_per_packet");
source.codec = v.second.get<std::string>("codec");
source.ttl = v.second.get<uint8_t>("ttl");
source.payload_type = v.second.get<uint8_t>("payload_type");
source.dscp = v.second.get<uint8_t>("dscp");
source.refclk_ptp_traceable = v.second.get<bool>("refclk_ptp_traceable");
/* source map determite the association with
ALSA output channels used to playing */
BOOST_FOREACH (const boost::property_tree::ptree::value_type& vm,
v.second.get_child("map")) {
source.map.push_back(std::stoi(vm.second.data()));
}
sources.push_back(source);
}
}
void json_to_sources(std::istream& js,
std::list<StreamSource>& sources) {
try {
boost::property_tree::ptree pt;
boost::property_tree::read_json(js, pt);
parse_json_sources(pt, sources);
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
}
}
void json_to_sinks(const std::string & json,
std::list<StreamSink>& sinks) {
std::stringstream ss(json);
return json_to_sinks(ss, sinks);
}
static void parse_json_sinks(boost::property_tree::ptree& pt,
std::list<StreamSink>& sinks) {
BOOST_FOREACH (auto const& v, pt.get_child("sinks")) {
StreamSink sink;
sink.id = v.second.get<uint8_t>("id");
sink.name = v.second.get<std::string>("name");
sink.io = v.second.get<std::string>("io");
sink.source = v.second.get<std::string>("source");
sink.use_sdp = v.second.get<bool>("use_sdp");
sink.sdp = v.second.get<std::string>("sdp");
sink.delay = v.second.get<uint32_t>("delay");
sink.ignore_refclk_gmid = v.second.get<bool>("ignore_refclk_gmid");
/* source map determite the association with
ALSA input channels used to recording */
BOOST_FOREACH (const boost::property_tree::ptree::value_type& vm,
v.second.get_child("map")) {
sink.map.push_back(std::stoi(vm.second.data()));
}
sinks.push_back(sink);
}
}
void json_to_sinks(std::istream& js,
std::list<StreamSink>& sinks) {
try {
boost::property_tree::ptree pt;
boost::property_tree::read_json(js, pt);
parse_json_sinks(pt, sinks);
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
}
}
void json_to_streams(const std::string & json,
std::list<StreamSource>& sources,
std::list<StreamSink>& sinks) {
std::stringstream ss(json);
json_to_streams(ss, sources, sinks);
}
void json_to_streams(std::istream& js,
std::list<StreamSource>& sources,
std::list<StreamSink>& sinks) {
try {
boost::property_tree::ptree pt;
boost::property_tree::read_json(js, pt);
parse_json_sources(pt, sources);
parse_json_sinks(pt, sinks);
} catch (boost::property_tree::json_parser::json_parser_error& je) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
}
}

60
daemon/json.hpp Normal file
View File

@ -0,0 +1,60 @@
//
// json.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _JSON_HPP_
#define _JSON_HPP_
#include <list>
#include "session_manager.hpp"
/* JSON serializers */
std::string config_to_json(const Config& config);
std::string source_to_json(const StreamSource& source);
std::string sink_to_json(const StreamSink& sink);
std::string sink_status_to_json(const SinkStreamStatus& status);
std::string ptp_config_to_json(const PTPConfig& config);
std::string ptp_status_to_json(const PTPStatus& status);
std::string sources_to_json(const std::list<StreamSource>& sources);
std::string sinks_to_json(const std::list<StreamSink>& sinks);
std::string streams_to_json(const std::list<StreamSource>& sources,
const std::list<StreamSink>& sinks);
/* JSON deserializers */
Config json_to_config(std::istream& jstream, const Config& curCconfig);
Config json_to_config(std::istream& jstream);
Config json_to_config(const std::string& json, const Config& curConfig);
Config json_to_config(const std::string& json);
StreamSource json_to_source(const std::string& id, const std::string& json);
StreamSink json_to_sink(const std::string& id, const std::string& json);
PTPConfig json_to_ptp_config(const std::string& json);
void json_to_sources(std::istream& jstream,
std::list<StreamSource>& sources);
void json_to_sources(const std::string& json,
std::list<StreamSource>& sources);
void json_to_sinks(std::istream& jstream,
std::list<StreamSink>& sinks);
void json_to_sinks(const std::string& json,
std::list<StreamSink>& sinks);
void json_to_streams(std::istream& jstream,
std::list<StreamSource>& sources,
std::list<StreamSink>& sinks);
void json_to_streams(const std::string& json,
std::list<StreamSource>& sources,
std::list<StreamSink>& sinks);
#endif

87
daemon/log.cpp Normal file
View File

@ -0,0 +1,87 @@
//
// log.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <vector>
#include <boost/algorithm/string.hpp>
#include <boost/log/core.hpp>
#include <boost/log/sinks/syslog_backend.hpp>
#include <boost/log/sources/severity_logger.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/utility/setup/console.hpp>
#include "config.hpp"
#include "log.hpp"
namespace logging = boost::log;
namespace src = boost::log::sources;
namespace sinks = boost::log::sinks;
namespace keywords = boost::log::keywords;
using sink_t = sinks::synchronous_sink<sinks::syslog_backend>;
void log_init(const Config& config) {
boost::shared_ptr<logging::core> core = logging::core::get();
// remove all sink in case of re-configuration
core->remove_all_sinks();
// set log level
core->set_filter(logging::trivial::severity >= config.get_log_severity());
/* if proto is none, syslog is disable, logging on stdout */
if (config.get_syslog_proto() != "none") {
boost::shared_ptr<sinks::syslog_backend> backend;
/* if proto is udp, syslog_server address is used */
if (config.get_syslog_proto() == "udp") {
backend = boost::make_shared<sinks::syslog_backend>(
keywords::facility = sinks::syslog::local0,
keywords::use_impl = sinks::syslog::udp_socket_based);
std::vector<std::string> address;
boost::split(address, config.get_syslog_server(), boost::is_any_of(":"));
if (address.size() == 2) {
// Setup the target address and port to send syslog messages to
backend->set_target_address(address[0],
boost::lexical_cast<int>(address[1]));
}
} else {
/* if proto is local, local syslog is used */
backend = boost::make_shared<sinks::syslog_backend>(
keywords::facility = sinks::syslog::user,
keywords::use_impl = sinks::syslog::native);
}
// Create and fill in another level translator for "MyLevel" attribute of
// type string
sinks::syslog::custom_severity_mapping<std::string> mapping("MyLevel");
mapping["debug"] = sinks::syslog::debug;
mapping["normal"] = sinks::syslog::info;
mapping["warning"] = sinks::syslog::warning;
mapping["failure"] = sinks::syslog::critical;
backend->set_severity_mapper(mapping);
// Wrap it into the frontend and register in the core.
core->add_sink(boost::make_shared<sink_t>(backend));
}
}

29
daemon/log.hpp Normal file
View File

@ -0,0 +1,29 @@
//
// log.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include <boost/log/trivial.hpp>
#include "config.hpp"
void log_init(const Config& config);
#endif

176
daemon/main.cpp Normal file
View File

@ -0,0 +1,176 @@
//
// main.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#include <boost/program_options.hpp>
#include <iostream>
#include <thread>
#include "config.hpp"
#include "driver_manager.hpp"
#include "http_server.hpp"
#include "log.hpp"
#include "session_manager.hpp"
#include "interface.hpp"
namespace po = boost::program_options;
namespace postyle = boost::program_options::command_line_style;
namespace logging = boost::log;
static std::atomic<bool> terminate = false;
void termination_handler(int signum) {
BOOST_LOG_TRIVIAL(info) << "main:: got signal " << signum;
// Terminate program
terminate = true;
}
bool is_terminated() {
return terminate.load();
}
int main(int argc, char* argv[]) {
int rc = EXIT_SUCCESS;
po::options_description desc("Options");
desc.add_options()
("config,c", po::value<std::string>()->default_value("/etc/daemon.conf"),
"daemon configuration file")
("interface_name,i", po::value<std::string>(), "Network interface name")
("http_port,p", po::value<int>(), "HTTP server port")
("help,h", "Print this help message");
int unix_style = postyle::unix_style | postyle::short_allow_next;
po::variables_map vm;
try {
po::store(po::command_line_parser(argc, argv)
.options(desc)
.style(unix_style)
.run(),
vm);
po::notify(vm);
if (vm.count("help")) {
std::cout << "USAGE: " << argv[0] << '\n' << desc << '\n';
return EXIT_SUCCESS;
}
} catch (po::error& poe) {
std::cerr << poe.what() << '\n'
<< "USAGE: " << argv[0] << '\n'
<< desc << '\n';
return EXIT_FAILURE;
}
signal(SIGINT, termination_handler);
signal(SIGTERM, termination_handler);
std::srand(std::time(nullptr));
std::string filename = vm["config"].as<std::string>();
while (!is_terminated() && rc == EXIT_SUCCESS) {
/* load configuration from file */
auto config = Config::parse(filename);
if (config == nullptr) {
return EXIT_FAILURE;
}
/* override configuration according to command line args */
if (vm.count("interface_name")) {
config->set_interface_name(vm["interface_name"].as<std::string>());
}
if (vm.count("http_port")) {
config->set_http_port(vm["http_port"].as<int>());
}
/* init logging */
log_init(*config);
BOOST_LOG_TRIVIAL(debug) << "main:: initializing daemon";
try {
auto driver = DriverManager::create();
/* setup and init driver */
if (driver == nullptr || !driver->init(*config)) {
throw std::runtime_error(std::string("DriverManager:: init failed"));
}
/* start session manager */
auto session_manager = SessionManager::create(driver, config);
if (session_manager == nullptr || !session_manager->start()) {
throw std::runtime_error(
std::string("SessionManager:: start failed"));
}
/* start http server */
HttpServer http_server(session_manager, config);
if (!http_server.start()) {
throw std::runtime_error(std::string("HttpServer:: start failed"));
}
/* load session status from file */
session_manager->load_status();
BOOST_LOG_TRIVIAL(debug) << "main:: init done, entering loop...";
while (!is_terminated()) {
auto [ip_addr, ip_str] = get_interface_ip(config->get_interface_name());
if (!ip_str.empty() && config->get_ip_addr_str() != ip_str) {
BOOST_LOG_TRIVIAL(warning) << "main:: IP address changed, restarting ...";
config->set_ip_addr_str(ip_str);
config->set_ip_addr(ip_addr);
break;
}
if (config->get_need_restart()) {
BOOST_LOG_TRIVIAL(warning) << "main:: config changed, restarting ...";
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
/* save session status to file */
session_manager->save_status();
/* stop http server */
if (!http_server.stop()) {
throw std::runtime_error(
std::string("HttpServer:: stop failed"));
}
/* stop session manager */
if (!session_manager->stop()) {
throw std::runtime_error(
std::string("SessionManager:: stop failed"));
}
/* stop driver manager */
if (!driver->terminate()) {
throw std::runtime_error(
std::string("DriverManager:: terminate failed"));
}
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(fatal) << "main:: fatal exception error: " << e.what();
rc = EXIT_FAILURE;
}
BOOST_LOG_TRIVIAL(info) << "main:: end ";
}
std::cout << "daemon exiting with code: " << rc << std::endl;
return rc;
}

26
daemon/main.hpp Normal file
View File

@ -0,0 +1,26 @@
//
// main.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _MAIN_HPP_
#define _MAIN_HPP_
bool is_terminated();
#endif

88
daemon/netlink.hpp Normal file
View File

@ -0,0 +1,88 @@
//
// netlink.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _NETLINK_HPP_
#define _NETLINK_HPP_
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
template <typename Protocol>
class nl_endpoint {
private:
sockaddr_nl sockaddr {.nl_family = AF_NETLINK};
public:
using protocol_type = Protocol;
using data_type = boost::asio::detail::socket_addr_type;
nl_endpoint() {
sockaddr.nl_groups = 0;
sockaddr.nl_pid = getpid();
}
nl_endpoint(int group, int pid = getpid()) {
sockaddr.nl_groups = group;
sockaddr.nl_pid = pid;
}
nl_endpoint(const nl_endpoint& other) { sockaddr = other.sockaddr; }
nl_endpoint& operator=(const nl_endpoint& other) {
sockaddr = other.sockaddr;
return *this;
}
protocol_type protocol() const { return protocol_type(); }
data_type* data() { return reinterpret_cast<struct sockaddr*>(&sockaddr); }
const data_type* data() const {
return reinterpret_cast<const struct sockaddr*>(&sockaddr);
}
std::size_t size() const { return sizeof(sockaddr); }
void resize(std::size_t size) { /* nothing we can do here */
}
std::size_t capacity() const { return sizeof(sockaddr); }
};
class nl_protocol {
public:
nl_protocol() { proto = 0; }
explicit nl_protocol(int proto) { this->proto = proto; }
int type() const { return SOCK_RAW; }
int protocol() const { return proto; }
int family() const { return PF_NETLINK; }
typedef nl_endpoint<nl_protocol> endpoint;
typedef boost::asio::basic_raw_socket<nl_protocol> socket;
private:
int proto;
};
#endif

103
daemon/netlink_client.hpp Normal file
View File

@ -0,0 +1,103 @@
//
// netlink_client.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _NETLINK_CLIENT_HPP_
#define _NETLINK_CLIENT_HPP_
#include <boost/asio.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp>
#include <cstdlib>
#include <iostream>
#include "netlink.hpp"
using boost::asio::deadline_timer;
class NetlinkClient {
public:
NetlinkClient() = delete;
NetlinkClient(const std::string & name) : name_(name) { }
void init(const nl_endpoint<nl_protocol>& listen_endpoint,
const nl_protocol& protocol) {
socket_.open(protocol);
socket_.bind(listen_endpoint);
deadline_.expires_at(boost::posix_time::pos_infin);
check_deadline();
}
void terminate() {
socket_.close();
}
std::size_t receive(const boost::asio::mutable_buffer& buffer,
boost::posix_time::time_duration timeout,
boost::system::error_code& ec) {
// Set a deadline for the asynchronous operation.
deadline_.expires_from_now(timeout);
ec = boost::asio::error::would_block;
std::size_t length = 0;
// Start the asynchronous operation itself. The handle_receive function
// used as a callback will update the ec and length variables.
nl_endpoint<nl_protocol> endpoint;
socket_.async_receive_from(
boost::asio::buffer(buffer), endpoint,
boost::bind(&NetlinkClient::handle_receive, _1, _2, &ec, &length));
// Block until the asynchronous operation has completed.
do {
io_service_.run_one();
} while (ec == boost::asio::error::would_block);
return length;
}
boost::asio::basic_raw_socket<nl_protocol>& get_socket() { return socket_; }
private:
void check_deadline() {
if (deadline_.expires_at() <= deadline_timer::traits_type::now()) {
socket_.cancel();
deadline_.expires_at(boost::posix_time::pos_infin);
//BOOST_LOG_TRIVIAL(debug) << "netlink_client:: (" << name_ << ") timeout expired";
}
deadline_.async_wait(boost::bind(&NetlinkClient::check_deadline, this));
}
static void handle_receive(const boost::system::error_code& ec,
std::size_t length,
boost::system::error_code* out_ec,
std::size_t* out_length) {
*out_ec = ec;
*out_length = length;
}
private:
boost::asio::io_service io_service_;
boost::asio::basic_raw_socket<nl_protocol> socket_{io_service_};
deadline_timer deadline_{io_service_};
std::string name_;
};
#endif

106
daemon/sap.hpp Normal file
View File

@ -0,0 +1,106 @@
//
// sap.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _SAP_HPP_
#define _SAP_HPP_
#include <boost/asio.hpp>
#include "log.hpp"
using namespace boost::asio;
class SAP {
public:
constexpr static const char addr[] = "224.2.127.254";
constexpr static uint16_t port = 9875;
constexpr static uint16_t max_deletions = 3;
constexpr static uint16_t bandwidth_limit = 4000; // bits x xsec
constexpr static uint16_t min_interval = 300; // secs
constexpr static uint16_t sap_header_len = 24;
constexpr static uint16_t max_length = 1024;
SAP() { socket_.open(boost::asio::ip::udp::v4()); };
bool set_multicast_interface(const std::string& interface_ip) {
ip::address_v4 local_interface = ip::address_v4::from_string(interface_ip);
ip::multicast::outbound_interface oi_option(local_interface);
boost::system::error_code ec;
socket_.set_option(oi_option, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error)
<< "sap::outbound_interface option " << ec.message();
return false;
}
ip::multicast::enable_loopback el_option(true);
socket_.set_option(el_option, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error)
<< "sap::enable_loopback option " << ec.message();
return false;
}
return true;
}
bool announcement(uint16_t msg_id_hash,
uint32_t addr,
const std::string& sdp) {
BOOST_LOG_TRIVIAL(info) << "sap::announcement " << std::hex << msg_id_hash;
return send(true, msg_id_hash, htonl(addr), sdp);
}
bool deletion(uint16_t msg_id_hash, uint32_t addr, const std::string& sdp) {
BOOST_LOG_TRIVIAL(info) << "sap::deletetion " << std::hex << msg_id_hash;
return send(false, msg_id_hash, htonl(addr), sdp);
}
private:
bool send(bool is_announce,
uint16_t msg_id_hash,
uint32_t addr,
const std::string& sdp) {
if (sdp.length() > max_length - sap_header_len) {
BOOST_LOG_TRIVIAL(error) << "sap:: SDP is too long";
return false;
}
uint8_t buffer[max_length];
buffer[0] = is_announce ? 0x20 : 0x24;
buffer[1] = 0;
memcpy(buffer + 2, &msg_id_hash, 2);
memcpy(buffer + 4, &addr, 4);
memcpy(buffer + 8, "application/sdp", 16); /* include trailing 0 */
memcpy(buffer + sap_header_len, sdp.c_str(), sdp.length());
try {
socket_.send_to(
boost::asio::buffer(buffer, sap_header_len + sdp.length()), remote_);
} catch (boost::system::error_code& ec) {
BOOST_LOG_TRIVIAL(error) << "sap::send_to " << ec.message();
return false;
}
return true;
}
io_service io_service_;
ip::udp::socket socket_{io_service_};
ip::udp::endpoint remote_{
ip::udp::endpoint(ip::address::from_string(addr), port)};
};
#endif

View File

@ -0,0 +1 @@
sudo ptp4l -i $1 -m -l7 -E -S

View File

@ -0,0 +1 @@
sudo ptp4l -i $1 -m -l7 -s -S

1
daemon/scripts/sink_record.sh Executable file
View File

@ -0,0 +1 @@
arecord -D hw:1,0 -f cd -d 30 -r 44100 -t wav sink.wav

1
daemon/scripts/source_play.sh Executable file
View File

@ -0,0 +1 @@
speaker-test -D hw:1,0 -r 44100 -c 2 -t sine

999
daemon/session_manager.cpp Normal file
View File

@ -0,0 +1,999 @@
//
// seesion_manager.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH 4096 //max for SDP file
#include <httplib.h>
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <experimental/map>
#include <iostream>
#include <chrono>
#include <map>
#include <set>
#include "json.hpp"
#include "log.hpp"
#include "session_manager.hpp"
static uint8_t get_codec_word_lenght(const std::string& codec) {
if (codec == "L16") {
return 2;
}
if (codec == "L24") {
return 3;
}
if (codec == "L2432") {
return 4;
}
if (codec == "DSD64") {
return 1;
}
if (codec == "DSD128") {
return 2;
}
if (codec == "DSD64_32" || codec == "DSD128_32" || codec == "DSD256") {
return 4;
}
return 0;
}
static std::tuple<bool /* res */,
std::string /* protocol */,
std::string /* host */,
std::string /* port */,
std::string /* path */>
parse_url(const std::string& _url) {
std::string url = httplib::detail::decode_url(_url);
size_t protocol_sep_pos = url.find_first_of("://");
if (protocol_sep_pos == std::string::npos) {
/* no protocol, invalid URL */
return std::make_tuple(false, "", "", "", "");
}
std::string port, host, path("/");
std::string protocol = url.substr(0, protocol_sep_pos);
std::string url_new = url.substr(protocol_sep_pos + 3);
size_t path_sep_pos = url_new.find_first_of("/");
size_t port_sep_pos = url_new.find_first_of(":");
if (port_sep_pos != std::string::npos) {
/* port specified */
if (path_sep_pos != std::string::npos) {
/* path specified */
port = url_new.substr(port_sep_pos + 1, path_sep_pos - port_sep_pos - 1);
path = url_new.substr(path_sep_pos);
} else {
/* path not specified */
port = url_new.substr(port_sep_pos + 1);
}
host = url_new.substr(0, port_sep_pos);
} else if (path_sep_pos != std::string::npos) {
/* port not specified, path specified */
host = url_new.substr(0, path_sep_pos);
path = url_new.substr(path_sep_pos);
} else {
/* port and path not specified */
host = url_new;
}
return std::make_tuple(host.length() > 0, protocol, host, port, path);
}
bool SessionManager::parse_sdp(const std::string sdp, StreamInfo& info) const {
/*
v=0
o=- 4 0 IN IP4 10.0.0.12
s=ALSA (on ubuntu)_4
c=IN IP4 239.1.0.12/15
t=0 0
a=clock-domain:PTPv2 0
m=audio 5004 RTP/AVP 98
c=IN IP4 239.1.0.12/15
a=rtpmap:98 L16/44100/8
a=sync-time:0
a=framecount:64-192
a=ptime:4.35374165
a=mediaclk:direct=0
a=ts-refclk:ptp=IEEE1588-2008:00-0C-29-FF-FE-0E-90-C8:0
a=recvonly
*/
int num = 0;
try {
enum class sdp_parser_status { init, time, media };
sdp_parser_status status = sdp_parser_status::init;
std::stringstream ssstrem(sdp);
std::string line;
while (getline(ssstrem, line, '\n')) {
++num;
if (line[1] != '=') {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: invalid SDP file at line " << num;
return false;
}
std::string val = line.substr(2);
switch (line[0]) {
case 'v':
/* v=0 */
if (val != "0") {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: unsupported SDP version at line " << num;
return false;
}
break;
case 'o':
break;
case 't':
/* t=0 0 */
status = sdp_parser_status::time;
break;
case 'a': {
auto pos = val.find(':');
if (pos == std::string::npos) {
/* skip this attribute */
break;
}
std::string name = val.substr(0, pos);
std::string value = val.substr(pos + 1);
switch (status) {
case sdp_parser_status::init:
break;
case sdp_parser_status::time:
/* time attributes */
if (name == "clock-domain") {
/* a=clock-domain:PTPv2 0 */
if (value.substr(0, 5) != "PTPv2") {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: unsupported PTP "
"clock version in SDP at line "
<< num;
return false;
}
}
break;
case sdp_parser_status::media:
/* audio media attributes */
if (name == "rtpmap") {
/* a=rtpmap:98 L16/44100/8 */
std::vector<std::string> fields;
boost::split(fields, value,
[line](char c) { return c == ' ' || c == '/'; });
if (fields.size() < 4) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: invalid audio "
"rtpmap in SDP at line "
<< num;
return false;
}
// if matching payload
if (info.stream.m_byPayloadType == std::stoi(fields[0])) {
strncpy(info.stream.m_cCodec, fields[1].c_str(),
sizeof(info.stream.m_cCodec) - 1);
info.stream.m_byWordLength = get_codec_word_lenght(fields[1]);
info.stream.m_ui32SamplingRate = std::stoi(fields[2]);
if (info.stream.m_byNbOfChannels != std::stoi(fields[3])) {
BOOST_LOG_TRIVIAL(warning)
<< "session_manager:: invalid audio channel "
"number in SDP at line "
<< num << ", using "
<< (int)info.stream.m_byNbOfChannels;
/*return false; */
}
}
} else if (name == "sync-time") {
/* a=sync-time:0 */
info.stream.m_ui32RTPTimestampOffset = std::stoi(value);
} else if (name == "framecount") {
/* a=framecount:64-192 */
} else if (name == "ptime") {
/* a=mediaclk:ptime=4.35374165 */
info.stream.m_ui32MaxSamplesPerPacket =
(static_cast<double>(info.stream.m_ui32SamplingRate) *
std::stod(value)) /
1000;
} else if (name == "mediaclk") {
/* a=mediaclk:direct=0 */
std::vector<std::string> fields;
boost::split(fields, value,
[line](char c) { return c == ':'; });
if (fields.size() == 2 && fields[0] == "direct") {
info.stream.m_ui32RTPTimestampOffset = std::stoi(fields[1]);
}
} else if (name == "ts-refclk" && !info.ignore_refclk_gmid) {
/* a=ts-refclk:ptp=IEEE1588-2008:00-0C-29-FF-FE-0E-90-C8:0 */
std::vector<std::string> fields;
boost::split(fields, value,
[line](char c) { return c == ':'; });
if (fields.size() == 3) {
if (fields[1] != ptp_status_.gmid ||
stoi(fields[2]) != ptp_config_.domain) {
BOOST_LOG_TRIVIAL(warning)
<< "session_manager:: configured PTP grand master "
"clock "
"doesn't match the PTP clock in SDP at line "
<< num;
return false;
}
}
}
break;
}
} break;
case 'm': {
/* m=audio 5004 RTP/AVP 98 */
std::vector<std::string> fields;
boost::split(fields, val, [line](char c) { return c == ' '; });
if (fields.size() < 4) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: invalid nedia in SDP at line " << num;
return false;
}
if (fields[0] == "audio") {
info.stream.m_usDestPort = std::stoi(fields[1]);
info.stream.m_byPayloadType =
std::stoi(fields[3]); /* take first payload */
status = sdp_parser_status::media;
}
break;
}
case 'c':
/* c=IN IP4 239.1.0.12/15 */
/* connction info of audio media */
if (status == sdp_parser_status::media) {
std::vector<std::string> fields;
boost::split(fields, val,
[line](char c) { return c == ' ' || c == '/'; });
if (fields.size() != 4) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: invalid connection in SDP at line "
<< num;
return false;
}
if (fields[0] != "IN" || fields[1] != "IP4") {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: unsupported connection in SDP at line "
<< num;
return false;
}
info.stream.m_ui32DestIP =
ip::address_v4::from_string(fields[2].c_str()).to_ulong();
if (info.stream.m_ui32DestIP == INADDR_NONE) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: invalid IPv4 "
"connection address in SDP at line "
<< num;
return false;
}
info.stream.m_byTTL = std::stoi(fields[3]);
}
break;
default:
break;
}
}
} catch (...) {
BOOST_LOG_TRIVIAL(fatal) << "session_manager:: invalid SDP at line " << num
<< ", cannot perform number convesrion";
return false;
}
return true;
}
std::shared_ptr<SessionManager> SessionManager::create(
std::shared_ptr<DriverManager> driver,
std::shared_ptr<Config> config) {
// no need to be thread-safe here
static std::weak_ptr<SessionManager> instance;
if (auto ptr = instance.lock()) {
return ptr;
}
auto ptr =
std::shared_ptr<SessionManager>(new SessionManager(driver, config));
instance = ptr;
return ptr;
}
std::error_code SessionManager::get_source(uint8_t id,
StreamSource& source) const {
std::shared_lock sources_lock(sources_mutex_);
auto const it = sources_.find(id);
if (it == sources_.end()) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: source " << id << " not in use";
return DaemonErrc::stream_id_not_in_use;
}
const auto& info = (*it).second;
source = get_source_(id, info);
return std::error_code{};
}
std::error_code SessionManager::get_sink(uint8_t id, StreamSink& sink) const {
std::shared_lock sinks_lock(sinks_mutex_);
auto const it = sinks_.find(id);
if (it == sinks_.end()) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: sink " << id << " not in use";
return DaemonErrc::stream_id_not_in_use;
}
const auto& info = (*it).second;
sink = get_sink_(id, info);
return std::error_code{};
}
std::list<StreamSink> SessionManager::get_sinks() const {
std::shared_lock sinks_lock(sinks_mutex_);
std::list<StreamSink> sinks_list;
for (auto const& [id, info] : sinks_) {
sinks_list.emplace_back(get_sink_(id, info));
}
return sinks_list;
}
std::list<StreamSource> SessionManager::get_sources() const {
std::shared_lock sources_lock(sources_mutex_);
std::list<StreamSource> sources_list;
for (auto const& [id, info] : sources_) {
sources_list.emplace_back(get_source_(id, info));
}
return sources_list;
}
StreamSource SessionManager::get_source_(uint8_t id,
const StreamInfo& info) const {
StreamSource source;
source.id = id;
source.enabled = info.enabled;
source.name = info.stream.m_cName;
source.io = info.io;
source.max_samples_per_packet = info.stream.m_ui32MaxSamplesPerPacket;
source.codec = info.stream.m_cCodec;
source.ttl = info.stream.m_byTTL;
source.payload_type = info.stream.m_byPayloadType;
source.dscp = info.stream.m_ucDSCP;
source.refclk_ptp_traceable = info.refclk_ptp_traceable;
for (auto i = 0; i < info.stream.m_byNbOfChannels; i++) {
source.map.push_back(info.stream.m_aui32Routing[i]);
}
return source;
}
StreamSink SessionManager::get_sink_(uint8_t id, const StreamInfo& info) const {
StreamSink sink;
sink.id = id;
sink.name = info.stream.m_cName;
sink.io = info.io;
sink.use_sdp = info.sink_use_sdp;
sink.sdp = info.sink_sdp;
sink.source = info.sink_source;
sink.delay = info.stream.m_ui32PlayOutDelay;
sink.ignore_refclk_gmid = info.ignore_refclk_gmid;
for (auto i = 0; i < info.stream.m_byNbOfChannels; i++) {
sink.map.push_back(info.stream.m_aui32Routing[i]);
}
return sink;
}
bool SessionManager::load_status() {
if (config_->get_status_file().empty()) {
return true;
}
std::ifstream jsonstream(config_->get_status_file());
if (!jsonstream) {
BOOST_LOG_TRIVIAL(fatal)
<< "session_manager:: cannot load status file " << config_->get_status_file();
return false;
}
std::list<StreamSource> sources_list;
std::list<StreamSink> sinks_list;
try {
json_to_streams(jsonstream, sources_list, sinks_list);
} catch (const std::runtime_error& e) {
BOOST_LOG_TRIVIAL(fatal)
<< "session_manager:: cannot parse status file " << e.what();
return false;
}
for (auto const& source : sources_list) {
add_source(source);
}
for (auto const& sink : sinks_list) {
add_sink(sink);
}
return true;
}
bool SessionManager::save_status() {
if (config_->get_status_file().empty()) {
return true;
}
std::ofstream jsonstream(config_->get_status_file());
if (!jsonstream) {
BOOST_LOG_TRIVIAL(fatal) << "session_manager:: cannot save to status file "
<< config_->get_status_file();
return false;
}
jsonstream << streams_to_json(get_sources(), get_sinks());
BOOST_LOG_TRIVIAL(info) << "session_manager:: status file saved";
return true;
}
std::error_code SessionManager::add_source(const StreamSource& source) {
if (source.id > stream_id_max) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: source id "
<< std::to_string(source.id) << " is not valid";
return DaemonErrc::invalid_stream_id;
}
StreamInfo info;
memset(&info.stream, 0, sizeof info.stream);
info.stream.m_bSource = 1; // source
info.stream.m_ui32CRTP_stream_info_sizeof = sizeof(info.stream);
strncpy(info.stream.m_cName, source.name.c_str(),
sizeof(info.stream.m_cName) - 1);
info.stream.m_ucDSCP = source.dscp; // IPv4 DSCP
info.stream.m_byTTL = source.ttl;
info.stream.m_byPayloadType = source.payload_type;
info.stream.m_byWordLength = get_codec_word_lenght(source.codec);
info.stream.m_byNbOfChannels = source.map.size();
strncpy(info.stream.m_cCodec, source.codec.c_str(),
sizeof(info.stream.m_cCodec) - 1);
info.stream.m_ui32MaxSamplesPerPacket = source.max_samples_per_packet; // only for Source
info.stream.m_ui32SamplingRate = driver_->get_current_sample_rate(); // last set from driver or config
info.stream.m_uiId = source.id;
info.stream.m_ui32RTCPSrcIP = config_->get_ip_addr();
info.stream.m_ui32SrcIP = config_->get_ip_addr(); // only for Source
info.stream.m_ui32DestIP =
ip::address_v4::from_string(config_->get_rtp_mcast_base().c_str()).to_ulong() +
source.id;
info.stream.m_usSrcPort = config_->get_rtp_port();
info.stream.m_usDestPort = config_->get_rtp_port();
info.stream.m_ui32SSRC = rand() % 65536; // use random number
std::copy(source.map.begin(), source.map.end(), info.stream.m_aui32Routing);
std::copy(std::begin(config_->get_mac_addr()), std::end(config_->get_mac_addr()),
info.stream.m_ui8DestMAC);
info.refclk_ptp_traceable = source.refclk_ptp_traceable;
info.enabled = source.enabled;
info.io = source.io;
// info.m_ui32PlayOutDelay = 0; // only for Sink
std::unique_lock sources_lock(sources_mutex_);
auto const it = sources_.find(source.id);
if (it != sources_.end()) {
BOOST_LOG_TRIVIAL(info) << "session_manager:: source id "
<< std::to_string(source.id) << " is in use, updating";
const auto& info = (*it).second;
// remove previous stream if enabled
if (info.enabled) {
(void)driver_->remove_rtp_stream(info.handle);
}
}
std::error_code ret;
if (info.enabled) {
ret = driver_->add_rtp_stream(info.stream, info.handle);
if (ret) {
return ret;
}
igmp_.join(config_->get_ip_addr_str(),
ip::address_v4(info.stream.m_ui32DestIP).to_string());
}
// update source map
sources_[source.id] = info;
BOOST_LOG_TRIVIAL(info) << "session_manager:: added source "
<< std::to_string(source.id) << " " << info.handle;
return ret;
}
std::string SessionManager::get_removed_source_sdp_(uint32_t id,
uint32_t addr) const {
std::string sdp("o=- " + std::to_string(id) + " 0 IN IP4 " +
ip::address_v4(addr).to_string() + "\n");
return sdp;
}
std::string SessionManager::get_source_sdp_(uint32_t id,
const StreamInfo& info) const {
std::shared_lock ptp_lock(ptp_mutex_);
uint32_t sample_rate = driver_->get_current_sample_rate();
// need a 12 digit precision for ptime
std::ostringstream ss_ptime;
ss_ptime.precision(12);
ss_ptime << std::fixed
<< static_cast<double>(info.stream.m_ui32MaxSamplesPerPacket) * 1000 /
static_cast<double>(sample_rate);
std::string ptime = ss_ptime.str();
ptime.erase(ptime.find_last_not_of('0') + 1, std::string::npos); // remove trailing zeros
// build SDP
std::stringstream ss;
ss << "v=0\n"
<< "o=- " << static_cast<unsigned>(id) << " 0 IN IP4 "
<< ip::address_v4(info.stream.m_ui32SrcIP).to_string() << "\n"
<< "s=" << info.stream.m_cName << "\n"
<< "c=IN IP4 " << ip::address_v4(info.stream.m_ui32DestIP).to_string()
<< "/" << static_cast<unsigned>(info.stream.m_byTTL) << "\n"
<< "t=0 0\n"
<< "a=clock-domain:PTPv2 " << static_cast<unsigned>(ptp_config_.domain)
<< "\n"
<< "m=audio " << info.stream.m_usSrcPort << " RTP/AVP "
<< static_cast<unsigned>(info.stream.m_byPayloadType) << "\n"
<< "c=IN IP4 " << ip::address_v4(info.stream.m_ui32DestIP).to_string()
<< "/" << static_cast<unsigned>(info.stream.m_byTTL) << "\n"
<< "a=rtpmap:" << static_cast<unsigned>(info.stream.m_byPayloadType) << " "
<< info.stream.m_cCodec << "/" << sample_rate << "/"
<< static_cast<unsigned>(info.stream.m_byNbOfChannels) << "\n"
<< "a=sync-time:0\n"
<< "a=framecount:" << info.stream.m_ui32MaxSamplesPerPacket << "\n"
<< "a=ptime:" << ptime << "\n"
<< "a=mediaclk:direct=0\n";
if (info.refclk_ptp_traceable) {
ss << "a=ts-refclk:ptp=traceable\n";
} else {
ss << "a=ts-refclk:ptp=IEEE1588-2008:" << ptp_status_.gmid << ":"
<< static_cast<unsigned>(ptp_config_.domain) << "\n";
}
ss << "a=recvonly\n";
return ss.str();
}
std::error_code SessionManager::get_source_sdp(uint32_t id,
std::string& sdp) const {
std::shared_lock sources_lock(sources_mutex_);
auto const it = sources_.find(id);
if (it == sources_.end()) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: source " << id << " not in use";
return DaemonErrc::stream_id_not_in_use;
}
const auto& info = (*it).second;
sdp = get_source_sdp_(id, info);
return std::error_code{};
}
std::error_code SessionManager::remove_source(uint32_t id) {
if (id > stream_id_max) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: source id "
<< std::to_string(id) << " is not valid";
return DaemonErrc::invalid_stream_id;
}
std::unique_lock sources_lock(sources_mutex_);
auto const it = sources_.find(id);
if (it == sources_.end()) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: source " << id << " not in use";
return DaemonErrc::stream_id_not_in_use;
}
std::error_code ret;
const auto& info = (*it).second;
if (info.enabled) {
ret = driver_->remove_rtp_stream(info.handle);
if (!ret) {
igmp_.leave(config_->get_ip_addr_str(),
ip::address_v4(info.stream.m_ui32DestIP).to_string());
}
}
if (!ret) {
sources_.erase(id);
}
return ret;
}
std::error_code SessionManager::add_sink(const StreamSink& sink) {
if (sink.id > stream_id_max) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: sink id "
<< std::to_string(sink.id) << " is not valid";
return DaemonErrc::invalid_stream_id;
}
StreamInfo info;
memset(&info.stream, 0, sizeof info.stream);
info.stream.m_bSource = 0; // sink
info.stream.m_ui32CRTP_stream_info_sizeof = sizeof(info.stream);
strncpy(info.stream.m_cName, sink.name.c_str(),
sizeof(info.stream.m_cName) - 1);
info.stream.m_uiId = sink.id;
info.stream.m_byNbOfChannels = sink.map.size();
std::copy(sink.map.begin(), sink.map.end(), info.stream.m_aui32Routing);
info.stream.m_ui32PlayOutDelay = sink.delay;
info.stream.m_ui32RTCPSrcIP = config_->get_ip_addr();
info.ignore_refclk_gmid = sink.ignore_refclk_gmid;
info.io = sink.io;
if (!sink.use_sdp) {
auto const [ok, protocol, host, port, path] = parse_url(sink.source);
if (!ok) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: cannot parse URL " << sink.source;
return DaemonErrc::invalid_url;
}
if (!boost::iequals(protocol, "http")) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: unsupported protocol in URL " << sink.source;
return DaemonErrc::invalid_url;
}
httplib::Client cli(host.c_str(),
!atoi(port.c_str()) ? 80 : atoi(port.c_str()));
cli.set_timeout_sec(10);
auto res = cli.Get(path.c_str());
if (!res) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: cannot retrieve SDP from URL " << sink.source;
return DaemonErrc::cannot_retrieve_sdp;
}
if (res->status != 200) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: cannot retrieve SDP from URL " << sink.source
<< " server reply " << res->status;
return DaemonErrc::cannot_retrieve_sdp;
}
BOOST_LOG_TRIVIAL(info)
<< "session_manager:: SDP from URL " << sink.source << " :\n"
<< res->body;
if (!parse_sdp(res->body, info)) {
return DaemonErrc::cannot_parse_sdp;
}
info.sink_sdp = res->body;
} else {
BOOST_LOG_TRIVIAL(info) << "session_manager:: using SDP " << sink.sdp;
if (!parse_sdp(sink.sdp, info)) {
return DaemonErrc::cannot_parse_sdp;
}
info.sink_sdp = sink.sdp;
}
info.sink_source = sink.source;
info.sink_use_sdp = true; // save back and use with SDP file
info.stream.m_ui32FrameSize = info.stream.m_ui32MaxSamplesPerPacket *
info.stream.m_byNbOfChannels * info.stream.m_byWordLength;
if (!info.stream.m_ui32FrameSize) {
// if not from SDP use config
info.stream.m_ui32FrameSize = config_->get_max_tic_frame_size();
}
BOOST_LOG_TRIVIAL(info) << "session_manager:: sink samples per packet " <<
info.stream.m_ui32MaxSamplesPerPacket;
BOOST_LOG_TRIVIAL(info) << "session_manager:: sink frame size " <<
info.stream.m_ui32FrameSize;
// info.m_ui32SrcIP = addr; // only for Source
// info.m_usSrcPort = 5004;
// info.m_ui32MaxSamplesPerPacket = 48;
// info.m_ui32SSRC = 65544;
// info.m_ucDSCP = source.dscp;
// info.m_byTTL = source.ttl;
// info.m_ui8DestMAC
std::unique_lock sinks_lock(sinks_mutex_);
auto const it = sinks_.find(sink.id);
if (it != sinks_.end()) {
BOOST_LOG_TRIVIAL(info) << "session_manager:: sink id "
<< std::to_string(sink.id) << " is in use, updating";
// remove previous stream
const auto& info = (*it).second;
(void)driver_->remove_rtp_stream(info.handle);
}
auto ret = driver_->add_rtp_stream(info.stream, info.handle);
if (ret) {
return ret;
}
if (it == sinks_.end()) {
igmp_.join(config_->get_ip_addr_str(),
ip::address_v4(info.stream.m_ui32DestIP).to_string());
}
// update sinks map
sinks_[sink.id] = info;
BOOST_LOG_TRIVIAL(info) << "session_manager:: added sink "
<< std::to_string(sink.id) << " " << info.handle;
return ret;
}
std::error_code SessionManager::remove_sink(uint32_t id) {
if (id > stream_id_max) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: sink id "
<< std::to_string(id) << " is not valid";
return DaemonErrc::stream_id_in_use;
}
std::unique_lock sinks_lock(sinks_mutex_);
auto const it = sinks_.find(id);
if (it == sinks_.end()) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: sink " << id << " not in use";
return DaemonErrc::stream_id_not_in_use;
}
const auto& info = (*it).second;
auto ret = driver_->remove_rtp_stream(info.handle);
if (!ret) {
igmp_.leave(config_->get_ip_addr_str(),
ip::address_v4(info.stream.m_ui32DestIP).to_string());
sinks_.erase(id);
}
return ret;
}
std::error_code SessionManager::get_sink_status(
uint32_t id,
SinkStreamStatus& sink_status) const {
if (id > stream_id_max) {
BOOST_LOG_TRIVIAL(error) << "session_manager:: sink id "
<< std::to_string(id) << " is not valid";
return DaemonErrc::invalid_stream_id;
}
std::unique_lock sinks_lock(sinks_mutex_);
auto const it = sinks_.find(id);
if (it == sinks_.end()) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: sink " << id << " not in use";
return DaemonErrc::stream_id_not_in_use;
}
TRTP_stream_status status;
const auto& info = (*it).second;
auto ret = driver_->get_rtp_stream_status(info.handle, status);
if (!ret) {
sink_status.is_rtp_seq_id_error = status.u.flags & 0x01;
sink_status.is_rtp_ssrc_error = status.u.flags & 0x02;
sink_status.is_rtp_payload_type_error = status.u.flags & 0x04;
sink_status.is_rtp_sac_error = status.u.flags & 0x08;
sink_status.is_receiving_rtp_packet = status.u.flags & 0x10;
sink_status.is_muted = status.u.flags & 0x20;
sink_status.is_some_muted = status.u.flags & 0x40;
sink_status.min_time = status.sink_min_time;
}
return ret;
}
std::error_code SessionManager::set_ptp_config(const PTPConfig& config) {
TPTPConfig ptp_config;
ptp_config.ui8Domain = config.domain;
ptp_config.ui8DSCP = config.dscp;
auto ret = driver_->set_ptp_config(ptp_config);
if (!ret) {
std::unique_lock ptp_lock(ptp_mutex_);
ptp_config_ = config;
}
return ret;
}
void SessionManager::get_ptp_config(PTPConfig& config) const {
std::shared_lock ptp_lock(ptp_mutex_);
config = ptp_config_;
}
void SessionManager::get_ptp_status(PTPStatus& status) const {
std::shared_lock ptp_lock(ptp_mutex_);
status = ptp_status_;
}
static uint16_t crc16(const uint8_t* p, size_t len) {
uint8_t x;
uint16_t crc = 0xFFFF;
while (len--) {
x = crc >> 8 ^ *p++;
x ^= x >> 4;
crc = (crc << 8) ^
(static_cast<uint16_t>(x << 12)) ^
(static_cast<uint16_t>(x << 5)) ^
(static_cast<uint16_t>(x));
}
return crc;
}
size_t SessionManager::process_sap() {
size_t sdp_len_sum = 0;
// set to contain sources currently announced
std::set<uint32_t> active_sources;
// announce all active sources
std::shared_lock sources_lock(sources_mutex_);
for (auto const& [id, info] : sources_) {
if (info.enabled) {
// retrieve current active source SDP
std::string sdp = get_source_sdp_(id, info);
// compute source 16bit crc
uint16_t msg_crc =
crc16(reinterpret_cast<const uint8_t*>(sdp.c_str()), sdp.length());
// compute source hash
uint32_t msg_id_hash = (static_cast<uint32_t>(id) << 16) + msg_crc;
// add/update this source in the announced sources
announced_sources_[msg_id_hash] =
std::make_pair(id, info.stream.m_ui32RTCPSrcIP);
// add this source to the active sources
active_sources.insert(msg_id_hash);
// remove this source from deleted sources
deleted_sources_count_.erase(msg_id_hash);
// send announcement for this source
sap_.announcement(msg_crc, info.stream.m_ui32RTCPSrcIP, sdp);
// update amount of byte sent
sdp_len_sum += sdp.length();
}
}
// check for sources that are no more announced and send deletion/s
for (auto const& [msg_id_hash, pair] : announced_sources_) {
const auto &id = pair.first;
const auto &src_addr = pair.second;
// check if this source is no more announced
if (active_sources.find(msg_id_hash) ==
active_sources.end()) {
// retrieve deleted source SDP
std::string sdp = get_removed_source_sdp_(id, src_addr);
// send deletion for this source
sap_.deletion(static_cast<uint16_t>(msg_id_hash), src_addr, sdp);
// update amount of byte sent
sdp_len_sum += sdp.length();
// increase count
deleted_sources_count_[msg_id_hash]++;
}
}
// remove all deleted sources announced SAP::max_deletions times
std::experimental::erase_if(announced_sources_, [this](auto source) {
const auto &msg_id_hash = source.first;
if (this->deleted_sources_count_[msg_id_hash] >= SAP::max_deletions) {
// remove from deleted sources
this->deleted_sources_count_.erase(msg_id_hash);
// remove from announced sources
return true;
}
return false;
});
return sdp_len_sum;
}
bool SessionManager::worker() {
using clock_ = std::chrono::high_resolution_clock;
using second_t = std::chrono::duration<double, std::ratio<1> >;
using timepoint_t = std::chrono::time_point<clock_>;
TPTPConfig ptp_config;
TPTPStatus ptp_status;
timepoint_t sap_timepoint = clock_::now();
timepoint_t ptp_timepoint = clock_::now();
int sap_interval = 1;
int ptp_interval = 0;
sap_.set_multicast_interface(config_->get_ip_addr_str());
while (running_) {
// check if it's time to update the PTP status
if (std::chrono::duration_cast<second_t> (clock_::now() - ptp_timepoint).count() >
ptp_interval) {
ptp_timepoint = clock_::now();
if (driver_->get_ptp_config(ptp_config) ||
driver_->get_ptp_status(ptp_status)) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: failed to retrieve PTP clock info";
// return false;
} else {
char ptp_clock_id[24];
snprintf(ptp_clock_id, sizeof(ptp_clock_id),
"%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X",
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[0]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[1]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[2]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[3]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[4]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[5]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[6]),
(reinterpret_cast<uint8_t*>(&ptp_status.ui64GMID)[7]));
// update PTP clock status
std::unique_lock ptp_lock(ptp_mutex_);
ptp_status_.gmid = ptp_clock_id;
ptp_status_.jitter = ptp_status.i32Jitter;
std::string new_ptp_status;
switch (ptp_status.nPTPLockStatus) {
case PTPLS_UNLOCKED:
new_ptp_status = "unlocked";
break;
case PTPLS_LOCKING:
new_ptp_status = "locking";
break;
case PTPLS_LOCKED:
new_ptp_status = "locked";
break;
}
if (ptp_status_.status != new_ptp_status) {
BOOST_LOG_TRIVIAL(info)
<< "session_manager:: new PTP clock status " << new_ptp_status;
ptp_status_.status = new_ptp_status;
if (new_ptp_status == "locked") {
// set sample rate, this may require seconds
(void)driver_->set_sample_rate(driver_->get_current_sample_rate());
}
}
// update config
ptp_config_.domain = ptp_config.ui8Domain;
ptp_config_.dscp = ptp_config.ui8DSCP;
}
ptp_interval = 10;
}
// check if it's time to send SAP announcements
if (std::chrono::duration_cast<second_t> (clock_::now() - sap_timepoint).count() >
sap_interval) {
sap_timepoint = clock_::now();
auto sdp_len_sum = process_sap();
if (config_->get_sap_interval()) {
// if announcement interval specified in configuration
sap_interval = config_->get_sap_interval();
} else {
// compute next announcement interval
sap_interval = std::max(static_cast<size_t>(SAP::min_interval),
sdp_len_sum * 8 / SAP::bandwidth_limit);
sap_interval +=
(std::rand() % (sap_interval * 2 / 3)) - (sap_interval / 3);
}
BOOST_LOG_TRIVIAL(info) << "session_manager:: next SAP announcements in "
<< sap_interval << " secs";
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// at end, send deletion for all announced sources
for (auto const& [msg_id_hash, pair] : announced_sources_) {
const auto &id = pair.first;
const auto &addr = pair.second;
// retrieve deleted source SDP
std::string sdp = get_removed_source_sdp_(id, addr);
// send deletion for this source
sap_.deletion(static_cast<uint16_t>(msg_id_hash), addr, sdp);
}
return true;
}

192
daemon/session_manager.hpp Normal file
View File

@ -0,0 +1,192 @@
//
// session_manager.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
#ifndef _SESSION_MANAGER_HPP_
#define _SESSION_MANAGER_HPP_
#include <future>
#include <map>
#include <shared_mutex>
#include <thread>
#include "config.hpp"
#include "driver_manager.hpp"
#include "igmp.hpp"
#include "sap.hpp"
struct StreamSource {
uint8_t id{0};
bool enabled{false};
std::string name;
std::string io;
std::vector<uint8_t> map;
uint32_t max_samples_per_packet{0};
std::string codec;
uint8_t ttl{0};
uint8_t payload_type{0};
uint8_t dscp{0};
bool refclk_ptp_traceable{false};
};
struct StreamSink {
uint8_t id;
std::string name;
std::string io;
bool use_sdp{false};
std::string source;
std::string sdp;
uint32_t delay{0};
bool ignore_refclk_gmid{false};
std::vector<uint8_t> map;
};
struct SinkStreamStatus {
bool is_rtp_seq_id_error{false};
bool is_rtp_ssrc_error{false};
bool is_rtp_payload_type_error{false};
bool is_rtp_sac_error{false};
bool is_receiving_rtp_packet{false};
bool is_muted{false};
bool is_some_muted{false};
int min_time{0};
};
struct PTPConfig {
uint8_t domain{0};
uint8_t dscp{0};
};
struct PTPStatus {
std::string status;
std::string gmid;
int32_t jitter{0};
};
struct StreamInfo {
TRTP_stream_info stream;
uint64_t handle{0};
bool enabled{0};
bool refclk_ptp_traceable{false};
bool ignore_refclk_gmid{false};
std::string io;
bool sink_use_sdp{true};
std::string sink_source;
std::string sink_sdp;
};
class SessionManager {
public:
constexpr static uint8_t stream_id_max = 63;
static std::shared_ptr<SessionManager> create(
std::shared_ptr<DriverManager> driver,
std::shared_ptr<Config> config);
SessionManager(const SessionManager&) = delete;
SessionManager& operator=(const SessionManager&) = delete;
virtual ~SessionManager(){ stop(); };
// session manager interface
bool start() {
if (!running_) {
running_ = true;
res_ = std::async(std::launch::async, &SessionManager::worker, this);
}
return true;
}
bool stop() {
if (running_) {
running_ = false;
auto ret = res_.get();
for (auto source : get_sources()) {
remove_source(source.id);
}
for (auto sink : get_sinks()) {
remove_sink(sink.id);
}
return ret;
}
return true;
}
std::error_code add_source(const StreamSource& source);
std::error_code get_source(uint8_t id, StreamSource& source) const;
std::list<StreamSource> get_sources() const;
std::error_code get_source_sdp(uint32_t id, std::string& sdp) const;
std::error_code remove_source(uint32_t id);
std::error_code add_sink(const StreamSink& sink);
std::error_code get_sink(uint8_t id, StreamSink& sink) const;
std::list<StreamSink> get_sinks() const;
std::error_code get_sink_status(uint32_t id, SinkStreamStatus& status) const;
std::error_code remove_sink(uint32_t id);
std::error_code set_ptp_config(const PTPConfig& config);
void get_ptp_config(PTPConfig& config) const;
void get_ptp_status(PTPStatus& status) const;
bool load_status();
bool save_status();
size_t process_sap();
protected:
std::string get_removed_source_sdp_(uint32_t id, uint32_t src_addr) const;
std::string get_source_sdp_(uint32_t id, const StreamInfo& info) const;
StreamSource get_source_(uint8_t id, const StreamInfo& info) const;
StreamSink get_sink_(uint8_t id, const StreamInfo& info) const;
bool parse_sdp(const std::string sdp, StreamInfo& info) const;
bool worker();
// singleton, use create() to build
SessionManager(std::shared_ptr<DriverManager> driver,
std::shared_ptr<Config> config)
: driver_(driver), config_(config){};
std::shared_ptr<DriverManager> driver_;
std::shared_ptr<Config> config_;
std::future<bool> res_;
std::atomic_bool running_{false};
/* current sources */
std::map<uint8_t /* id */, StreamInfo> sources_;
mutable std::shared_mutex sources_mutex_;
/* current sinks */
std::map<uint8_t /* id */, StreamInfo> sinks_;
mutable std::shared_mutex sinks_mutex_;
/* current announced sources */
std::map<uint32_t /* msg_id_hash */,
std::pair<uint8_t /* id */, uint32_t /* src_addr */> >
announced_sources_;
/* number of deletions sent for a a deleted source */
std::map<uint32_t /* msg_id_hash */, int /* count */>
deleted_sources_count_;
PTPConfig ptp_config_;
PTPStatus ptp_status_;
mutable std::shared_mutex ptp_mutex_;
SAP sap_;
IGMP igmp_;
};
#endif

11
daemon/tests/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# cmake
/CMakeFiles
/Testings
CMakeCache.txt
cmake_install.cmake
Makefile
CTestTestfile.cmake
daemon-test

View File

@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.7.0)
project(daemon-test CXX)
set(CMAKE_CXX_STANDARD 17)
set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/")
set(CMAKE_CXX_FLAGS "-g -O3 -DBOOST_LOG_DYN_LINK -DBOOST_LOG_USE_NATIVE_SYSLOG -Wall")
find_package(Boost COMPONENTS unit_test_framework filesystem system thread REQUIRED)
include_directories(aes67-daemon ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR})
add_executable(daemon-test daemon_test.cpp)
target_link_libraries(daemon-test ${Boost_LIBRARIES})
add_test(daemon-test daemon-test)

20
daemon/tests/daemon.conf Normal file
View File

@ -0,0 +1,20 @@
{
"http_port": 9999,
"http_base_dir": ".",
"log_severity": 5,
"playout_delay": 0,
"tic_frame_size_at_1fs": 192,
"max_tic_frame_size": 1024,
"sample_rate": 44100,
"rtp_mcast_base": "239.2.0.1",
"rtp_port": 6004,
"ptp_domain": 0,
"ptp_dscp": 46,
"sap_interval": 1,
"syslog_proto": "none",
"syslog_server": "255.255.255.254:1234",
"status_file": "",
"interface_name": "lo",
"mac_addr": "01:00:5e:01:00:01",
"ip_addr": "127.0.0.1"
}

View File

@ -0,0 +1,620 @@
//
// daemon_test.cpp
//
// Copyright (c) 2019 Andrea Bondavalli. All rights reserved.
//
// 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/>.
//
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH 4096 //max for SDP file
#include <httplib.h>
#include <boost/foreach.hpp>
#include <boost/asio.hpp>
#include <boost/process.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <set>
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE DaemonTest
#include <boost/test/unit_test.hpp>
#define _MEMORY_CHECK_
constexpr static const char g_daemon_address[] = "127.0.0.1";
constexpr static uint16_t g_daemon_port = 9999;
constexpr static const char g_sap_address[] = "224.2.127.254";
constexpr static uint16_t g_sap_port = 9875;
constexpr static uint16_t udp_size = 1024;
constexpr static uint16_t sap_header = 24;
using namespace boost::process;
using namespace boost::asio::ip;
using namespace boost::asio;
struct DaemonInstance {
DaemonInstance() {
BOOST_TEST_MESSAGE("Starting up test daemon instance ...");
std::this_thread::sleep_for(std::chrono::seconds(1));
while (daemon_.running()) {
BOOST_TEST_MESSAGE("Checking daemon instance ...");
httplib::Client cli(g_daemon_address, g_daemon_port);
auto res = cli.Get("/");
if (res) {
break;
}
daemon_.wait_for(std::chrono::seconds(1));
}
BOOST_REQUIRE(daemon_.running());
ok = true;
}
~DaemonInstance() {
BOOST_TEST_MESSAGE("Tearing down test daemon instance...");
auto pid = daemon_.native_handle();
/* trigger normal daemon termination */
kill(pid, SIGTERM);
daemon_.wait();
BOOST_REQUIRE_MESSAGE(!daemon_.exit_code(), "daemon exited normally");
ok = false;
}
static bool is_ok() { return ok; }
private:
child daemon_{
#if defined _MEMORY_CHECK_
search_path("valgrind"),
#endif
"../aes67-daemon",
"-c",
"daemon.conf",
"-p",
"9999",
"-i",
"lo"};
inline static bool ok{false};
};
BOOST_TEST_GLOBAL_FIXTURE(DaemonInstance);
struct Client {
Client() {
socket_.open(listen_endpoint_.protocol());
socket_.set_option(udp::socket::reuse_address(true));
socket_.bind(listen_endpoint_);
socket_.set_option(
multicast::join_group(address::from_string(g_sap_address).to_v4(),
address::from_string(g_daemon_address).to_v4()));
}
bool is_alive() {
auto res = cli_.Get("/");
return (res->status == 200);
}
std::pair<bool, std::string> get_config() {
auto res = cli_.Get("/api/config");
return std::make_pair(res->status == 200, res->body);
}
bool set_ptp_config(int domain, int dscp) {
std::ostringstream os;
os << "{ \"domain\": " << domain << ", \"dscp\": " << dscp << " }";
auto res = cli_.Post("/api/ptp/config", os.str(), "application/json");
return (res->status == 200);
}
std::pair<bool, std::string> get_ptp_status() {
auto res = cli_.Get("/api/ptp/status");
return std::make_pair(res->status == 200, res->body);
}
std::pair<bool, std::string> get_ptp_config() {
auto res = cli_.Get("/api/ptp/config");
return std::make_pair(res->status == 200, res->body);
}
bool add_source(int id) {
std::string json = R"(
{
"enabled": true,
"name": "ALSA",
"io": "Audio Device",
"map": [ 0, 1, 2, 3, 4, 5, 6, 7 ],
"max_samples_per_packet": 48,
"codec": "L16",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": false
}
)";
std::string url = std::string("/api/source/") + std::to_string(id);
auto res = cli_.Put(url.c_str(), json, "application/json");
return (res->status == 200);
}
std::pair<bool, std::string> get_source_sdp(int id) {
std::string url = std::string("/api/source/sdp/") + std::to_string(id);
auto res = cli_.Get(url.c_str());
return std::make_pair(res->status == 200, res->body);
}
std::pair<bool, std::string> get_sink_status(int id) {
std::string url = std::string("/api/sink/status/") + std::to_string(id);
auto res = cli_.Get(url.c_str());
return std::make_pair(res->status == 200, res->body);
}
std::pair<bool, std::string> get_streams() {
std::string url = std::string("/api/streams");
auto res = cli_.Get(url.c_str());
return std::make_pair(res->status == 200, res->body);
}
std::pair<bool, std::string> get_sources() {
std::string url = std::string("/api/sources");
auto res = cli_.Get(url.c_str());
return std::make_pair(res->status == 200, res->body);
}
std::pair<bool, std::string> get_sinks() {
std::string url = std::string("/api/sinks");
auto res = cli_.Get(url.c_str());
return std::make_pair(res->status == 200, res->body);
}
bool remove_source(int id) {
std::string url = std::string("/api/source/") + std::to_string(id);
auto res = cli_.Delete(url.c_str());
return (res->status == 200);
}
bool add_sink_sdp(int id) {
std::string json = R"(
{
"name": "ALSA",
"io": "Audio Device",
"source": "",
"use_sdp": true,
"sdp": "v=0\no=- 1 0 IN IP4 10.0.0.12\ns=ALSA (on ubuntu)_1\nc=IN IP4 239.2.0.12/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 6004 RTP/AVP 98\nc=IN IP4 239.2.0.12/15\na=rtpmap:98 L16/44100/8\na=sync-time:0\na=framecount:64-192\na=ptime:1.088435374150\na=maxptime:1.088435374150\na=mediaclk:direct=0\na=ts-refclk:ptp=IEEE1588-2008:00-0C-29-FF-FE-0E-90-C8:0\na=recvonly",
"delay": 1024,
"ignore_refclk_gmid": true,
"map": [ 0, 1, 2, 3, 4, 5, 6, 7 ]
}
)";
std::string url = std::string("/api/sink/") + std::to_string(id);
auto res = cli_.Put(url.c_str(), json, "application/json");
return (res->status == 200);
}
bool add_sink_url(int id) {
std::string json1 = R"(
{
"name": "ALSA",
"io": "Audio Device",
"use_sdp": false,
"sdp": "",
"delay": 1024,
"ignore_refclk_gmid": true,
"map": [ 0, 1, 2, 3, 4, 5, 6, 7 ],
)";
std::string json = json1 +
std::string("\"source\": \"http://") +
g_daemon_address + ":" + std::to_string(g_daemon_port) +
std::string("/api/source/sdp/") + std::to_string(id) + "\"\n}";
std::string url = std::string("/api/sink/") + std::to_string(id);
auto res = cli_.Put(url.c_str(), json, "application/json");
return (res->status == 200);
}
bool remove_sink(int id) {
std::string url = std::string("/api/sink/") + std::to_string(id);
auto res = cli_.Delete(url.c_str());
return (res->status == 200);
}
bool sap_wait_announcement(int id, const std::string& sdp, int count = 1) {
char data[udp_size];
while (count-- > 0) {
BOOST_TEST_MESSAGE("waiting announcement for source " +
std::to_string(id));
std::string sap_sdp;
do {
auto len = socket_.receive(boost::asio::buffer(data, udp_size));
if (len <= sap_header) {
continue;
}
sap_sdp.assign(data + sap_header, data + len);
} while(data[0] != 0x20 || sap_sdp != sdp);
BOOST_CHECK_MESSAGE(true, "SAP announcement SDP and source SDP match");
}
return true;
}
void sap_wait_all_deletions() {
char data[udp_size];
std::set<uint8_t> ids;
while (ids.size() < 64) {
auto len = socket_.receive(boost::asio::buffer(data, udp_size));
if (len <= sap_header) {
continue;
}
std::string sap_sdp_(data + sap_header, data + len);
if (data[0] == 0x24 && sap_sdp_.length() > 3) {
//o=- 56 0 IN IP4 127.0.0.1
ids.insert(std::atoi(sap_sdp_.c_str() + 3));
BOOST_TEST_MESSAGE("waiting deletion for " +
std::to_string(64 - ids.size()) + " sources");
}
}
}
bool sap_wait_deletion(int id, const std::string& sdp, int count = 1) {
char data[udp_size];
while (count-- > 0) {
BOOST_TEST_MESSAGE("waiting deletion for source " + std::to_string(id));
std::string sap_sdp;
do {
auto len = socket_.receive(boost::asio::buffer(data, udp_size));
if (len <= sap_header) {
continue;
}
sap_sdp.assign(data + sap_header, data + len);
} while(data[0] != 0x24 || sdp.find(sap_sdp) == std::string::npos);
BOOST_CHECK_MESSAGE(true, "SAP deletion SDP matches");
}
return true;
}
private:
httplib::Client cli_{g_daemon_address, g_daemon_port};
io_service io_service_;
udp::socket socket_{io_service_};
udp::endpoint listen_endpoint_{
udp::endpoint(address::from_string("0.0.0.0"), g_sap_port)};
};
BOOST_AUTO_TEST_CASE(is_alive) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.is_alive(), "server is alive");
}
BOOST_AUTO_TEST_CASE(get_config) {
Client cli;
auto json = cli.get_config();
BOOST_REQUIRE_MESSAGE(json.first, "got config");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
auto http_port = pt.get<int>("http_port");
//auto log_severity = pt.get<int>("log_severity");
auto playout_delay = pt.get<int>("playout_delay");
auto tic_frame_size_at_1fs = pt.get<int>("tic_frame_size_at_1fs");
auto max_tic_frame_size = pt.get<int>("max_tic_frame_size");
auto sample_rate = pt.get<int>("sample_rate");
auto rtp_mcast_base = pt.get<std::string>("rtp_mcast_base");
auto rtp_port = pt.get<int>("rtp_port");
auto ptp_domain = pt.get<int>("ptp_domain");
auto ptp_dscp = pt.get<int>("ptp_dscp");
auto sap_interval = pt.get<int>("sap_interval");
auto syslog_proto = pt.get<std::string>("syslog_proto");
auto syslog_server = pt.get<std::string>("syslog_server");
auto status_file = pt.get<std::string>("status_file");
auto interface_name = pt.get<std::string>("interface_name");
auto mac_addr = pt.get<std::string>("mac_addr");
auto ip_addr = pt.get<std::string>("ip_addr");
BOOST_CHECK_MESSAGE(http_port == 9999, "config as excepcted");
//BOOST_CHECK_MESSAGE(log_severity == 5, "config as excepcted");
BOOST_CHECK_MESSAGE(playout_delay == 0, "config as excepcted");
BOOST_CHECK_MESSAGE(tic_frame_size_at_1fs == 192, "config as excepcted");
BOOST_CHECK_MESSAGE(max_tic_frame_size == 1024, "config as excepcted");
BOOST_CHECK_MESSAGE(sample_rate == 44100, "config as excepcted");
BOOST_CHECK_MESSAGE(rtp_mcast_base == "239.2.0.1", "config as excepcted");
BOOST_CHECK_MESSAGE(rtp_port == 6004, "config as excepcted");
BOOST_CHECK_MESSAGE(ptp_domain == 0, "config as excepcted");
BOOST_CHECK_MESSAGE(ptp_dscp == 46, "config as excepcted");
BOOST_CHECK_MESSAGE(sap_interval == 1, "config as excepcted");
BOOST_CHECK_MESSAGE(syslog_proto == "none", "config as excepcted");
BOOST_CHECK_MESSAGE(syslog_server == "255.255.255.254:1234", "config as excepcted");
BOOST_CHECK_MESSAGE(status_file == "", "config as excepcted");
BOOST_CHECK_MESSAGE(interface_name == "lo", "config as excepcted");
BOOST_CHECK_MESSAGE(mac_addr == "01:00:5e:01:00:01", "config as excepcted");
BOOST_CHECK_MESSAGE(ip_addr == "127.0.0.1", "config as excepcted");
}
BOOST_AUTO_TEST_CASE(get_ptp_status) {
Client cli;
auto json = cli.get_ptp_status();
BOOST_REQUIRE_MESSAGE(json.first, "got ptp status");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
auto status = pt.get<std::string>("status");
auto jitter = pt.get<int>("jitter");
BOOST_REQUIRE_MESSAGE(status == "unlocked" && jitter == 0, "ptp status as excepcted");
}
BOOST_AUTO_TEST_CASE(get_ptp_config) {
Client cli;
auto json = cli.get_ptp_config();
BOOST_REQUIRE_MESSAGE(json.first, "got ptp config");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
auto domain = pt.get<int>("domain");
auto dscp = pt.get<int>("dscp");
BOOST_REQUIRE_MESSAGE(domain == 0 && dscp == 46, "ptp config as excepcted");
}
BOOST_AUTO_TEST_CASE(set_ptp_config) {
Client cli;
auto res = cli.set_ptp_config(1, 48);
BOOST_REQUIRE_MESSAGE(res, "set new ptp config");
auto json = cli.get_ptp_config();
BOOST_REQUIRE_MESSAGE(json.first, "got new ptp config");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
auto domain = pt.get<int>("domain");
auto dscp = pt.get<int>("dscp");
BOOST_REQUIRE_MESSAGE(domain == 1 && dscp == 48, "ptp config as excepcted");
std::ifstream fs("./daemon.conf");
BOOST_REQUIRE_MESSAGE(fs.good(), "config file status as excepcted");
boost::property_tree::read_json(fs, pt);
domain = pt.get<int>("ptp_domain");
dscp = pt.get<int>("ptp_dscp");
BOOST_REQUIRE_MESSAGE(domain == 1 && dscp == 48, "ptp config file as excepcted");
res = cli.set_ptp_config(0, 46);
BOOST_REQUIRE_MESSAGE(res, "set default ptp config");
}
BOOST_AUTO_TEST_CASE(add_invalid_source) {
Client cli;
BOOST_REQUIRE_MESSAGE(!cli.add_source(64), "not added source 64");
BOOST_REQUIRE_MESSAGE(!cli.add_source(-1), "not added source -1");
}
BOOST_AUTO_TEST_CASE(remove_invalid_source) {
Client cli;
BOOST_REQUIRE_MESSAGE(!cli.remove_source(64), "not removed source 64");
BOOST_REQUIRE_MESSAGE(!cli.remove_source(-1), "not removed source -1");
}
BOOST_AUTO_TEST_CASE(add_remove_source) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.add_source(0), "added source 0");
BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
}
BOOST_AUTO_TEST_CASE(add_update_remove_source) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.add_source(0), "added source 0");
BOOST_REQUIRE_MESSAGE(cli.add_source(0), "updated source 0");
BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
}
BOOST_AUTO_TEST_CASE(add_remove_sink) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(0), "added sink 0");
BOOST_REQUIRE_MESSAGE(cli.remove_sink(0), "removed sink 0");
}
BOOST_AUTO_TEST_CASE(add_update_remove_sink) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(0), "added sink 0");
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(0), "updated sink 0");
BOOST_REQUIRE_MESSAGE(cli.remove_sink(0), "removed sink 0");
}
BOOST_AUTO_TEST_CASE(source_check_sap) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.add_source(0), "added source 0");
auto sdp = cli.get_source_sdp(0);
BOOST_REQUIRE_MESSAGE(sdp.first, "got source sdp 0");
cli.sap_wait_announcement(0, sdp.second);
BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
cli.sap_wait_deletion(0, sdp.second, 3);
std::this_thread::sleep_for(std::chrono::seconds(10));
}
BOOST_AUTO_TEST_CASE(sink_check_status) {
Client cli;
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(0), "added sink 0");
auto json = cli.get_sink_status(0);
BOOST_REQUIRE_MESSAGE(json.first, "got sink status 0");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
auto is_sink_muted = pt.get<bool>("sink_flags.muted");
auto is_sink_some_muted = pt.get<bool>("sink_flags.some_muted");
BOOST_REQUIRE_MESSAGE(is_sink_muted, "sink is not receiving packets");
BOOST_REQUIRE_MESSAGE(!is_sink_some_muted, "sink is not receiving packets");
BOOST_REQUIRE_MESSAGE(cli.remove_sink(0), "removed sink 0");
}
BOOST_AUTO_TEST_CASE(add_remove_all_sources) {
Client cli;
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
std::string("added source ") + std::to_string(id));
}
auto json = cli.get_sources();
BOOST_REQUIRE_MESSAGE(json.first, "got sources");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
uint8_t id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned source " + std::to_string(id));
++id;
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_source(id),
std::string("removed source ") + std::to_string(id));
}
}
BOOST_AUTO_TEST_CASE(add_remove_all_sinks) {
Client cli;
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id),
std::string("added sink ") + std::to_string(id));
}
auto json = cli.get_sinks();
BOOST_REQUIRE_MESSAGE(json.first, "got sinks");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
uint8_t id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sinks")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned sink " + std::to_string(id));
++id;
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_sink(id),
std::string("removed sink ") + std::to_string(id));
}
}
BOOST_AUTO_TEST_CASE(add_remove_check_all) {
Client cli;
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
std::string("added source ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id),
std::string("added sink ") + std::to_string(id));
}
auto json = cli.get_streams();
BOOST_REQUIRE_MESSAGE(json.first, "got streams");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
uint8_t id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned source " + std::to_string(id));
++id;
}
id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sinks")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned sink " + std::to_string(id));
++id;
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_source(id),
std::string("removed source ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_sink(id),
std::string("removed sink ") + std::to_string(id));
}
}
BOOST_AUTO_TEST_CASE(add_remove_update_check_all) {
Client cli;
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
std::string("added source ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
std::string("updated source ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id),
std::string("added sink ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_sink_url(id),
std::string("updated sink ") + std::to_string(id));
}
auto json = cli.get_streams();
BOOST_REQUIRE_MESSAGE(json.first, "got streams");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
uint8_t id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned source " + std::to_string(id));
++id;
}
id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sinks")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned sink " + std::to_string(id));
++id;
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_source(id),
std::string("removed source ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_sink(id),
std::string("removed sink ") + std::to_string(id));
}
}
BOOST_AUTO_TEST_CASE(add_remove_check_sap_all) {
Client cli;
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
std::string("added source ") + std::to_string(id));
}
for (int id = 0; id < 64; id++) {
auto sdp = cli.get_source_sdp(id);
BOOST_REQUIRE_MESSAGE(sdp.first, std::string("got source sdp ") + std::to_string(id));
cli.sap_wait_announcement(id, sdp.second);
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id),
std::string("added sink ") + std::to_string(id));
}
auto json = cli.get_streams();
BOOST_REQUIRE_MESSAGE(json.first, "got streams");
boost::property_tree::ptree pt;
std::stringstream ss(json.second);
boost::property_tree::read_json(ss, pt);
uint8_t id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned source " + std::to_string(id));
++id;
}
id = 0;
BOOST_FOREACH (auto const& v, pt.get_child("sinks")) {
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
"returned sink " + std::to_string(id));
++id;
}
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_source(id),
std::string("removed source ") + std::to_string(id));
}
cli.sap_wait_all_deletions();
for (int id = 0; id < 64; id++) {
BOOST_REQUIRE_MESSAGE(cli.remove_sink(id),
std::string("removed sink ") + std::to_string(id));
}
}

2
demo/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
sink_test.wav

20
demo/daemon.conf Normal file
View File

@ -0,0 +1,20 @@
{
"http_port": 8080,
"http_base_dir": "./webui/build",
"log_severity": 3,
"playout_delay": 0,
"tic_frame_size_at_1fs": 192,
"max_tic_frame_size": 1024,
"sample_rate": 44100,
"rtp_mcast_base": "239.1.0.1",
"rtp_port": 5004,
"ptp_domain": 0,
"ptp_dscp": 48,
"sap_interval": 30,
"syslog_proto": "none",
"syslog_server": "255.255.255.254:1234",
"status_file": "./demo/status.json",
"interface_name": "lo",
"mac_addr": "01:00:5e:01:00:01",
"ip_addr": "127.0.0.1"
}

28
demo/status.json Normal file
View File

@ -0,0 +1,28 @@
{
"sources": [
{
"id": 0,
"enabled": true,
"name": "ALSA Source 0",
"io": "Audio Device",
"max_samples_per_packet": 48,
"codec": "L16",
"ttl": 15,
"payload_type": 98,
"dscp": 34,
"refclk_ptp_traceable": false,
"map": [ 0, 1 ]
} ],
"sinks": [
{
"id": 0,
"name": "ALSA Sink 0",
"io": "Audio Device",
"use_sdp": true,
"source": "http://127.0.0.1:8080/api/source/sdp/0",
"sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=IEEE1588-2008:00-00-00-FF-FE-00-00-00:0\na=recvonly\n",
"delay": 576,
"ignore_refclk_gmid": true,
"map": [ 0, 1 ]
} ]
}

89
run_demo.sh Executable file
View File

@ -0,0 +1,89 @@
#!/bin/bash
#
# Tested on Ubuntu 18.04
#
function cleanup {
#kill and wait for previous daemon instances to exit
sudo killall -q ptp4l
killall -q aes67-daemon
while killall -0 aes67-daemon 2>/dev/null ; do
sleep 1
done
}
if ! [ -x "$(command -v ptp4l)" ]; then
echo 'Error: ptp4l is not installed.' >&2
exit 1
fi
if ! [ -x "$(command -v arecord)" ]; then
echo 'Error: arecord is not installed.' >&2
exit 1
fi
if ! [ -x "$(command -v speaker-test)" ]; then
echo 'Error: speaker-test is not installed.' >&2
exit 1
fi
if ! [ -x "$(command -v ./daemon/aes67-daemon)" ]; then
echo 'Error: aes67-daemon is not compiled.' >&2
exit 1
fi
if ! [ -r "3rdparty/ravenna-alsa-lkm/driver/MergingRavennaALSA.ko" ]; then
echo 'Error: MergingRavennaALSA.ko module is not compiled.' >&2
exit 1
fi
trap cleanup EXIT
#configure system parms
sudo sysctl -w net/ipv4/igmp_max_memberships=64
#stop pulseaudio, this seems to open/close ALSA continuosly
systemctl --user stop pulseaudio.socket > /dev/null 2>&1
systemctl --user stop pulseaudio.sservice > /dev/null 2>&1
#install kernel module
sudo insmod 3rdparty/ravenna-alsa-lkm/driver/MergingRavennaALSA.ko
cleanup
if [ -f ./demo/sink_test.wav ] ; then
rm -f sink_test.wav
fi
echo "Starting PTP master ..."
sudo ptp4l -i lo -l7 -E -S &
#echo "Starting AES67 daemon ..."
./daemon/aes67-daemon -c ./demo/daemon.conf &
#open browser on configuration page
if [ -x "$(command -v xdg-open)" ]; then
xdg-open "http://127.0.0.1:8080/PTP"
fi
echo "Waiting for PTP slave to sync ..."
sleep 30
#starting recording on sink
echo "Starting to record 60 secs from sink ..."
arecord -D hw:1,0 -f cd -d 60 -r 44100 -c 2 -t wav ./demo/sink_test.wav > /dev/null 2>&1 &
sleep 10
#starting playback on source
echo "Starting to playback test on source ..."
speaker-test -D hw:1,0 -r 44100 -c 2 -t sine > /dev/null 2>&1 &
while killall -0 arecord 2>/dev/null ; do
sleep 1
done
killall speaker-test
if [ -f ./demo/sink_test.wav ] ; then
echo "Recording to file \"demo/sink_test.wav\" successfull"
else
echo "Recording failed"
fi
echo "Terminating processes ..."

16
ubuntu-packages.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
#
# Tested on Ubuntu 18.04
#
sudo apt update
sudo apt-get install -y build-essential
sudo apt-get install -y git
sudo apt-get install -y cmake
sudo apt-get install -y npm
sudo apt-get install -y libboost-all-dev
sudo apt-get install -y valgrind
sudo apt-get install -y linux-sound-base alsa-base alsa-utils
sudo apt-get install -y linuxptp
sudo apt install -y linux-headers-$(uname -r)

19
webui/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json

674
webui/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:
{{ project }} Copyright (C) {{ year }} {{ organization }}
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>.

1623
webui/README.md Normal file

File diff suppressed because it is too large Load Diff

20
webui/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "aes67-daemon-webui",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-modal": "^3.11.1",
"react-router-dom": "^5.1.2",
"react-scripts": "0.9.5",
"react-toastify": "^5.5.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

BIN
webui/public/edit.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

31
webui/public/index.html Normal file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>

BIN
webui/public/loader.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
webui/public/plus.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
webui/public/reload.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
webui/public/trash.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

221
webui/src/Config.js Normal file
View File

@ -0,0 +1,221 @@
//
// Config.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import { toast } from 'react-toastify';
import RestAPI from './Services';
import Loader from './Loader';
require('./styles.css');
class Config extends Component {
constructor(props) {
super(props);
this.state = {
httpPort: '',
logSeverity: '',
playoutDelay: '',
playoutDelayErr: false,
ticFrameSizeAt1fs: '',
ticFrameSizeAt1fsErr: false,
maxTicFrameSize: '',
maxTicFrameSizeErr: false,
sampleRate: '',
rtpMcastBase: '',
rtpMcastBaseErr: false,
rtpPort: '',
rtpPortErr: false,
ptpDomain: '',
ptpDscp: '',
sapInterval: '',
sapIntervalErr: false,
syslogProto: '',
syslogServer: '',
syslogServerErr: false,
statusFile: '',
interfaceName: '',
macAddr: '',
ipAddr: '',
errors: 0,
isConfigLoading: false
};
this.onSubmit = this.onSubmit.bind(this);
this.inputIsValid = this.inputIsValid.bind(this);
}
componentDidMount() {
this.setState({isConfigLoading: true});
RestAPI.getConfig()
.then(response => response.json())
.then(
data => this.setState(
{
httpPort: data.http_port,
logSeverity: data.log_severity,
playoutDelay: data.playout_delay,
ticFrameSizeAt1fs: data.tic_frame_size_at_1fs,
maxTicFrameSize: data.max_tic_frame_size,
sampleRate: data.sample_rate,
rtpMcastBase: data.rtp_mcast_base,
rtpPort: data.rtp_port,
ptpDomain: data.ptp_domain,
ptpDscp: data.ptp_dscp,
sapInterval: data.sap_interval,
syslogProto: data.syslog_proto,
syslogServer: data.syslog_server,
statusFile: data.status_file,
interfaceName: data.interface_name,
macAddr: data.mac_addr,
ipAddr: data.ip_addr,
isConfigLoading: false
}))
.catch(err => this.setState({isConfigLoading: false}));
}
inputIsValid() {
return !this.state.playoutDelayErr &&
!this.state.ticFrameSizeAt1fsErr &&
!this.state.maxTicFrameSizeErr &&
!this.state.rtpMcastBaseErr &&
!this.state.rtpPortErr &&
!this.state.sapIntervalErr &&
!this.state.syslogServerErr &&
!this.state.isConfigLoading;
}
onSubmit(event) {
event.preventDefault();
RestAPI.setConfig(
this.state.logSeverity,
this.state.syslogProto,
this.state.syslogServer,
this.state.rtpMcastBase,
this.state.rtpPort,
this.state.playoutDelay,
this.state.ticFrameSizeAt1fs,
this.state.sampleRate,
this.state.maxTicFrameSize,
this.state.sapInterval)
.then(response => toast.success('Config updated, daemon restart ...'));
}
render() {
return (
<div className='config'>
{this.state.isConfigLoading ? <Loader/> : <h3>Audio Config</h3>}
<table><tbody>
<tr>
<th align="left"> <label>Safety Playout delay (samples) </label> </th>
<th align="left"> <input type='number' min='0' max='4000' className='input-number' value={this.state.playoutDelay} onChange={e => this.setState({playoutDelay: e.target.value, playoutDelayErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>TIC Frame size at @1FS </label> </th>
<th align="left"> <input type='number' min='192' max='8192' className='input-number' value={this.state.ticFrameSizeAt1fs} onChange={e => this.setState({ticFrameSizeAt1fs: e.target.value, ticFrameSizeAt1fsErr: !e.currentTarget.checkValidity()})} disabled required/> </th>
</tr>
<tr>
<th align="left"> <label>Max TIC frame size </label> </th>
<th align="left"> <input type='number' min='192' max='8192' className='input-number' value={this.state.maxTicFrameSize} onChange={e => this.setState({maxTicFrameSize: e.target.value, maxTicFrameSizeErr: !e.currentTarget.checkValidity()})} disabled required/> </th>
</tr>
<tr>
<th align="left"> <label>Default Sample rate</label> </th>
<th align="left">
<select value={this.state.sampleRate} onChange={e => this.setState({sampleRate: e.target.value})}>
<option value="44100">44.1 kHz</option>
<option value="48000">48 kHz</option>
<option value="96000">96 kHz</option>
</select>
</th>
</tr>
</tbody></table>
<br/>
{this.state.isConfigLoading ? <Loader/> : <h3>Network Config</h3>}
<table><tbody>
<tr>
<th align="left"> <label>HTTP port</label> </th>
<th align="left"> <input type='number' min='1024' max='65536' className='input-number' value={this.state.httpPort} disabled/> </th>
</tr>
<tr>
<th align="left"> <label>RTP base address</label> </th>
<th align="left"> <input type="text" minLength="7" maxLength="15" size="15" pattern="^((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$" value={this.state.rtpMcastBase} onChange={e => this.setState({rtpMcastBase: e.target.value, rtpMcastBaseErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>RTP port</label> </th>
<th align="left"> <input type='number' min='1024' max='65536' className='input-number' value={this.state.rtpPort} onChange={e => this.setState({rtpPort: e.target.value, rtpPortErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>SAP interval (ms)</label> </th>
<th align="left"> <input type='number' min='0' max='255' className='input-number' value={this.state.sapInterval} onChange={e => this.setState({sapInterval: e.target.value, sapIntervalErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>Network Interface</label> </th>
<th align="left"> <input value={this.state.interfaceName} disabled/> </th>
</tr>
<tr>
<th align="left"> <label>MAC address</label> </th>
<th align="left"> <input value={this.state.macAddr} disabled/> </th>
</tr>
<tr>
<th align="left"> <label>IP address</label> </th>
<th align="left"> <input value={this.state.ipAddr} disabled/> </th>
</tr>
</tbody></table>
<br/>
{this.state.isConfigLoading ? <Loader/> : <h3>Logging Config</h3>}
<table><tbody>
<tr>
<th align="left"> <label>Syslog protocol</label> </th>
<th align="left">
<select value={this.state.syslogProto} onChange={e => this.setState({syslogProto: e.target.value})}>
<option value="none">none</option>
<option value="">local</option>
<option value="udp">UDP server</option>
</select>
</th>
</tr>
<tr>
<th align="left"> <label>Syslog server</label> </th>
<th align="left"> <input value={this.state.syslogServer} onChange={e => this.setState({syslogServer: e.target.value, syslogServerErr: !e.currentTarget.checkValidity()})} disabled={this.state.syslogProto === 'udp' ? undefined : true} /> </th>
</tr>
<tr>
<th align="left"> <label>Log severity</label> </th>
<th align="left">
<select value={this.state.logSeverity} onChange={e => this.setState({logSeverity: e.target.value})}>
<option value="1">debug</option>
<option value="2">info</option>
<option value="3">warning</option>
<option value="4">error</option>
<option value="5">fatal</option>
</select>
</th>
</tr>
</tbody></table>
<br/>
<table><tbody>
<tr>
<th> <button disabled={this.inputIsValid() ? undefined : true} onClick={this.onSubmit}>Submit</button> </th>
</tr>
</tbody></table>
</div>
)
}
}
export default Config;

60
webui/src/ConfigTabs.js Normal file
View File

@ -0,0 +1,60 @@
//
// ConfigTabs.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Tabs from './Tabs';
import PTP from './PTP';
import Config from './Config';
import Sources from './Sources';
import Sinks from './Sinks';
require('./styles.css');
class ConfigTabs extends Component {
static propTypes = {
currentTab: PropTypes.func.isRequired
};
render() {
return (
<div>
<h1>AES67 Daemon</h1>
<Tabs currentTab={this.props.currentTab}>
<div label="Config">
<Config/>
</div>
<div label="PTP">
<PTP/>
</div>
<div label="Sources">
<Sources/>
</div>
<div label="Sinks">
<Sinks/>
</div>
</Tabs>
</div>
);
}
}
export default ConfigTabs;

31
webui/src/Loader.js Normal file
View File

@ -0,0 +1,31 @@
//
// Loader.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React from 'react';
function Loader() {
return (
<div className='loader'>
<img src='/loader.gif' alt='loading'/>
</div>
);
}
export default Loader;

138
webui/src/PTP.js Normal file
View File

@ -0,0 +1,138 @@
//
// PTP.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import {toast} from 'react-toastify';
import RestAPI from './Services';
import Loader from './Loader';
require('./styles.css');
class PTP extends Component {
constructor(props) {
super(props);
this.state = {
domain: '',
domainErr: false,
dscp: '',
status: '',
gmid: '',
jitter: '',
isConfigLoading: false,
isStatusLoading: false,
};
this.onSubmit = this.onSubmit.bind(this);
this.inputIsValid = this.inputIsValid.bind(this);
}
fetchStatus() {
this.setState({isStatusLoading: true});
RestAPI.getPTPStatus()
.then(response => response.json())
.then(
data => this.setState({
status: data.status,
gmid: data.gmid,
jitter: data.jitter,
isStatusLoading: false
}))
.catch(err => this.setState({isStatusLoading: false}));
}
componentDidMount() {
this.setState({isConfigLoading: true});
RestAPI.getPTPConfig()
.then(response => response.json())
.then(data =>
this.setState({domain: data.domain, dscp: data.dscp, isConfigLoading: false}))
.catch(err => this.setState({isConfigLoading: false}));
this.fetchStatus();
this.interval = setInterval(() => { this.fetchStatus() }, 10000)
}
componentWillUnmount() {
clearInterval(this.interval);
}
inputIsValid() {
return !this.state.domainErr &&
!this.state.isConfigLoading;
}
onSubmit(event) {
event.preventDefault();
RestAPI.setPTPConfig(this.state.domain, this.state.dscp)
.then(response => toast.success('PTP config updated'));
}
render() {
return (
<div className='ptp'>
{this.state.isConfigLoading ? <Loader/> : <h3>Config</h3>}
<table><tbody>
<tr>
<th align="left"> <label>Type</label> </th>
<th align="left"> <label>PTPv2</label> </th>
</tr>
<tr>
<th align="left"> <label>Domain</label> </th>
<th align="left"> <input type='number' min='0' max='255' className='input-number' value={this.state.domain} onChange={e => this.setState({domain: e.target.value, domainErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>DSCP</label> </th>
<th align="left">
<select value={this.state.dscp} onChange={e => this.setState({dscp: e.target.value})}>
<option value='46'>46 (EF)</option>
<option value='48'>48 (CS6)</option>
</select>
</th>
</tr>
<tr>
<th> <button disabled={this.inputIsValid() ? undefined : true} onClick={this.onSubmit}>Submit</button> </th>
</tr>
</tbody></table>
<br/>
{ this.state.isStatusLoading ? <Loader/> : <h3>Status</h3> }
<table><tbody>
<tr>
<th align="left"> <label>Mode</label> </th>
<th align="left"> <input value='Slave' disabled/> </th>
</tr>
<tr>
<th align="left"> <label>Status</label> </th>
<th align="left"> <input value={this.state.status} disabled/> </th>
</tr>
<tr>
<th align="left"> <label>GMID</label> </th>
<th align="left"> <input value={this.state.gmid} disabled/> </th>
</tr>
<tr>
<th align="left"> <label>Delta</label> </th>
<th align="left"> <input value={this.state.jitter} disabled/> </th>
</tr>
</tbody></table>
</div>
)
}
}
export default PTP;

223
webui/src/Services.js Normal file
View File

@ -0,0 +1,223 @@
//
// Services.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import { toast } from 'react-toastify';
const API = '/api';
const config = '/config';
const streams = '/streams';
const sources = '/sources';
const sinks = '/sinks';
const ptpConfig = '/ptp/config';
const ptpStatus = '/ptp/status';
const source = '/source';
const sdp = '/sdp';
const sink = '/sink';
const status = '/status';
const defaultParams = {
credentials: 'same-origin',
redirect: 'error',
headers: new Headers({
'X-USER-ID': 'test'
})
};
export default class RestAPI {
static getBaseUrl() {
return location.protocol + '//' + location.hostname + ':8080';
}
static doFetch(url, params = {}) {
if (params.method === undefined) {
params.method = 'GET';
}
return fetch(this.getBaseUrl() + API + url, Object.assign({}, defaultParams, params))
.then(
response => {
if (response.ok) {
return response;
}
console.log(this.getBaseUrl() + API + url + ' HTTP ' + response.status);
return Promise.reject(Error('HTTP ' + response.status));
}
).catch(
err => {
console.log(this.getBaseUrl() + API + url + ' failed: ' + err.message);
return Promise.reject(Error(err.message));
}
);
}
static getConfig() {
return this.doFetch(config).catch(err => {
toast.error('Config get failed: ' + err.message);
return Promise.reject(Error(err.message));
});
}
static setConfig(log_severity, syslog_proto, syslog_server, rtp_mcast_base, rtp_port, playout_delay, tic_frame_size_at_1fs, sample_rate, max_tic_frame_size, sap_interval) {
return this.doFetch(config, {
body: JSON.stringify({
log_severity: parseInt(log_severity, 10),
syslog_proto: syslog_proto,
syslog_server: syslog_server,
rtp_mcast_base: rtp_mcast_base,
rtp_port: parseInt(rtp_port, 10),
playout_delay: parseInt(playout_delay, 10),
tic_frame_size_at_1fs: parseInt(tic_frame_size_at_1fs, 10),
sample_rate: parseInt(sample_rate, 10),
max_tic_frame_size: parseInt(max_tic_frame_size, 10),
sap_interval: parseInt(sap_interval, 10)
}),
method: 'POST'
}).catch(err => {
toast.error('Config set failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getPTPConfig() {
return this.doFetch(ptpConfig).catch(err => {
toast.error('PTP config get failed: ' + err.message);
return Promise.reject(Error(err.message));
});
}
static setPTPConfig(domain, dscp) {
return this.doFetch(ptpConfig, {
body: JSON.stringify({
domain: parseInt(domain, 10),
dscp: parseInt(dscp, 10)
}),
method: 'POST'
}).catch(err => {
toast.error('PTP config set failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static addSource(id, enabled, name, io, max_samples_per_packet, codec, ttl, payload_type, dscp, refclk_ptp_traceable, map, is_edit) {
return this.doFetch(source + '/' + id, {
body: JSON.stringify({
enabled: enabled,
name: name,
io: io,
codec: codec,
map: map,
max_samples_per_packet: parseInt(max_samples_per_packet, 10),
ttl: parseInt(ttl, 10),
payload_type: parseInt(payload_type, 10),
dscp: parseInt(dscp, 10),
refclk_ptp_traceable: refclk_ptp_traceable
}),
method: 'PUT'
}).catch(err => {
toast.error((is_edit ? 'Update Source failed: ' : 'Add Source failed: ') + err.message)
return Promise.reject(Error(err.message));
});
}
static removeSource(id) {
return this.doFetch(source + '/' + id, {
method: 'DELETE'
}).catch(err => {
toast.error('Remove Source failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getSourceSDP(id) {
return this.doFetch(source + sdp + '/' + id, {
method: 'GET'
}).catch(err => {
toast.error('Get Source SDP failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getSinkStatus(id) {
return this.doFetch(sink + status + '/' + id, {
method: 'GET'
}).catch(err => {
toast.error('Get Sink status failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static addSink(id, name, io, delay, use_sdp, source, sdp, ignore_refclk_gmid, map, is_edit) {
return this.doFetch(sink + '/' + id, {
body: JSON.stringify({
name: name,
io: io,
delay: parseInt(delay, 10),
use_sdp: use_sdp,
source: source,
sdp: sdp,
ignore_refclk_gmid: ignore_refclk_gmid,
map: map
}),
method: 'PUT'
}).catch(err => {
toast.error((is_edit ? 'Update Sink failed: ' : 'Add Sink failed: ') + err.message)
return Promise.reject(Error(err.message));
});
}
static removeSink(id) {
return this.doFetch(sink + '/' + id, {
method: 'DELETE'
}).catch(err => {
toast.error('Remove Sink failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getPTPStatus() {
return this.doFetch(ptpStatus).catch(err => {
toast.error('PTP status get failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getSources() {
return this.doFetch(sources).catch(err => {
toast.error('Sources get failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getSinks() {
return this.doFetch(sinks).catch(err => {
toast.error('Sinks get failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
static getStreams() {
return this.doFetch(streams).catch(err => {
toast.error('Streams get failed: ' + err.message)
return Promise.reject(Error(err.message));
});
}
}

214
webui/src/SinkEdit.js Normal file
View File

@ -0,0 +1,214 @@
//
// SinkEdit.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {toast} from 'react-toastify';
import Modal from 'react-modal';
import RestAPI from './Services';
require('./styles.css');
const editCustomStyles = {
content : {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)'
}
};
class SinkEdit extends Component {
static propTypes = {
sink: PropTypes.object.isRequired,
applyEdit: PropTypes.func.isRequired,
closeEdit: PropTypes.func.isRequired,
editIsOpen: PropTypes.bool.isRequired,
isEdit: PropTypes.bool.isRequired,
editTitle: PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.state = {
id: this.props.sink.id,
name: this.props.sink.name,
nameErr: false,
io: this.props.sink.io,
delay: this.props.sink.delay,
ignoreRefclkGmid: this.props.sink.ignore_refclk_gmid,
useSdp: this.props.sink.use_sdp,
source: this.props.sink.source,
sourceErr: false,
sdp: this.props.sink.sdp,
channels: this.props.sink.map.length,
map: this.props.sink.map,
audioMap: []
}
let v;
for (v = 0; v <= (64 - this.state.channels); v += 1) {
this.state.audioMap.push(v);
}
this.onSubmit = this.onSubmit.bind(this);
this.onCancel = this.onCancel.bind(this);
this.addSink = this.addSink.bind(this);
this.onChangeChannels = this.onChangeChannels.bind(this);
this.onChangeChannelsMap = this.onChangeChannelsMap.bind(this);
this.inputIsValid = this.inputIsValid.bind(this);
}
componentDidMount() {
Modal.setAppElement('body');
}
addSink(message) {
RestAPI.addSink(
this.state.id,
this.state.name,
this.state.io,
this.state.delay,
this.state.useSdp,
this.state.source ? this.state.source : '',
this.state.sdp ? this.state.sdp : '',
this.state.ignoreRefclkGmid,
this.state.map,
this.props.isEdit)
.then(function(response) {
this.props.applyEdit();
toast.success(message);
}.bind(this));
}
onSubmit() {
this.addSink('Sink ' + this.state.id + (this.props.isEdit ? ' updated ' : ' added'));
}
onCancel() {
this.props.closeEdit();
}
onChangeChannels(e) {
if (e.currentTarget.checkValidity()) {
let channels = parseInt(e.target.value, 10);
let audioMap = [], map = [], v;
for (v = 0; v <= (64 - channels); v += 1) {
audioMap.push(v);
}
for (v = 0; v < channels; v++) {
map.push(v + this.state.map[0]);
}
this.setState({ map: map, channels: channels, audioMap: audioMap });
}
}
onChangeChannelsMap(e) {
let startChannel = parseInt(e.target.value, 10);
let map = [], v;
for (v = 0; v < this.state.channels; v++) {
map.push(v + startChannel);
}
this.setState({ map: map });
}
inputIsValid() {
return !this.state.nameErr &&
!this.state.sourceErr &&
(this.state.useSdp || this.state.source) &&
(!this.state.useSdp || this.state.sdp);
}
render() {
return (
<div id='sink-edit'>
<Modal ariaHideApp={false}
isOpen={this.props.editIsOpen}
onRequestClose={this.props.closeEdit}
style={editCustomStyles}
contentLabel="Sink Edit">
<h2><center>{this.props.editTitle}</center></h2>
<table><tbody>
<tr>
<th align="left"> <font color='grey'>ID</font> </th>
<th align="left"> <input type='number' min='0' max='63' className='input-number' value={this.state.id} onChange={e => this.setState({id: e.target.value})} disabled required/> </th>
</tr>
<tr>
<th align="left"> <label>Name</label> </th>
<th align="left"> <input value={this.state.name} onChange={e => this.setState({name: e.target.value, nameErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr height="35">
<th align="left"> <label>Use SDP</label> </th>
<th align="left"> <input type="checkbox" defaultChecked={this.state.useSdp} onChange={e => this.setState({useSdp: e.target.checked})} /> </th>
</tr>
<tr>
<th align="left"> <label>Source URL</label> </th>
<th align="left"> <input type='url' size="30" value={this.state.source} onChange={e => this.setState({source: e.target.value, sourceErr: !e.currentTarget.checkValidity()})} disabled={this.state.useSdp ? true : undefined} required/> </th>
</tr>
<tr>
<th align="left"> <font color={this.state.source ? 'grey' : 'black'}>SDP</font> </th>
<th align="left"> <textarea rows='15' cols='55' value={this.state.sdp} onChange={e => this.setState({sdp: e.target.value})} disabled={this.state.useSdp ? undefined : true} required/> </th>
</tr>
<tr>
<th align="left"> <label>Delay (samples)</label> </th>
<th align="left">
<select value={this.state.delay} onChange={e => this.setState({delay: e.target.value})}>
<option value="192">192</option>
<option value="384">384</option>
<option value="576">576</option>
<option value="768">768</option>
<option value="960">960</option>
</select>
</th>
</tr>
<tr height="35">
<th align="left"> <label>Ignore RefClk GMID</label> </th>
<th align="left"> <input type="checkbox" defaultChecked={this.state.ignoreRefclkGmid} onChange={e => this.setState({ignoreRefclkGmid: e.target.checked})} /> </th>
</tr>
<tr>
<th align="left"> <label>Channels</label> </th>
<th align="left"> <input type='number' min='1' max='8' className='input-number' value={this.state.channels} onChange={this.onChangeChannels} required/> </th>
</tr>
<tr>
<th align="left">Audio Channels map</th>
<th align="left">
<select value={this.state.map[0]} onChange={this.onChangeChannelsMap}>
{ this.state.channels > 1 ?
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Input ' + parseInt(v + 1, 10) + ' -> ALSA Input ' + parseInt(v + this.state.channels, 10)}</option>) :
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Input ' + parseInt(v + 1, 10)}</option>)
}
</select>
</th>
</tr>
</tbody></table>
<br/>
<div style={{textAlign: 'center'}}>
<button onClick={this.onSubmit} disabled={this.inputIsValid() ? undefined : true}>Submit</button>
&nbsp;&nbsp;&nbsp;&nbsp;
<button onClick={this.onCancel}>Cancel</button>
</div>
</Modal>
</div>
);
}
}
export default SinkEdit;

97
webui/src/SinkRemove.js Normal file
View File

@ -0,0 +1,97 @@
//
// SinkRemove.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {toast} from 'react-toastify';
import Modal from 'react-modal';
import RestAPI from './Services';
require('./styles.css');
const removeCustomStyles = {
content : {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)'
}
};
class SinkRemove extends Component {
static propTypes = {
sink: PropTypes.object.isRequired,
applyEdit: PropTypes.func.isRequired,
closeEdit: PropTypes.func.isRequired,
removeIsOpen: PropTypes.bool.isRequired
};
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onCancel = this.onCancel.bind(this);
this.removeSink = this.removeSink.bind(this);
}
componentDidMount() {
Modal.setAppElement('body');
}
removeSink(message) {
RestAPI.removeSink(this.props.sink.id).then(function(response) {
toast.success(message);
this.props.applyEdit();
}.bind(this));
}
onSubmit() {
this.removeSink('Sink ' + this.props.sink.id + ' removed');
}
onCancel() {
this.props.closeEdit();
}
render() {
return (
<div id='sink-remove'>
<Modal ariaHideApp={false}
isOpen={this.props.removeIsOpen}
onRequestClose={this.props.closeEdit}
style={removeCustomStyles}
contentLabel="Sink Remove">
<h2><center>{'Remove sink ' + this.props.sink.id}</center></h2>
<h3><center>Do you want to proceed ?</center></h3>
<br/>
<div style={{textAlign: 'center'}}>
<button onClick={this.onSubmit}>Submit</button>
&nbsp;&nbsp;&nbsp;&nbsp;
<button onClick={this.onCancel}>Cancel</button>
</div>
</Modal>
</div>
);
}
}
export default SinkRemove;

268
webui/src/Sinks.js Normal file
View File

@ -0,0 +1,268 @@
//
// Sinks.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import RestAPI from './Services';
import Loader from './Loader';
import SinkEdit from './SinkEdit';
import SinkRemove from './SinkRemove';
require('./styles.css');
class SinkEntry extends Component {
static propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
channels: PropTypes.number.isRequired,
onEditClick: PropTypes.func.isRequired,
onTrashClick: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
min_time: 'n/a',
flags: 'n/a',
errors: 'n/a'
};
}
handleEditClick = () => {
this.props.onEditClick(this.props.id);
};
handleTrashClick = () => {
this.props.onTrashClick(this.props.id);
};
componentDidMount() {
RestAPI.getSinkStatus(this.props.id)
.then(response => response.json())
.then(function(status) {
let errors = '';
if (status.sink_flags.rtp_seq_id_error)
errors += 'SEQID';
if (status.sink_flags.rtp_ssrc_error)
errors += (errors ? ',' : '') + 'SSRC';
if (status.sink_flags.rtp_payload_type_error)
errors += (errors ? ',' : '') + 'payload type';
if (status.sink_flags.rtp_sac_error)
errors += (errors ? ',' : '') + 'SAC';
let flags = '';
if (status.sink_flags.receiving_rtp_packet)
flags += 'receiving';
if (status.sink_flags._some_muted)
flags += (flags ? ',' : '') + 'some muted';
if (status.sink_flags._muted)
flags += (flags ? ',' : '') + 'muted';
this.setState({
min_time: status.sink_min_time + ' ms',
flags: flags ? flags : 'idle',
errors: errors ? errors : 'none'
});
}.bind(this));
}
render() {
return (
<tr className='tr-stream'>
<td> <label>{this.props.id}</label> </td>
<td> <label>{this.props.name}</label> </td>
<td align='center'> <label>{this.props.channels}</label> </td>
<td align='center'> <label>{this.state.flags}</label> </td>
<td align='center'> <label>{this.state.errors}</label> </td>
<td align='center'> <label>{this.state.min_time}</label> </td>
<td> <span className='pointer-area' onClick={this.handleEditClick}> <img width='20' height='20' src='/edit.png' alt=''/> </span> </td>
<td> <span className='pointer-area' onClick={this.handleTrashClick}> <img width='20' height='20' src='/trash.png' alt=''/> </span> </td>
</tr>
);
}
}
class SinkList extends Component {
static propTypes = {
onAddClick: PropTypes.func.isRequired,
onReloadClick: PropTypes.func.isRequired
};
handleAddClick = () => {
this.props.onAddClick();
};
handleReloadClick = () => {
this.props.onReloadClick();
};
render() {
return (
<div id='sinks-table'>
<table className="table-stream"><tbody>
{this.props.sinks.length > 0 ?
<tr className='tr-stream'>
<th>ID</th>
<th>Name</th>
<th>Channels</th>
<th>Status</th>
<th>Errors</th>
<th>Min. arrival time</th>
</tr>
: <tr>
<th>No sinks configured</th>
</tr> }
{this.props.sinks}
</tbody></table>
&nbsp;
<span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span>
&nbsp;&nbsp;
{this.props.sinks.length < 64 ?
<span className='pointer-area' onClick={this.handleAddClick}> <img width='30' height='30' src='/plus.png' alt=''/> </span>
: undefined}
</div>
);
}
}
class Sinks extends Component {
constructor(props) {
super(props);
this.state = {
sinks: [],
sink: {},
isLoading: false,
isEdit: false,
editIsOpen: false,
removeIsOpen: false,
editTitle: ''
};
this.onEditClick = this.onEditClick.bind(this);
this.onTrashClick = this.onTrashClick.bind(this);
this.onAddClick = this.onAddClick.bind(this);
this.onReloadClick = this.onReloadClick.bind(this);
this.openEdit = this.openEdit.bind(this);
this.closeEdit = this.closeEdit.bind(this);
this.applyEdit = this.applyEdit.bind(this);
this.fetchSinks = this.fetchSinks.bind(this);
}
fetchSinks() {
this.setState({isLoading: true});
RestAPI.getSinks()
.then(response => response.json())
.then(
data => this.setState( { sinks: data.sinks, isLoading: false }))
.catch(err => this.setState( { isLoading: false } ));
}
componentDidMount() {
this.fetchSinks();
}
openEdit(title, sink, isEdit) {
this.setState({editIsOpen: true, editTitle: title, sink: sink, isEdit: isEdit});
}
applyEdit() {
this.closeEdit();
this.fetchSinks();
}
onReloadClick() {
this.fetchSinks();
}
closeEdit() {
this.setState({editIsOpen: false});
this.setState({removeIsOpen: false});
}
onEditClick(id) {
const sink = this.state.sinks.find(s => s.id === id);
this.openEdit("Edit Sink " + id, sink, true);
}
onTrashClick(id) {
const sink = this.state.sinks.find(s => s.id === id);
this.setState({removeIsOpen: true, sink: sink});
}
onAddClick() {
let id;
/* find first free id */
for (id = 0; id < 63; id++) {
if (this.state.sinks[id] === undefined ||
this.state.sinks[id].id !== id) {
break;
}
}
const defaultSink = {
'id': id,
'name': 'ALSA Sink ' + id,
'io': 'Audio Device',
'delay': 576,
'use_sdp': false,
'source': RestAPI.getBaseUrl() + '/api/source/sdp/' + id,
'sdp': '',
'ignore_refclk_gmid': true,
'map': [ (id * 2) % 64, (id * 2 + 1) % 64 ]
};
this.openEdit('Add Sink ' + id, defaultSink, false);
}
render() {
this.state.sinks.sort((a, b) => (a.id > b.id) ? 1 : -1);
const sinks = this.state.sinks.map((sink) => (
<SinkEntry key={sink.id}
id={sink.id}
channels={sink.map.length}
name={sink.name}
onEditClick={this.onEditClick}
onTrashClick={this.onTrashClick}
/>
));
return (
<div id='sinks'>
{ this.state.isLoading ? <Loader/>
: <SinkList onAddClick={this.onAddClick}
onReloadClick={this.onReloadClick}
sinks={sinks} /> }
{ this.state.editIsOpen ?
<SinkEdit editIsOpen={this.state.editIsOpen}
closeEdit={this.closeEdit}
applyEdit={this.applyEdit}
editTitle={this.state.editTitle}
isEdit={this.state.isEdit}
sink={this.state.sink} />
: undefined }
{ this.state.removeIsOpen ?
<SinkRemove removeIsOpen={this.state.removeIsOpen}
closeEdit={this.closeEdit}
applyEdit={this.applyEdit}
sink={this.state.sink}
key={this.state.sink.id} />
: undefined }
</div>
);
}
}
export default Sinks;

257
webui/src/SourceEdit.js Normal file
View File

@ -0,0 +1,257 @@
//
// SourceEdit.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {toast} from 'react-toastify';
import Modal from 'react-modal';
import RestAPI from './Services';
require('./styles.css');
const editCustomStyles = {
content : {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)'
}
};
const max_packet_size = 1440; //bytes
class SourceEdit extends Component {
static propTypes = {
source: PropTypes.object.isRequired,
applyEdit: PropTypes.func.isRequired,
closeEdit: PropTypes.func.isRequired,
editIsOpen: PropTypes.bool.isRequired,
isEdit: PropTypes.bool.isRequired,
editTitle: PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.state = {
id: this.props.source.id,
enabled: this.props.source.enabled,
name: this.props.source.name,
nameErr: false,
io: this.props.source.io,
maxSamplesPerPacket: this.props.source.max_samples_per_packet,
codec: this.props.source.codec,
ttl: this.props.source.ttl,
ttlErr: false,
payloadType: this.props.source.payload_type,
payloadTypeErr: false,
dscp: this.props.source.dscp,
refclkPtpTraceable: this.props.source.refclk_ptp_traceable,
channels: this.props.source.map.length,
channelsErr: false,
maxChannels: Math.floor(max_packet_size / (this.props.source.max_samples_per_packet * (this.props.source.codec === 'L16' ? 2 : 3))),
map: this.props.source.map,
audioMap: []
}
let v;
for (v = 0; v <= (64 - this.state.channels); v += 1) {
this.state.audioMap.push(v);
}
this.onSubmit = this.onSubmit.bind(this);
this.onCancel = this.onCancel.bind(this);
this.addSource = this.addSource.bind(this);
this.onChangeChannels = this.onChangeChannels.bind(this);
this.onChangeChannelsMap = this.onChangeChannelsMap.bind(this);
this.onChangeMaxSamplesPerPacket = this.onChangeMaxSamplesPerPacket.bind(this);
this.onChangeCodec = this.onChangeCodec.bind(this);
this.inputIsValid = this.inputIsValid.bind(this);
}
componentDidMount() {
Modal.setAppElement('body');
}
addSource(message) {
RestAPI.addSource(
this.state.id,
this.state.enabled,
this.state.name,
this.state.io,
this.state.maxSamplesPerPacket,
this.state.codec,
this.state.ttl,
this.state.payloadType,
this.state.dscp,
this.state.refclkPtpTraceable,
this.state.map,
this.props.isEdit)
.then(function(response) {
toast.success(message);
this.props.applyEdit();
}.bind(this));
}
onSubmit() {
this.addSource('Source ' + this.state.id + (this.props.isEdit ? ' updated ' : ' added'));
}
onCancel() {
this.props.closeEdit();
}
onChangeMaxSamplesPerPacket(e) {
let samples = parseInt(e.target.value, 10);
let maxChannels = Math.floor(max_packet_size / (samples * (this.state.codec === 'L16' ? 2 : 3)));
this.setState({ maxSamplesPerPacket: samples, maxChannels: maxChannels, channelsErr: this.state.channels > maxChannels });
}
onChangeCodec(e) {
let codec = e.target.value;
let maxChannels = Math.floor(max_packet_size / (this.state.maxSamplesPerPacket * (codec === 'L16' ? 2 : 3)));
this.setState({ codec: codec, maxChannels: maxChannels, channelsErr: this.state.channels > maxChannels });
}
onChangeChannels(e) {
if (e.currentTarget.checkValidity()) {
let channels = parseInt(e.target.value, 10);
let audioMap = [], map = [], v;
for (v = 0; v <= (64 - channels); v += 1) {
audioMap.push(v);
}
for (v = 0; v < channels; v++) {
map.push(v + this.state.map[0]);
}
this.setState({ map: map, channels: channels, audioMap: audioMap, channelsErr: false });
}
}
onChangeChannelsMap(e) {
let startChannel = parseInt(e.target.value, 10);
let map = [], v;
for (v = 0; v < this.state.channels; v++) {
map.push(v + startChannel);
}
this.setState({ map: map });
}
inputIsValid() {
return !this.state.nameErr &&
!this.state.ttlErr &&
!this.state.channelsErr &&
!this.state.payloadTypeErr;
}
render() {
return (
<div id='source-edit'>
<Modal ariaHideApp={false}
isOpen={this.props.editIsOpen}
onRequestClose={this.props.closeEdit}
style={editCustomStyles}
contentLabel="Source Edit">
<h2><center>{this.props.editTitle}</center></h2>
<table><tbody>
<tr>
<th align="left"> <font color='grey'>ID</font> </th>
<th align="left"> <input type='number' min='0' max='63' className='input-number' value={this.state.id} onChange={e => this.setState({id: e.target.value})} disabled required/> </th>
</tr>
<tr height="35">
<th align="left"> <label>Enabled</label> </th>
<th align="left"> <input type="checkbox" defaultChecked={this.state.enabled} onChange={e => this.setState({enabled: e.target.checked})} /> </th>
</tr>
<tr>
<th align="left"> <label>Name</label> </th>
<th align="left"> <input value={this.state.name} onChange={e => this.setState({name: e.target.value, nameErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>Max samples per packet </label> </th>
<th align="left">
<select value={this.state.maxSamplesPerPacket} onChange={this.onChangeMaxSamplesPerPacket}>
<option value="12">12 - 250us@48Khz</option>
<option value="16">16 - 333us@48Khz</option>
<option value="48">48 - 1ms@48Khz</option>
<option value="96">96 - 2ms@48Khz</option>
<option value="192">192 - 4ms@48Khz</option>
</select>
</th>
</tr>
<tr>
<th align="left"> <label>Codec</label> </th>
<th align="left">
<select value={this.state.codec} onChange={this.onChangeCodec}>
<option value="L16">L16</option>
<option value="L24">L24</option>
</select>
</th>
</tr>
<tr>
<th align="left"> <label>Payload Type</label> </th>
<th align="left"> <input type='number' min='77' max='127' className='input-number' value={this.state.payloadType} onChange={e => this.setState({payloadType: e.target.value, payloadTypeErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>TTL</label> </th>
<th align="left"> <input type='number' min='1' max='255' className='input-number' value={this.state.ttl} onChange={e => this.setState({ttl: e.target.value, ttlErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>DSCP</label> </th>
<th align="left">
<select value={this.state.dscp} onChange={e => this.setState({dscp: e.target.value})}>
<option value="46">46 (EF)</option>
<option value="34">34 (AF41)</option>
<option value="36">26 (AF31)</option>
<option value="48">0 (BE)</option>
</select>
</th>
</tr>
<tr height="35">
<th align="left"> <label>RefClk PTP traceable</label> </th>
<th align="left"> <input type="checkbox" defaultChecked={this.state.refclkPtpTraceable} onChange={e => this.setState({refclkPtpTraceable: e.target.checked})} /> </th>
</tr>
<tr>
<th align="left"> <label>Channels</label> </th>
<th align="left"> <input type='number' min='1' max={this.state.maxChannels} className='input-number' value={this.state.channels} onChange={this.onChangeChannels} required/> </th>
</tr>
<tr>
<th align="left">Audio Channels map</th>
<th align="left">
<select value={this.state.map[0]} onChange={this.onChangeChannelsMap}>
{ this.state.channels > 1 ?
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Output ' + parseInt(v + 1, 10) + ' -> ALSA Output ' + parseInt(v + this.state.channels, 10)}</option>) :
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Output ' + parseInt(v + 1, 10)}</option>)
}
</select>
</th>
</tr>
</tbody></table>
<br/>
<div style={{textAlign: 'center'}}>
<button onClick={this.onSubmit} disabled={this.inputIsValid() ? undefined : true}>Submit</button>
&nbsp;&nbsp;&nbsp;&nbsp;
<button onClick={this.onCancel}>Cancel</button>
</div>
</Modal>
</div>
);
}
}
export default SourceEdit;

97
webui/src/SourceRemove.js Normal file
View File

@ -0,0 +1,97 @@
//
// SourceRemove.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {toast} from 'react-toastify';
import Modal from 'react-modal';
import RestAPI from './Services';
require('./styles.css');
const removeCustomStyles = {
content : {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)'
}
};
class SourceRemove extends Component {
static propTypes = {
source: PropTypes.object.isRequired,
applyEdit: PropTypes.func.isRequired,
closeEdit: PropTypes.func.isRequired,
removeIsOpen: PropTypes.bool.isRequired
};
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onCancel = this.onCancel.bind(this);
this.removeSource = this.removeSource.bind(this);
}
componentDidMount() {
Modal.setAppElement('body');
}
removeSource(message) {
RestAPI.removeSource(this.props.source.id).then(function(response) {
toast.success(message);
this.props.applyEdit();
}.bind(this));
}
onSubmit() {
this.removeSource('Source ' + this.props.source.id + ' removed');
}
onCancel() {
this.props.closeEdit();
}
render() {
return (
<div id='source-remove'>
<Modal ariaHideApp={false}
isOpen={this.props.removeIsOpen}
onRequestClose={this.props.closeEdit}
style={removeCustomStyles}
contentLabel="Source Remove">
<h2><center>{'Remove source ' + this.props.source.id}</center></h2>
<h3><center>Do you want to proceed ?</center></h3>
<br/>
<div style={{textAlign: 'center'}}>
<button onClick={this.onSubmit}>Submit</button>
&nbsp;&nbsp;&nbsp;&nbsp;
<button onClick={this.onCancel}>Cancel</button>
</div>
</Modal>
</div>
);
}
}
export default SourceRemove;

251
webui/src/Sources.js Normal file
View File

@ -0,0 +1,251 @@
//
// Sources.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import RestAPI from './Services';
import Loader from './Loader';
import SourceEdit from './SourceEdit';
import SourceRemove from './SourceRemove';
require('./styles.css');
class SourceEntry extends Component {
static propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
channels: PropTypes.number.isRequired,
onEditClick: PropTypes.func.isRequired,
onTrashClick: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
address: 'n/a',
port: 'n/a'
};
}
handleEditClick = () => {
this.props.onEditClick(this.props.id);
};
handleTrashClick = () => {
this.props.onTrashClick(this.props.id);
};
componentDidMount() {
RestAPI.getSourceSDP(this.props.id)
.then(response => response.text())
.then(function(sdp) {
var address = sdp.match(/(c=IN IP4 )([0-9.]+)/g);
var port = sdp.match(/(m=audio )([0-9]+)/g);
if (address && port) {
this.setState({ address: address[0].substr(9), port: port[0].substr(8) });
}
}.bind(this));
}
render() {
return (
<tr className='tr-stream'>
<td> <label>{this.props.id}</label> </td>
<td> <label>{this.props.name}</label> </td>
<td> <label>{this.state.address}</label> </td>
<td> <label>{this.state.port}</label> </td>
<td align='center'> <label>{this.props.channels}</label> </td>
<td> <span className='pointer-area' onClick={this.handleEditClick}> <img width='20' height='20' src='/edit.png' alt=''/> </span> </td>
<td> <span className='pointer-area' onClick={this.handleTrashClick}> <img width='20' height='20' src='/trash.png' alt=''/> </span> </td>
</tr>
);
}
}
class SourceList extends Component {
static propTypes = {
onAddClick: PropTypes.func.isRequired,
onReloadClick: PropTypes.func.isRequired
};
handleAddClick = () => {
this.props.onAddClick();
};
handleReloadClick = () => {
this.props.onReloadClick();
};
render() {
return (
<div id='sources-table'>
<table className="table-stream"><tbody>
{this.props.sources.length > 0 ?
<tr className='tr-stream'>
<th>ID</th>
<th>Name</th>
<th>Address</th>
<th>Port</th>
<th>Channels</th>
</tr>
: <tr>
<th>No sources configured</th>
</tr> }
{this.props.sources}
</tbody></table>
&nbsp;
<span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span>
&nbsp;&nbsp;
{this.props.sources.length < 64 ?
<span className='pointer-area' onClick={this.handleAddClick}> <img width='30' height='30' src='/plus.png' alt=''/> </span>
: undefined}
</div>
);
}
}
class Sources extends Component {
constructor(props) {
super(props);
this.state = {
sources: [],
source: {},
isLoading: false,
isEdit: false,
editIsOpen: false,
removeIsOpen: false,
editTitle: ''
};
this.onEditClick = this.onEditClick.bind(this);
this.onTrashClick = this.onTrashClick.bind(this);
this.onAddClick = this.onAddClick.bind(this);
this.onReloadClick = this.onReloadClick.bind(this);
this.openEdit = this.openEdit.bind(this);
this.closeEdit = this.closeEdit.bind(this);
this.applyEdit = this.applyEdit.bind(this);
this.fetchSources = this.fetchSources.bind(this);
}
fetchSources() {
this.setState({isLoading: true});
RestAPI.getSources()
.then(response => response.json())
.then(
data => this.setState( { sources: data.sources, isLoading: false }))
.catch(err => this.setState( { isLoading: false } ));
}
componentDidMount() {
this.fetchSources();
}
openEdit(title, source, isEdit) {
this.setState({editIsOpen: true, editTitle: title, source: source, isEdit: isEdit});
}
applyEdit() {
this.closeEdit();
this.fetchSources();
}
closeEdit() {
this.setState({editIsOpen: false});
this.setState({removeIsOpen: false});
}
onEditClick(id) {
const source = this.state.sources.find(s => s.id === id);
this.openEdit("Edit Source " + id, source, true);
}
onTrashClick(id) {
const source = this.state.sources.find(s => s.id === id);
this.setState({removeIsOpen: true, source: source});
}
onReloadClick() {
this.fetchSources();
}
onAddClick() {
let id;
/* find first free id */
for (id = 0; id < 63; id++) {
if (this.state.sources[id] === undefined ||
this.state.sources[id].id !== id) {
break;
}
}
const defaultSource = {
'id': id,
'enabled': true,
'name': 'ALSA Source ' + id,
'io': 'Audio Device',
'max_samples_per_packet': 48,
'codec': 'L16',
'ttl': 15,
'payload_type': 98,
'dscp': 34,
'refclk_ptp_traceable': false,
'map': [ (id * 2) % 64, (id * 2 + 1) % 64 ]
};
this.openEdit('Add Source ' + id, defaultSource, false);
}
render() {
this.state.sources.sort((a, b) => (a.id > b.id) ? 1 : -1);
const sources = this.state.sources.map((source) => (
<SourceEntry key={source.id}
id={source.id}
name={source.name}
channels={source.map.length}
onEditClick={this.onEditClick}
onTrashClick={this.onTrashClick}
/>
));
return (
<div id='sources'>
{ this.state.isLoading ? <Loader/>
: <SourceList onAddClick={this.onAddClick}
onReloadClick={this.onReloadClick}
sources={sources} /> }
{ this.state.editIsOpen ?
<SourceEdit editIsOpen={this.state.editIsOpen}
closeEdit={this.closeEdit}
applyEdit={this.applyEdit}
editTitle={this.state.editTitle}
isEdit={this.state.isEdit}
source={this.state.source} />
: undefined }
{ this.state.removeIsOpen ?
<SourceRemove removeIsOpen={this.state.removeIsOpen}
closeEdit={this.closeEdit}
applyEdit={this.applyEdit}
source={this.state.source}
key={this.state.source.id} />
: undefined }
</div>
);
}
}
export default Sources;

60
webui/src/Tab.js Normal file
View File

@ -0,0 +1,60 @@
//
// Tab.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Tab extends Component {
static propTypes = {
activeTab: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
onClick = () => {
const {
label,
onClick
} = this.props;
onClick(label);
}
render() {
const {
onClick,
props: {
activeTab,
label,
},
} = this;
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
return (
<li className={className} onClick={onClick}>{label}</li>
);
}
}
export default Tab;

72
webui/src/Tabs.js Normal file
View File

@ -0,0 +1,72 @@
//
// Tabs.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Tab from './Tab';
class Tabs extends Component {
static propTypes = {
children: PropTypes.instanceOf(Array).isRequired,
currentTab: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = { activeTab: this.props.currentTab, };
}
onClickTabItem = (tab) => {
this.setState({ activeTab: tab });
window.history.pushState(tab, tab, '/' + tab);
}
render() {
const {
onClickTabItem,
props: { children, },
state: { activeTab, }
} = this;
return (
<div className="tabs">
<ol className="tab-list"> { children.map((child) => {
const { label } = child.props;
return (
<Tab activeTab={ activeTab }
key={ label }
label={ label }
onClick={ onClickTabItem }
/>
); })
}
</ol>
<div className="tab-content"> { children.map((child) => {
if (child.props.label !== activeTab) return undefined;
return child.props.children;
}) }
</div>
</div>
);
}
}
export default Tabs;

50
webui/src/index.js Normal file
View File

@ -0,0 +1,50 @@
//
// index.js
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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/>.
//
//
//
//
import React from 'react'
import { Route, BrowserRouter as Router, Switch } from 'react-router-dom'
import { render } from "react-dom";
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import ConfigTabs from './ConfigTabs';
toast.configure()
function App() {
return ( <div>
<Router>
<div>
<Switch>
<Route exact path='/PTP' component={() => <ConfigTabs key='PTP' currentTab='PTP' />} />
<Route exact path='/Sources' component={() => <ConfigTabs key='Sources' currentTab='Sources' />} />
<Route exact path='/Sinks' component={() => <ConfigTabs key='Sinks' currentTab='Sinks' />} />
<Route component={() => <ConfigTabs key='Config' currentTab='Config' />} />
</Switch>
</div>
</Router>
</div>
);
}
const container = document.createElement('div');
document.body.appendChild(container);
render( <App/>, container);

39
webui/src/styles.css Normal file
View File

@ -0,0 +1,39 @@
.tab-list {
border-bottom: 1px solid #ccc;
padding-left: 0;
cursor: pointer;
pointer-events: auto;
}
.tab-list-item {
display: inline-block;
list-style: none;
margin-bottom: -1px;
padding: 0.5rem 0.75rem;
}
.tab-list-active {
background-color: white;
border: solid #ccc;
border-width: 1px 1px 0 1px;
}
.input-number {
width: 5em;
}
.table-stream {
border-collapse: separate;
border-spacing: 15px 1rem;
}
.tr-stream {
padding: 5px;
text-align: left;
}
.pointer-area {
cursor: pointer;
pointer-events: auto;
}