Compare commits
24 Commits
a473c84b0e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ec72d0c76 | |||
| 7f567e43a5 | |||
| e13a817245 | |||
| 04a991bf18 | |||
| 865c697174 | |||
| 8f59049579 | |||
| 3768397bfd | |||
| dd75cac4a3 | |||
| f5852dafe0 | |||
| 4fa5f9212a | |||
| 7dca5f1c3d | |||
| 4742b1a1e5 | |||
| 584d4165ff | |||
| a61767f668 | |||
| 6058d3ae64 | |||
| e81dda3b3f | |||
| 7a1b81c6ea | |||
| b8cb750fb4 | |||
| da5ff8ea4a | |||
| e7b032ce77 | |||
| f3f53fc7ad | |||
| a0e46a7bb5 | |||
| ea8c9eeddd | |||
| 841c14f497 |
@@ -1,2 +1,3 @@
|
||||
.DS_Store
|
||||
build
|
||||
imgui.ini
|
||||
|
||||
@@ -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>.
|
||||
@@ -0,0 +1,224 @@
|
||||
# MXL Fabrics ingress plan
|
||||
|
||||
## Goal
|
||||
|
||||
Receive remote MXL audio and video through `go-mxl/fabrics`, place the received data into local MXL storage, and feed it into the existing player without changing playback, retry, session, synchronization, playlist, renderer, or audio-output semantics.
|
||||
|
||||
Start with the SHM provider. TCP, verbs, and EFA follow after the lifecycle and media paths are stable.
|
||||
|
||||
## Correct architecture
|
||||
|
||||
`go-mxl/fabrics` is not a second reader backend. A Fabrics target receives remote transfers into memory owned by a local `mxl.Writer`:
|
||||
|
||||
```text
|
||||
remote fabrics.Initiator
|
||||
|
|
||||
| remote transfer
|
||||
v
|
||||
fabrics.Target
|
||||
|
|
||||
| writes into a local mxl.Writer ring
|
||||
v
|
||||
local MXL flow
|
||||
|
|
||||
v
|
||||
VideoFrame / AudioSamples -> playback workers -> output
|
||||
```
|
||||
|
||||
Consequences:
|
||||
|
||||
- the local MXL media path remains the reference implementation;
|
||||
- Fabrics is an ingress/transport concern, not a playback backend;
|
||||
- playback and playlist code must not know about providers or endpoints;
|
||||
- each remotely transferred flow requires its own target;
|
||||
- target completions must be matched to the exact local grain/sample range;
|
||||
- do not add a blind polling loop that could return stale or overwritten media.
|
||||
|
||||
## Confirmed API facts
|
||||
|
||||
The project pins `github.com/qvest-digital/go-mxl v1.1.0-rc.1`. It includes `github.com/qvest-digital/go-mxl/fabrics` and providers `shm`, `tcp`, `verbs`, `efa`, and `any`.
|
||||
|
||||
The receive-side flow is:
|
||||
|
||||
1. create a local `mxl.Instance`;
|
||||
2. create an `mxl.Writer` from the flow definition;
|
||||
3. create a `fabrics.Instance` from the local MXL instance;
|
||||
4. create a `fabrics.Target`;
|
||||
5. call `Target.Setup` with provider, node/service, writer, and options;
|
||||
6. publish the returned `TargetInfo` to the remote initiator;
|
||||
7. consume completions with `ReadGrain` or `ReadSamples`.
|
||||
|
||||
`TargetInfo` is control-plane data. Initially serialize it to a file for the known sender; automatic discovery is out of scope.
|
||||
|
||||
`fabrics.ErrNotReady` means no completion is ready before the timeout. `fabrics.ErrInterrupted` can be caused by Go runtime signals and means retry the read operation. Neither is a feed failure by itself.
|
||||
|
||||
## Current native-library state
|
||||
|
||||
The installed pkg-config metadata reports `libmxl 1.2.0.0` and `libmxl-fabrics 1.2.0.0`. The pinned Go module records native MXL `v1.1.0-rc1` as its corresponding version. Resolving `libmxl-fabrics` currently fails because pkg-config cannot find `libfabric`.
|
||||
|
||||
This does not prove 1.2 is incompatible. Compare the stack with the user's known-working C++ sender/receiver and choose one tested version set. Do not conceal ABI/API mismatches with casts or copied declarations.
|
||||
|
||||
## Configuration boundary
|
||||
|
||||
Fabrics code should live in `internal/adapter/mxlfabrics`. A provisional configuration is:
|
||||
|
||||
```go
|
||||
type IngressConfig struct {
|
||||
Provider fabrics.Provider
|
||||
Node string
|
||||
Service string
|
||||
Options json.RawMessage
|
||||
TargetInfoPath string
|
||||
Flow mxl.FlowDefinition
|
||||
}
|
||||
```
|
||||
|
||||
Use the exact flow-definition type exposed by the selected library version. Video and audio have independent configurations and targets. They may use different remote endpoints/providers while landing in the same local MXL domain.
|
||||
|
||||
Do not add `--backend local|fabrics`. The useful distinction is whether a feed is already local or needs Fabrics ingress. Choose final per-feed CLI names after the configuration spike.
|
||||
|
||||
## Implementation stages
|
||||
|
||||
### Stage 0 — Align and prove the native stack
|
||||
|
||||
- Record exact versions/commits of `libfabric`, `libmxl`, `libmxl-fabrics`, and `go-mxl` used by the working C++ pair.
|
||||
- Decide whether to retain Go/native 1.1 RC or move the complete set together.
|
||||
- Make all pkg-config and dynamic-linker checks pass.
|
||||
- Run the known C++ pair, then the matching upstream Go SHM target/initiator examples.
|
||||
- Enumerate interfaces/providers through the Go API.
|
||||
- Preserve the current player test baseline.
|
||||
|
||||
```sh
|
||||
pkg-config --modversion libfabric libmxl libmxl-fabrics
|
||||
pkg-config --cflags --libs libfabric libmxl libmxl-fabrics
|
||||
ldd /usr/local/lib/libmxl-fabrics.so
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Acceptance: one documented compatible version set; repeatable Go SHM transfer; clean cancellation/shutdown; no player production changes.
|
||||
|
||||
### Stage 1 — Minimal SHM ingress spike
|
||||
|
||||
- Create a local MXL instance and writer from a supplied flow definition.
|
||||
- Set up one SHM target and serialize its `TargetInfo` to an explicit file.
|
||||
- Receive video grains and audio sample ranges from the known sender in isolated experiments.
|
||||
- Verify completion indices against the corresponding local ring contents.
|
||||
- Document buffer lifetime, overwrite behavior, and sender-absent behavior.
|
||||
|
||||
Acceptance: video and audio transfers work independently; no busy loop; shutdown promptly releases resources.
|
||||
|
||||
### Stage 2 — Define the ingress lifecycle
|
||||
|
||||
Create a narrow adapter owned by the composition root. It must:
|
||||
|
||||
- validate before allocating native resources;
|
||||
- create the writer before target setup and publish info only after setup;
|
||||
- classify timeouts, interruptions, cancellation, and terminal errors;
|
||||
- remain context-cancellable although native reads use timeouts;
|
||||
- allow only one completion reader per target;
|
||||
- close every resource exactly once and unwind partial setup safely.
|
||||
|
||||
Confirm the precise close order against the chosen version. Expected order:
|
||||
|
||||
1. cancel and join the completion loop;
|
||||
2. close target;
|
||||
3. close returned `TargetInfo`;
|
||||
4. close the Fabrics instance after all targets;
|
||||
5. close local readers/writers while respecting the target's writer reference;
|
||||
6. close the local MXL instance last.
|
||||
|
||||
Acceptance: allocation-failure tests, prompt idle/active cancellation, race-safe repeated start/stop, and no resource leaks.
|
||||
|
||||
### Stage 3 — Video ingress
|
||||
|
||||
- Receive completion indices and obtain the exact completed local grain.
|
||||
- Map metadata/payload into `playback.VideoFrame`.
|
||||
- Preserve borrowed-buffer lifetime, or make one deliberate bounded copy if required for safety.
|
||||
- Reject unsupported formats explicitly.
|
||||
- Track received, delivered, overwritten, and malformed grains.
|
||||
|
||||
Never independently poll for the newest local frame after a completion; it may be a different index.
|
||||
|
||||
Acceptance: correct V210 playback; monotonic indices except documented restart; stable 25/50/59.94/high-rate playback; clean disappearance/restoration.
|
||||
|
||||
### Stage 4 — Audio ingress
|
||||
|
||||
- Use `ReadSamples` completion ranges and read exactly the reported head/count.
|
||||
- Map rate, channels, format, label, UUID, and index into the audio contract.
|
||||
- Preserve SDL backpressure and queue policy.
|
||||
- Track samples, discontinuities, overruns, and queue depth.
|
||||
|
||||
Acceptance: correct mono/stereo/multichannel and common rates; no corruption/drift; no duplicated/skipped ranges; no stale queued audio after stop/reconnect.
|
||||
|
||||
### Stage 5 — Independent and synchronized A/V
|
||||
|
||||
Independent A/V uses two independent targets/lifecycles; one failure must not stop the other.
|
||||
|
||||
For synchronized A/V:
|
||||
|
||||
- land both flows in a compatible local MXL domain;
|
||||
- retain native local-MXL synchronization where supported;
|
||||
- coordinate completion availability so sync never consumes data not yet received;
|
||||
- verify different endpoints/providers and fractional frame rates such as 59.94.
|
||||
|
||||
Fabrics transports media; it is not a reason to invent manual synchronization. If native synchronization cannot safely use received rings, write a separate sync design first.
|
||||
|
||||
Acceptance: failure isolation when independent; one retry lifecycle when synchronized; runtime sync toggle retains current semantics; long playback stays aligned.
|
||||
|
||||
### Stage 6 — Player integration
|
||||
|
||||
- Add per-feed local/Fabrics transport configuration.
|
||||
- Construct ingress outside `internal/playback` and connect it to existing workers/slots.
|
||||
- Preserve stop, resume, replace, remove, sync toggle, retry, and playlist behavior.
|
||||
- Keep playlist entries provider-neutral where possible.
|
||||
- Add provider, endpoint, ingress state, completion age, and counters to stats.
|
||||
|
||||
Acceptance: local behavior is unchanged; Fabrics supports video-only, audio-only, independent A/V, and sync A/V; bad config fails before native goroutines; feeds remain independently controllable.
|
||||
|
||||
### Stage 7 — Provider rollout
|
||||
|
||||
Enable and validate in order:
|
||||
|
||||
1. SHM;
|
||||
2. TCP;
|
||||
3. verbs;
|
||||
4. EFA if deployment requires it.
|
||||
|
||||
Provider selection should affect only Fabrics interface configuration and deployment prerequisites. Playback controllers must not branch on provider names.
|
||||
|
||||
### Stage 8 — Playlist and resilience validation
|
||||
|
||||
Test empty startup; audio/video only; independent and synchronized A/V; either sender disappearing/restoring; finite/infinite retry; stop/resume/remove/replace; sync toggling during playback/backoff; `wait`/`next` playlists; timed looping/manual navigation; 59.94/high-rate video; and repeated long-running startup/shutdown.
|
||||
|
||||
Record CPU, memory, received/displayed FPS, completion delta, dropped/overwritten media, audio queue, reconnect latency, and native resource counts.
|
||||
|
||||
## Test layers
|
||||
|
||||
1. Unit tests for configuration, error classification, and lifecycle transitions.
|
||||
2. Adapter tests with injected wrappers around native allocations and reads.
|
||||
3. Real SHM initiator/target integration tests.
|
||||
4. Player smoke tests for all playback combinations.
|
||||
5. Race and repeated lifecycle tests.
|
||||
6. Provider-specific TCP/verbs/EFA live tests.
|
||||
|
||||
Native/live tests must be explicit integration tests and skip with a useful reason when prerequisites are absent.
|
||||
|
||||
## Out of scope for the first pass
|
||||
|
||||
- player as Fabrics initiator/sender;
|
||||
- automatic endpoint or TargetInfo discovery;
|
||||
- changing provider on an active target;
|
||||
- manual A/V sync without a reviewed design;
|
||||
- new media formats bundled into transport work;
|
||||
- provider-specific logic in playback, playlist, renderer, or GUI;
|
||||
- unsafe workarounds for native version mismatches.
|
||||
|
||||
## First action tomorrow
|
||||
|
||||
1. Build/install the exact known-working native stack.
|
||||
2. Record every native version/commit and the C++ reference commands.
|
||||
3. Make all pkg-config checks pass.
|
||||
4. Run the C++ sender/receiver baseline.
|
||||
5. Run the matching upstream Go SHM target/initiator example.
|
||||
6. Capture its flow definition and `TargetInfo` exchange.
|
||||
7. Start Stage 1 only when transfer and clean shutdown are repeatable.
|
||||
@@ -0,0 +1,524 @@
|
||||
# MXL Player
|
||||
|
||||
MXL Player is a desktop and command-line controlled player for local
|
||||
[Media eXchange Layer](https://github.com/dmf-mxl/mxl) audio and video flows.
|
||||
It can play either feed independently, play both simultaneously, or switch a
|
||||
configured pair into and out of native MXL synchronization while running.
|
||||
|
||||
The application uses SDL3 for its window, input, and audio output; Vulkan for
|
||||
video and ImGui rendering; and
|
||||
[`go-mxl`](https://github.com/qvest-digital/go-mxl) for MXL access.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This project is under active development. Builds and feed playback have been
|
||||
> verified in current Ubuntu and Fedora environments and on a separate Ubuntu
|
||||
> machine. Native builds and feed playback have also been verified on macOS
|
||||
> with MoltenVK. A macOS application bundle has been tested in a clean virtual
|
||||
> machine; publication of release artifacts, stable releases, and MXL Fabrics
|
||||
> support are planned.
|
||||
|
||||
## Features
|
||||
|
||||
- Video-only and audio-only playback.
|
||||
- Independent audio and video feeds, including feeds from different domains.
|
||||
- Native synchronized audio/video playback when both feeds are compatible.
|
||||
- Runtime feed apply, stop, resume, remove, and replacement.
|
||||
- Runtime synchronization enable/disable.
|
||||
- Finite or unlimited reconnect attempts with exponential backoff.
|
||||
- JSON playlists with manual selection, timed advance, looping, and failure
|
||||
policies.
|
||||
- Playback, media, timing, retry, and dropped-frame statistics.
|
||||
- Fullscreen mode and an empty idle window when no video is active.
|
||||
- Embedded UI font; no system font installation is required.
|
||||
|
||||
## Current requirements
|
||||
|
||||
- Go 1.26 or newer.
|
||||
- A C/C++ build toolchain for cgo dependencies.
|
||||
- A compatible shared build of `libmxl` and its development headers.
|
||||
- pkg-config metadata for `libmxl`.
|
||||
- SDL3 shared library.
|
||||
- Vulkan loader and a working Vulkan driver.
|
||||
|
||||
The project currently pins:
|
||||
|
||||
```text
|
||||
github.com/qvest-digital/go-mxl v1.1.0-rc.2
|
||||
```
|
||||
|
||||
The headers used while building and the `libmxl.so` loaded at runtime must be
|
||||
compatible. In particular, synchronized playback requires the native symbol:
|
||||
|
||||
```text
|
||||
mxlCreateFlowSynchronizationGroup
|
||||
```
|
||||
|
||||
Check it with:
|
||||
|
||||
```sh
|
||||
nm -D /path/to/libmxl.so | grep mxlCreateFlowSynchronizationGroup
|
||||
```
|
||||
|
||||
## Building on Linux
|
||||
|
||||
Verified source-build environments:
|
||||
|
||||
| Environment | Result | Required distribution packages |
|
||||
|---|---|---|
|
||||
| Ubuntu (`ubuntu:latest`, Distrobox) | Build and feed playback successful | `golang`, `libsdl3-dev`, `spdlog-dev` |
|
||||
| Fedora (`fedora:latest`, Distrobox) | Build and feed playback successful | `golang`, `SDL3-devel`, `spdlog-devel`, `g++` |
|
||||
| Ubuntu, separate physical machine | Build and feed playback successful | SDL3, compiler toolchain, and a compatible libmxl installation |
|
||||
|
||||
These checks used an existing shared libmxl installation with
|
||||
`PKG_CONFIG_PATH` pointing to a valid `libmxl.pc`. Package names can differ
|
||||
between distribution releases.
|
||||
|
||||
First verify that pkg-config resolves the intended MXL installation:
|
||||
|
||||
```sh
|
||||
pkg-config --modversion libmxl
|
||||
pkg-config --cflags --libs libmxl
|
||||
pkg-config --variable=pcfiledir libmxl
|
||||
pkg-config --variable=libdir libmxl
|
||||
```
|
||||
|
||||
If MXL is installed in a nonstandard prefix:
|
||||
|
||||
```sh
|
||||
export MXL_PREFIX=/opt/mxl/1.2.0
|
||||
export PKG_CONFIG_PATH="$MXL_PREFIX/lib/pkgconfig"
|
||||
export LD_LIBRARY_PATH="$MXL_PREFIX/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
```
|
||||
|
||||
Then build and test:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
go test ./...
|
||||
mkdir -p build
|
||||
go build -o build/mxl-player ./cmd/mxl-player
|
||||
```
|
||||
|
||||
Confirm which native library the executable will use:
|
||||
|
||||
```sh
|
||||
ldd build/mxl-player | grep libmxl
|
||||
```
|
||||
|
||||
Do not copy a `libmxl.pc` file containing paths from another computer. A
|
||||
relocatable SDK file can use `${pcfiledir}` to describe its own location.
|
||||
|
||||
## macOS status
|
||||
|
||||
The SDL3 and Vulkan loaders contain macOS library-name support. The Vulkan
|
||||
dependency is temporarily replaced by a portability fork while the upstream
|
||||
change is reviewed.
|
||||
|
||||
A macOS source build and native feed playback have succeeded on real Apple
|
||||
hardware. Building currently requires:
|
||||
|
||||
- Xcode Command Line Tools;
|
||||
- Go;
|
||||
- SDL3;
|
||||
- Vulkan loader and MoltenVK;
|
||||
- a macOS build of libmxl with matching headers and pkg-config metadata.
|
||||
|
||||
When libmxl has been built from its source tree but has not been installed into
|
||||
a standard prefix, expose its build metadata, source headers, and library
|
||||
directory to cgo. For example:
|
||||
|
||||
```sh
|
||||
export MXL_SOURCE=/Users/jitten/extra/mxl
|
||||
export MXL_BUILD="$MXL_SOURCE/build/Darwin-Clang-Release"
|
||||
|
||||
export PKG_CONFIG_PATH="$MXL_BUILD${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
|
||||
export CGO_CFLAGS="-I$MXL_SOURCE/lib/include"
|
||||
export CGO_LDFLAGS="-L$MXL_BUILD/lib"
|
||||
|
||||
mkdir -p build
|
||||
go build -o build/mxl-player ./cmd/mxl-player
|
||||
```
|
||||
|
||||
The `CGO_CFLAGS` workaround is needed because the headers are still in the MXL
|
||||
source tree. A properly installed MXL SDK should expose them through
|
||||
`libmxl.pc` instead.
|
||||
|
||||
Nonstandard loader locations can be supplied explicitly:
|
||||
|
||||
```sh
|
||||
export SDL3_LIBRARY="$(brew --prefix sdl3)/lib/libSDL3.dylib"
|
||||
export VULKAN_LIBRARY=/absolute/path/to/libMoltenVK.dylib
|
||||
export DYLD_LIBRARY_PATH="$MXL_BUILD/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
|
||||
|
||||
./build/mxl-player
|
||||
```
|
||||
|
||||
Direct loading of `libMoltenVK.dylib` is the configuration verified to work.
|
||||
The Vulkan loader installed through Homebrew did not work in the tested setup,
|
||||
so it is not currently recommended for running the player.
|
||||
|
||||
The resulting executable may currently need an additional libmxl runtime search
|
||||
path, for example:
|
||||
|
||||
```sh
|
||||
install_name_tool -add_rpath /absolute/path/to/mxl/lib build/mxl-player
|
||||
```
|
||||
|
||||
These workarounds are only needed for development builds. The packaged `.app`
|
||||
uses bundled libraries and relative runtime paths instead.
|
||||
|
||||
### Running the macOS application bundle
|
||||
|
||||
The ARM64 `.app` bundle has been tested on physical Apple Silicon hardware. The
|
||||
application extracted from its release ZIP has also been launched successfully
|
||||
in a clean macOS installation under UTM, without the development environment or
|
||||
Homebrew dependencies.
|
||||
|
||||
The initial package is unsigned and is not notarized by Apple. Consequently,
|
||||
Gatekeeper may block its first launch. Right-click **MXL Player.app**, select
|
||||
**Open**, and confirm the prompt. It can also be allowed from **System Settings
|
||||
→ Privacy & Security** after a blocked launch.
|
||||
|
||||
If macOS still retains a quarantine restriction, it can be removed manually:
|
||||
|
||||
```sh
|
||||
xattr -dr com.apple.quarantine "/Applications/MXL Player.app"
|
||||
```
|
||||
|
||||
Only use this command for an archive downloaded from the project's official
|
||||
GitHub release and verified against its published SHA-256 checksum.
|
||||
|
||||
## Running
|
||||
|
||||
Start with no configured feed and enter domains/UUIDs in the GUI:
|
||||
|
||||
```sh
|
||||
./build/mxl-player
|
||||
```
|
||||
|
||||
Video only:
|
||||
|
||||
```sh
|
||||
./build/mxl-player \
|
||||
--domain /dev/shm/mxl \
|
||||
--video VIDEO_UUID
|
||||
```
|
||||
|
||||
Audio only:
|
||||
|
||||
```sh
|
||||
./build/mxl-player \
|
||||
--domain /dev/shm/mxl \
|
||||
--audio AUDIO_UUID
|
||||
```
|
||||
|
||||
Independent video and audio:
|
||||
|
||||
```sh
|
||||
./build/mxl-player \
|
||||
--video-domain /dev/shm/video \
|
||||
--video VIDEO_UUID \
|
||||
--audio-domain /dev/shm/audio \
|
||||
--audio AUDIO_UUID
|
||||
```
|
||||
|
||||
Start a compatible pair synchronized:
|
||||
|
||||
```sh
|
||||
./build/mxl-player \
|
||||
--domain /dev/shm/mxl \
|
||||
--video VIDEO_UUID \
|
||||
--audio AUDIO_UUID \
|
||||
--sync
|
||||
```
|
||||
|
||||
Synchronization is not a permanent startup mode. It can be enabled and
|
||||
disabled at runtime from the settings window.
|
||||
|
||||
## Command-line options
|
||||
|
||||
| Option | Short | Meaning |
|
||||
|---|---:|---|
|
||||
| `--help` | `-h` | Show help and exit. |
|
||||
| `--domain PATH` | `-d` | Default domain for feeds without a specific domain. |
|
||||
| `--video-domain PATH` | | Domain for the video feed. |
|
||||
| `--audio-domain PATH` | | Domain for the audio feed. |
|
||||
| `--video UUID` | `-v` | Initial video flow UUID. |
|
||||
| `--audio UUID` | `-a` | Initial audio flow UUID. |
|
||||
| `--sync` | `-s` | Start a configured A/V pair synchronized. |
|
||||
| `--max-attempts N` | | Attempts per playback lifecycle; `0` means unlimited. |
|
||||
| `--playlist FILE` | | Load a JSON playlist. |
|
||||
| `--fullscreen` | `-f` | Start fullscreen. |
|
||||
| `--gpu-id N` | `-g` | Select GPU ID; currently incomplete. |
|
||||
| `--playback-id N` | `-p` | Select the SDL playback-device ID. |
|
||||
| `--list-playback` | | List SDL playback devices and exit. |
|
||||
| `--list-gpu` | | List Vulkan physical devices and exit. |
|
||||
| `--verbose` | | Reserved; currently incomplete. |
|
||||
|
||||
Run `./build/mxl-player --help` for the authoritative list.
|
||||
|
||||
## GUI controls
|
||||
|
||||
The settings window accepts separate video/audio domains and UUIDs.
|
||||
|
||||
- **Apply feeds** applies the complete desired configuration.
|
||||
- **Stop** stops a feed while retaining its UUID and domain.
|
||||
- **Resume** starts a stopped configured feed with a fresh lifecycle.
|
||||
- **Remove** stops a feed and clears its configuration.
|
||||
- **Stop all** and **Resume all** operate on both configured feeds.
|
||||
- **Synchronize** switches a compatible active pair between synchronized and
|
||||
independent playback.
|
||||
|
||||
Keyboard shortcuts:
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `F1` | Show or hide settings. |
|
||||
| `F2` | Show or hide statistics. |
|
||||
| `F` | Toggle fullscreen. |
|
||||
| `Q` or `Esc` | Quit. |
|
||||
|
||||
## Retry behavior
|
||||
|
||||
Each playback lifecycle starts at attempt 1. Failed connections/readers are
|
||||
retried with exponential delay from 500 ms up to 5 seconds.
|
||||
|
||||
```sh
|
||||
# Retry indefinitely
|
||||
./build/mxl-player -d /dev/shm/mxl -v VIDEO_UUID --max-attempts 0
|
||||
|
||||
# Stop after three failed attempts
|
||||
./build/mxl-player -d /dev/shm/mxl -v VIDEO_UUID --max-attempts 3
|
||||
```
|
||||
|
||||
With independent A/V, one failed feed retries without stopping the other. A
|
||||
synchronized pair reconnects as one lifecycle.
|
||||
|
||||
## Playlists
|
||||
|
||||
Start a playlist with:
|
||||
|
||||
```sh
|
||||
./build/mxl-player --playlist playlist.json
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"loop": true,
|
||||
"on_failure": "next",
|
||||
"retry": {
|
||||
"max_attempts": 3,
|
||||
"initial_delay": "500ms",
|
||||
"max_delay": "5s"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"name": "Synchronized pair",
|
||||
"video": {
|
||||
"domain": "/dev/shm/mxl",
|
||||
"uuid": "VIDEO_UUID"
|
||||
},
|
||||
"audio": {
|
||||
"domain": "/dev/shm/mxl",
|
||||
"uuid": "AUDIO_UUID"
|
||||
},
|
||||
"sync": true,
|
||||
"duration": "10s"
|
||||
},
|
||||
{
|
||||
"name": "Video only",
|
||||
"video": {
|
||||
"domain": "/dev/shm/mxl",
|
||||
"uuid": "ANOTHER_VIDEO_UUID"
|
||||
},
|
||||
"duration": "15s"
|
||||
},
|
||||
{
|
||||
"name": "Audio until manually advanced",
|
||||
"audio": {
|
||||
"domain": "/dev/shm/mxl",
|
||||
"uuid": "ANOTHER_AUDIO_UUID"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Playlist rules:
|
||||
|
||||
- `duration` uses Go duration syntax such as `500ms`, `10s`, or `2m`.
|
||||
- Missing/empty duration means manual advance.
|
||||
- `loop: true` returns to the first entry after the last.
|
||||
- `on_failure: "wait"` remains on an exhausted entry.
|
||||
- `on_failure: "next"` advances after retry exhaustion.
|
||||
- A synchronized entry must contain both video and audio.
|
||||
- Playlist retry settings override the CLI defaults unless
|
||||
`--max-attempts` was explicitly supplied.
|
||||
|
||||
The GUI supports entry selection, previous/next navigation, and pausing or
|
||||
resuming a timed entry.
|
||||
|
||||
## Development checks
|
||||
|
||||
```sh
|
||||
go fmt ./...
|
||||
go test ./...
|
||||
go test -race ./internal/playback ./cmd/mxl-player
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
Live media and GPU/audio integration still require real MXL producers and are
|
||||
not covered by the unit test suite.
|
||||
|
||||
## Architecture
|
||||
|
||||
Playback workers own individual reader lifecycles and retries. Slots ensure
|
||||
that only one worker lifecycle for a feed or synchronized pair runs at a time.
|
||||
The session controller converts the complete desired feed state into an
|
||||
independent or synchronized execution plan. Playlist logic sends the same
|
||||
session commands as the GUI, so it does not bypass lifecycle rules.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CLI[CLI startup configuration]
|
||||
GUI[ImGui controls]
|
||||
PC[PlaylistController]
|
||||
PEC[PlaylistEventCoordinator]
|
||||
|
||||
CMD[SessionCommand channel]
|
||||
SC[SessionController]
|
||||
PLAN[SessionPlan]
|
||||
|
||||
VS[VideoSlot]
|
||||
AS[AudioSlot]
|
||||
SS[SyncSlot]
|
||||
|
||||
VW[VideoWorker]
|
||||
AW[AudioWorker]
|
||||
SW[SyncWorker]
|
||||
|
||||
VF[VideoFactory]
|
||||
AF[AudioFactory]
|
||||
SF[SyncFactory]
|
||||
MXL[go-mxl / libmxl]
|
||||
|
||||
VB[VideoBridge]
|
||||
VR[Vulkan renderer]
|
||||
AO[SDL audio sink]
|
||||
|
||||
STATUS[StatusStore]
|
||||
MEDIA[MediaStatsStore]
|
||||
|
||||
CLI --> CMD
|
||||
GUI --> CMD
|
||||
PC --> CMD
|
||||
CMD --> SC
|
||||
SC --> PLAN
|
||||
|
||||
PLAN -->|independent video| VS
|
||||
PLAN -->|independent audio| AS
|
||||
PLAN -->|synchronized pair| SS
|
||||
|
||||
VS --> VW --> VF --> MXL
|
||||
AS --> AW --> AF --> MXL
|
||||
SS --> SW --> SF --> MXL
|
||||
|
||||
VW --> VB --> VR
|
||||
SW --> VB
|
||||
AW --> AO
|
||||
SW --> AO
|
||||
|
||||
VW --> STATUS
|
||||
AW --> STATUS
|
||||
SW --> STATUS
|
||||
VW --> MEDIA
|
||||
AW --> MEDIA
|
||||
SW --> MEDIA
|
||||
|
||||
STATUS --> GUI
|
||||
MEDIA --> GUI
|
||||
STATUS --> PEC
|
||||
SC --> PEC
|
||||
PC --> PEC
|
||||
PEC --> PC
|
||||
```
|
||||
|
||||
### Main entities
|
||||
|
||||
| Entity | Responsibility |
|
||||
|---|---|
|
||||
| `SessionConfig` | Complete desired state of both feeds, synchronization request, and retry policy. |
|
||||
| `SessionCommand` | One user or playlist request that produces a new desired session state. |
|
||||
| `SessionController` | Owns the current desired state and reconciles it with the required topology. |
|
||||
| `SessionPlan` | Validated execution plan: idle, independent feeds, or one synchronized pair. |
|
||||
| `VideoSlot` / `AudioSlot` | Own replacement and cancellation of one independent worker lifecycle. |
|
||||
| `SyncSlot` | Owns replacement and cancellation of the synchronized pair lifecycle. |
|
||||
| Workers | Open readers, consume media, apply retry policy, and publish lifecycle status. |
|
||||
| MXL adapter factories | Translate `go-mxl` readers, metadata, and errors into playback contracts. |
|
||||
| `VideoBridge` | Transfers the latest borrowed video frame safely to the render loop. |
|
||||
| `SDLAudioSink` | Interleaves planar F32 samples and manages SDL audio queueing. |
|
||||
| `StatusStore` | Holds the latest lifecycle status for each playback unit. |
|
||||
| `MediaStatsStore` | Holds feed metadata and received-media counters for diagnostics. |
|
||||
| `PlaylistController` | Owns selection, timing, looping, navigation, and failure policy. |
|
||||
| `PlaylistEventCoordinator` | Observes session/status state and tells the playlist when an entry is ready or exhausted. |
|
||||
|
||||
### Ownership rules
|
||||
|
||||
- Every incoming feed configuration represents the complete desired state for
|
||||
that slot.
|
||||
- A slot cancels and joins its current worker before starting a replacement;
|
||||
two worker lifecycles for one slot must never overlap.
|
||||
- Invalid commands do not disturb the current valid session.
|
||||
- Independent audio and video fail and retry independently.
|
||||
- A synchronized pair fails and retries as one lifecycle.
|
||||
- Runtime synchronization changes topology; it is not a startup-only mode.
|
||||
- Cancellation flows from the application context through controllers, slots,
|
||||
workers, readers, and sinks.
|
||||
- Playback owns lifecycle policy. Adapters know MXL details, while renderer,
|
||||
audio output, GUI, and playlists do not.
|
||||
|
||||
### Source layout
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `cmd/mxl-player` | Composition root, CLI, main loop, GUI, and playlist file decoding. |
|
||||
| `internal/playback` | Transport-independent state, workers, slots, controllers, retry, playlist, and statistics. |
|
||||
| `internal/adapter/mxl` | Local MXL implementations of playback reader contracts. |
|
||||
| `internal/source` | Low-level local MXL video/audio access and error translation. |
|
||||
| `internal/output` | SDL audio sink and sample interleaving. |
|
||||
| `internal/renderer` | Vulkan video rendering. |
|
||||
| `internal/imgui` | ImGui SDL input and Vulkan rendering integration. |
|
||||
| `internal/sdl` | Minimal dynamically loaded SDL3 bindings used by the player. |
|
||||
| `internal/assets` | Embedded UI assets and their licenses. |
|
||||
|
||||
MXL Fabrics is planned as an ingress transport that writes remote data into a
|
||||
local MXL flow; it will not be implemented as a separate playback backend. See
|
||||
[`MXLFABRICS_PLAN.md`](MXLFABRICS_PLAN.md).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Linux builds and feed playback have been verified on Ubuntu and Fedora;
|
||||
broader runtime and hardware coverage is still limited.
|
||||
- macOS/MoltenVK builds and feed playback work on tested Apple Silicon
|
||||
hardware. The packaged application launches in a clean UTM macOS VM, but it
|
||||
remains unsigned and unnotarized.
|
||||
- Windows support is not currently claimed.
|
||||
- GPU selection and verbose logging flags are incomplete.
|
||||
- Release artifacts have not yet been published.
|
||||
- MXL Fabrics ingress is planned but not implemented.
|
||||
- Native MXL headers and runtime libraries must be ABI-compatible.
|
||||
|
||||
## Third-party assets
|
||||
|
||||
The embedded JetBrainsMonoNL Nerd Font Mono is distributed under the SIL Open
|
||||
Font License 1.1. Its copyright notice, license, and source information are in
|
||||
[`internal/assets/fonts`](internal/assets/fonts/).
|
||||
|
||||
## License
|
||||
|
||||
MXL Player is licensed under the GNU General Public License, version 3 or any
|
||||
later version. See [`LICENSE`](LICENSE).
|
||||
|
||||
The embedded font retains its separate SIL Open Font License 1.1.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="143.653mm"
|
||||
height="143.653mm"
|
||||
viewBox="0 0 143.653 143.65301"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:export-filename="mxl-on-blue-square.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="mxl-on-blue-square.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.2479451"
|
||||
inkscape:cx="200.32933"
|
||||
inkscape:cy="276.05381"
|
||||
inkscape:window-width="2494"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="66"
|
||||
inkscape:window-y="663"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs2" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="bg"
|
||||
style="display:inline"
|
||||
transform="matrix(0.61643941,0,0,1,4.1386047,-120.68901)"
|
||||
sodipodi:insensitive="true"><rect
|
||||
style="fill:#1b4098;fill-opacity:1;stroke:none;stroke-width:1.23722"
|
||||
id="rect3467"
|
||||
width="233.0367"
|
||||
height="143.653"
|
||||
x="-6.7137251"
|
||||
y="120.68901"
|
||||
inkscape:label="bg" /></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="2" /><g
|
||||
inkscape:label="1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(21.246118,-169.37755)"
|
||||
style="display:inline"><g
|
||||
id="g6784"><g
|
||||
id="g860"
|
||||
transform="translate(3.0120144,38.690999)"><g
|
||||
aria-label="M"
|
||||
id="text113-7"
|
||||
style="fill:#ffffff;stroke-width:0.264582"
|
||||
transform="translate(-3.704032)"><path
|
||||
d="m 34.799623,220.64854 -7.823154,-25.26436 v 0 c 0.03387,0.67733 -0.06773,1.65945 0,2.94638 0.101599,1.25306 -0.06773,2.60772 0,4.06397 0.06773,1.45626 0,2.79399 0,4.01318 v 14.24083 h -7.721555 v -36.27098 h 11.683932 l 8.077152,24.17641 H 35.51503 l 8.331151,-24.17641 h 11.633132 v 36.27098 H 47.45296 v -14.44403 c 0,-1.15146 -0.03387,-2.42145 0,-3.80998 0.06773,-1.42239 -0.03387,-2.76011 0,-4.01317 0.06773,-1.28693 -0.03387,-2.26906 0,-2.94639 v 0 l -8.229552,25.21357 z"
|
||||
id="path10652"
|
||||
sodipodi:nodetypes="ccccssccccccccccscccccc" /></g><g
|
||||
aria-label="X"
|
||||
id="text113-0-35"
|
||||
style="fill:#ffffff;stroke-width:0.264582"><path
|
||||
d="m 84.156766,220.64854 h -9.956741 l -7.772355,-12.59832 -7.772354,12.59832 h -9.550344 l 12.03953,-18.64349 -11.328334,-17.62749 h 9.601144 l 7.264357,12.19193 7.010359,-12.19193 h 9.601143 l -11.379133,18.23709 z"
|
||||
id="path10672" /></g><g
|
||||
aria-label="L"
|
||||
id="text113-0-6-2"
|
||||
style="fill:#ffffff;stroke-width:0.264582"><path
|
||||
d="m 81.382722,220.64854 v -36.27098 h 8.635949 v 29.20983 h 14.376319 v 7.06115 z"
|
||||
id="path10669" /></g></g><rect
|
||||
style="fill:#ffffff;stroke-width:0.235289"
|
||||
id="rect526-1"
|
||||
width="23.011999"
|
||||
height="7.7199998"
|
||||
x="-6.2462406"
|
||||
y="223.06856" /><rect
|
||||
style="fill:#ffffff;stroke-width:0.235289"
|
||||
id="rect526-7-2"
|
||||
width="23.011999"
|
||||
height="7.7199998"
|
||||
x="-6.2462406"
|
||||
y="232.58556" /><rect
|
||||
style="display:inline;fill:#ffffff;stroke-width:0.259539"
|
||||
id="rect526-7-2-2"
|
||||
width="28"
|
||||
height="7.7199998"
|
||||
x="-6.2462406"
|
||||
y="242.10254" /><rect
|
||||
style="fill:#ffffff;stroke-width:0.235289"
|
||||
id="rect526-7-0-3-0"
|
||||
width="23.011999"
|
||||
height="7.7199998"
|
||||
x="-6.2462406"
|
||||
y="251.61954" /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ELF building
|
||||
export MXL_SOURCE=/Users/jitten/extra/mxl
|
||||
export MXL_BUILD="${MXL_SOURCE}/build/Darwin-Clang-Release"
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${WORK_DIR}"' EXIT
|
||||
|
||||
# libmxl.pc in the build tree hardcodes the install/ prefix; rewrite it so
|
||||
# cgo's pkg-config lookup gets the build tree's lib/include dirs.
|
||||
sed -e "s|${MXL_SOURCE}/install|${MXL_SOURCE}/build|g" \
|
||||
-e "s|^includedir=.*|includedir=${MXL_BUILD}/lib/include|" \
|
||||
"${MXL_BUILD}/lib/libmxl.pc" > "${WORK_DIR}/libmxl.pc"
|
||||
export PKG_CONFIG_PATH="${WORK_DIR}:${MXL_BUILD}/lib${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
|
||||
export CGO_CFLAGS="-I${MXL_SOURCE}/lib/include"
|
||||
export CGO_LDFLAGS="-L${MXL_BUILD}/lib"
|
||||
|
||||
PLAYER_ELF_PATH="./build/mxl-player"
|
||||
go build \
|
||||
-trimpath \
|
||||
-ldflags="-s -w" \
|
||||
-o "${PLAYER_ELF_PATH}" \
|
||||
./cmd/mxl-player
|
||||
|
||||
# MacOS .app package
|
||||
export APP_BUNDLE="./release/mac/MXL-player.app"
|
||||
rm -rf "${APP_BUNDLE}"
|
||||
mkdir -p "${APP_BUNDLE}/Contents/MacOS"
|
||||
mkdir -p "${APP_BUNDLE}/Contents/Frameworks"
|
||||
mkdir -p "${APP_BUNDLE}/Contents/Resources"
|
||||
|
||||
cp "${PLAYER_ELF_PATH}" "${APP_BUNDLE}/Contents/MacOS/mxl-player"
|
||||
chmod 0755 "${APP_BUNDLE}/Contents/MacOS/mxl-player"
|
||||
|
||||
# App icon
|
||||
ICONSET="${WORK_DIR}/MXL-player.iconset"
|
||||
mkdir -p "${ICONSET}"
|
||||
for s in 16 32 128 256 512; do
|
||||
sips -z $s $s ./assets/mxl-on-blue-square.png \
|
||||
--out "${ICONSET}/icon_${s}x${s}.png" > /dev/null
|
||||
sips -z $((s*2)) $((s*2)) ./assets/mxl-on-blue-square.png \
|
||||
--out "${ICONSET}/icon_${s}x${s}@2x.png" > /dev/null
|
||||
done
|
||||
iconutil -c icns "${ICONSET}" \
|
||||
-o "${APP_BUNDLE}/Contents/Resources/AppIcon.icns"
|
||||
|
||||
# Info.plist
|
||||
cat << EOF > "${APP_BUNDLE}/Contents/Info.plist"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple/DTD PLIST 1.0//EN"
|
||||
"https://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>MXL Player</string>
|
||||
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>MXL Player</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.github.jitten.mxl-player</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>mxl-player</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.video</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
plutil -lint "${APP_BUNDLE}/Contents/Info.plist"
|
||||
|
||||
# copy runtime libs (install IDs must stay untouched: libmxl is already
|
||||
# @rpath/libmxl.1.dylib from the build tree; SDL3/MoltenVK are dlopen'ed by
|
||||
# leaf name, so their IDs are never referenced)
|
||||
cp -L "${MXL_BUILD}/lib/libmxl.1.dylib" \
|
||||
"${APP_BUNDLE}/Contents/Frameworks/libmxl.1.dylib"
|
||||
cp -L "$(brew --prefix sdl3)/lib/libSDL3.0.dylib" \
|
||||
"${APP_BUNDLE}/Contents/Frameworks/libSDL3.0.dylib"
|
||||
cp -L "$(brew --prefix molten-vk)/lib/libMoltenVK.dylib" \
|
||||
"${APP_BUNDLE}/Contents/Frameworks/libMoltenVK.dylib"
|
||||
|
||||
# resolve @rpath/libmxl.1.dylib and the dlopen'ed SDL3/MoltenVK leaf names
|
||||
# against the bundle's Frameworks dir
|
||||
install_name_tool \
|
||||
-add_rpath "@loader_path/../Frameworks" \
|
||||
"${APP_BUNDLE}/Contents/MacOS/mxl-player"
|
||||
|
||||
# install_name_tool invalidated the executable's ad-hoc signature; re-sign
|
||||
codesign --force --sign - "${APP_BUNDLE}"
|
||||
codesign --verify "${APP_BUNDLE}"
|
||||
|
||||
env -u SDL3_LIBRARY \
|
||||
-u VULKAN_LIBRARY \
|
||||
-u DYLD_LIBRARY_PATH \
|
||||
"${APP_BUNDLE}/Contents/MacOS/mxl-player"
|
||||
|
||||
# add licenses
|
||||
mkdir -p "${APP_BUNDLE}/Contents/Resources/licenses"
|
||||
cp ./assets/third-party-licenses/*.txt "${APP_BUNDLE}/Contents/Resources/licenses"
|
||||
|
||||
# compress, preserve MacOS bundle meta
|
||||
ditto -c -k \
|
||||
--sequesterRsrc \
|
||||
--keepParent \
|
||||
"${APP_BUNDLE}" \
|
||||
./release/mac/mxl-player-0.1.0-macos-arm64.zip
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"mxl-player/internal/playback"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -17,6 +18,25 @@ func resolveDomain(shared, override string) string {
|
||||
return shared
|
||||
}
|
||||
|
||||
func normalizeFeedInput(domain, uuid string) (string, string) {
|
||||
return strings.TrimSpace(domain), strings.TrimSpace(uuid)
|
||||
}
|
||||
|
||||
func resolveRetryPolicy(
|
||||
cli playback.RetryPolicy,
|
||||
cliMaxAttemptsSet bool,
|
||||
playlist playback.Playlist,
|
||||
) playback.RetryPolicy {
|
||||
if playlist.Retry == nil {
|
||||
return cli
|
||||
}
|
||||
resolved := *playlist.Retry
|
||||
if cliMaxAttemptsSet {
|
||||
resolved.MaxAttempts = cli.MaxAttempts
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
func (a appArgs) playbackConfig() playback.SessionConfig {
|
||||
return playback.SessionConfig{
|
||||
Video: playback.FeedConfig{
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mxl-player/internal/playback"
|
||||
)
|
||||
|
||||
func TestAppArgsPlaybackConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -139,3 +144,55 @@ func TestAppArgsPlaybackConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRetryPolicy(t *testing.T) {
|
||||
cli := playback.RetryPolicy{
|
||||
MaxAttempts: 0, InitialDelay: time.Second, MaxDelay: 5 * time.Second,
|
||||
}
|
||||
fileRetry := playback.RetryPolicy{
|
||||
MaxAttempts: 4, InitialDelay: 250 * time.Millisecond, MaxDelay: 2 * time.Second,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
playlist playback.Playlist
|
||||
cliMaxAttemptsSet bool
|
||||
want playback.RetryPolicy
|
||||
}{
|
||||
{name: "no playlist retry uses CLI", want: cli},
|
||||
{
|
||||
name: "playlist retry is used by default",
|
||||
playlist: playback.Playlist{Retry: &fileRetry},
|
||||
want: fileRetry,
|
||||
},
|
||||
{
|
||||
name: "explicit CLI infinite overrides playlist attempts",
|
||||
playlist: playback.Playlist{Retry: &fileRetry},
|
||||
cliMaxAttemptsSet: true,
|
||||
want: playback.RetryPolicy{
|
||||
MaxAttempts: 0, InitialDelay: fileRetry.InitialDelay, MaxDelay: fileRetry.MaxDelay,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := resolveRetryPolicy(cli, test.cliMaxAttemptsSet, test.playlist); got != test.want {
|
||||
t.Fatalf("resolveRetryPolicy() = %#v, want %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFeedInput(t *testing.T) {
|
||||
domain, uuid := normalizeFeedInput(
|
||||
" \t/dev/shm/mxl\n",
|
||||
"\r 5fbec3b1-1b0f-417d-9059-8b94a47197ef \t",
|
||||
)
|
||||
if domain != "/dev/shm/mxl" {
|
||||
t.Fatalf("domain = %q", domain)
|
||||
}
|
||||
if uuid != "5fbec3b1-1b0f-417d-9059-8b94a47197ef" {
|
||||
t.Fatalf("UUID = %q", uuid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
|
||||
const guiFrameInterval = time.Second / 60
|
||||
|
||||
// videoPollInterval bounds GUI latency when no video producer is waiting.
|
||||
// It is not a video-rate cap: Next returns immediately whenever a frame
|
||||
// arrives, including for sources faster than this interval.
|
||||
const videoPollInterval = 8 * time.Millisecond
|
||||
|
||||
func remainingFrameTime(start, now time.Time, interval time.Duration) time.Duration {
|
||||
remaining := interval - now.Sub(start)
|
||||
if remaining < 0 {
|
||||
return 0
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func paceFrame(start time.Time) {
|
||||
if remaining := remainingFrameTime(start, time.Now(), guiFrameInterval); remaining > 0 {
|
||||
time.Sleep(remaining)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRemainingFrameTime(t *testing.T) {
|
||||
start := time.Date(2026, time.September, 2, 0, 0, 0, 0, time.UTC)
|
||||
interval := 16 * time.Millisecond
|
||||
tests := []struct {
|
||||
name string
|
||||
elapsed time.Duration
|
||||
want time.Duration
|
||||
}{
|
||||
{name: "no work", want: interval},
|
||||
{name: "partial budget", elapsed: 5 * time.Millisecond, want: 11 * time.Millisecond},
|
||||
{name: "exact budget", elapsed: interval},
|
||||
{name: "over budget", elapsed: 20 * time.Millisecond},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := remainingFrameTime(start, start.Add(test.elapsed), interval)
|
||||
if got != test.want {
|
||||
t.Fatalf("remainingFrameTime() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+105
-17
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mxl-player/internal/assets"
|
||||
"mxl-player/internal/imgui"
|
||||
"mxl-player/internal/playback"
|
||||
"mxl-player/internal/renderer"
|
||||
@@ -97,12 +98,6 @@ func checkMXLargs(args appArgs) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
// timelapse video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed
|
||||
// timelapse audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec
|
||||
// f1 video: 5fbec3b1-1b0f-417d-9059-8b94a47197ef
|
||||
// f1 audio: 5fbec3b1-1b0f-417d-9059-8b94a47197eb
|
||||
// sync video: 2618979d-76a5-45e0-83cb-0f192978d1cd
|
||||
// sync audio: 9d2a041b-01cf-4ee4-bffa-188fe093c99b
|
||||
var args appArgs
|
||||
flagSet := pflag.NewFlagSet(APP_NAME, pflag.ContinueOnError)
|
||||
flagSet.SortFlags = false
|
||||
@@ -176,6 +171,11 @@ func main() {
|
||||
os.Exit(2)
|
||||
}
|
||||
configuredPlaylist = playlist
|
||||
retryPolicy = resolveRetryPolicy(
|
||||
retryPolicy,
|
||||
flagSet.Changed("max-attempts"),
|
||||
configuredPlaylist,
|
||||
)
|
||||
}
|
||||
if args.VideoDomain == "" {
|
||||
args.VideoDomain = args.Domain
|
||||
@@ -194,6 +194,7 @@ func main() {
|
||||
log.Fatalf("SDL_Init: %s", sdl.GetError())
|
||||
return
|
||||
}
|
||||
defer sdl.Quit()
|
||||
|
||||
sdlAudioDevice := sdl.AudioDeviceDefaultPlayback
|
||||
if args.PlaybackId != 0 {
|
||||
@@ -210,19 +211,39 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
vulkanPath := os.Getenv("VULKAN_LIBRARY")
|
||||
if vulkanPath != "" {
|
||||
if !sdl.VulkanLoadLibrary(vulkanPath) {
|
||||
log.Fatalf(
|
||||
"SDL_Vulkan_LoadLibrary: %s",
|
||||
sdl.GetError(),
|
||||
)
|
||||
}
|
||||
defer sdl.VulkanUnloadLibrary()
|
||||
}
|
||||
|
||||
if err := vk.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
windowHandler := sdl.CreateWindow(fmt.Sprintf("%s %s", APP_NAME, APP_VER), WIN_WIDTH, WIN_HEIGHT, sdl.WindowVulkan|sdl.WindowResizable)
|
||||
if windowHandler == 0 {
|
||||
sdl.Quit()
|
||||
log.Fatalf("SDL_CreateWindow: %s", sdl.GetError())
|
||||
return
|
||||
}
|
||||
defer sdl.DestroyWindow(windowHandler)
|
||||
|
||||
fontData, fontDataSize, releaseFont := assets.PinUIFont()
|
||||
defer releaseFont()
|
||||
|
||||
// ImGui init
|
||||
gui := imgui.New()
|
||||
defer gui.Destroy()
|
||||
fontConfig := cimgui.NewFontConfig()
|
||||
font := gui.IO().Fonts().AddFontFromFileTTFV(
|
||||
"/home/itten/Downloads/JetBrainsMono/JetBrainsMonoNLNerdFontMono-Regular.ttf",
|
||||
fontConfig.SetFontDataOwnedByAtlas(false)
|
||||
font := gui.IO().Fonts().AddFontFromMemoryTTFV(
|
||||
fontData,
|
||||
fontDataSize,
|
||||
18,
|
||||
fontConfig,
|
||||
nil,
|
||||
@@ -237,10 +258,6 @@ func main() {
|
||||
defer sdl.StopTextInput(windowHandler)
|
||||
// fin on ImGui init
|
||||
|
||||
if err := vk.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sdlExtensions := sdl.VulkanGetInstanceExtensions()
|
||||
if len(sdlExtensions) == 0 {
|
||||
log.Fatal("sdlVulkanGetInstanceExtensions is 0")
|
||||
@@ -372,6 +389,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
doReconnect := func() {
|
||||
videoDomainStr, videoStr = normalizeFeedInput(videoDomainStr, videoStr)
|
||||
audioDomainStr, audioStr = normalizeFeedInput(audioDomainStr, audioStr)
|
||||
videoActive = videoStr != ""
|
||||
audioActive = audioStr != ""
|
||||
|
||||
@@ -465,6 +484,8 @@ func main() {
|
||||
displayedVideoWidth uint32 = placeholderWidth
|
||||
displayedVideoHeight uint32 = placeholderHeight
|
||||
displayedVideoStride uint32 = placeholderStride
|
||||
displayedVideoSource playback.FeedConfig
|
||||
hasDisplayedVideo bool
|
||||
|
||||
fps float64
|
||||
dropTracker videoDropTracker
|
||||
@@ -480,6 +501,8 @@ func main() {
|
||||
|
||||
// ImGui
|
||||
var (
|
||||
statsWindowWidth float32 = 460
|
||||
statsWindowHeight float32 = 550
|
||||
settingWindowWidth float32 = 700
|
||||
settingsWindowState bool = true
|
||||
)
|
||||
@@ -523,6 +546,7 @@ func main() {
|
||||
if err := r.RecreateSwapchain(); err != nil {
|
||||
if errors.Is(err, renderer.ErrMinimized) {
|
||||
resized = true
|
||||
paceFrame(frameStart)
|
||||
continue
|
||||
}
|
||||
panic(err)
|
||||
@@ -534,7 +558,7 @@ func main() {
|
||||
var shownSource playback.FeedConfig
|
||||
hasFrame := false
|
||||
|
||||
frameCtx, frameCancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
frameCtx, frameCancel := context.WithTimeout(ctx, videoPollInterval)
|
||||
pendingFrame, frameErr := videoBridge.Next(frameCtx)
|
||||
frameCancel()
|
||||
|
||||
@@ -570,6 +594,16 @@ func main() {
|
||||
}
|
||||
|
||||
snapshot, hasSnapshot := player.Controller.Snapshot()
|
||||
desiredVideo := desiredVideoFeed(snapshot, hasSnapshot)
|
||||
if !desiredVideo.Active ||
|
||||
desiredVideo.Domain != displayedVideoSource.Domain ||
|
||||
desiredVideo.UUID != displayedVideoSource.UUID {
|
||||
hasDisplayedVideo = false
|
||||
}
|
||||
if hasFrame {
|
||||
displayedVideoSource = shownSource
|
||||
hasDisplayedVideo = shouldShowVideo(desiredVideo, shownSource)
|
||||
}
|
||||
|
||||
// stats
|
||||
if hasFrame {
|
||||
@@ -592,8 +626,8 @@ func main() {
|
||||
if r != nil {
|
||||
gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height))
|
||||
if showStats {
|
||||
cimgui.SetNextWindowPos(cimgui.Vec2{X: 0, Y: 0})
|
||||
cimgui.SetNextWindowSize(cimgui.Vec2{X: 460, Y: 510})
|
||||
cimgui.SetNextWindowPos(cimgui.Vec2{X: float32(r.Extent().Width) - statsWindowWidth, Y: 0})
|
||||
cimgui.SetNextWindowSize(cimgui.Vec2{X: statsWindowWidth, Y: statsWindowHeight})
|
||||
cimgui.BeginV("Stats", &showStats, cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar)
|
||||
mediaStats := player.MediaStats.Snapshot()
|
||||
cimgui.SeparatorText("Video")
|
||||
@@ -717,7 +751,13 @@ func main() {
|
||||
drawFeedsSections := func() {
|
||||
cimgui.SeparatorText("Video")
|
||||
cimgui.InputTextWithHint("Video domain", "/dev/shm/mxl", &videoDomainStr, 0, nil)
|
||||
if cimgui.IsItemDeactivatedAfterEdit() {
|
||||
videoDomainStr, _ = normalizeFeedInput(videoDomainStr, "")
|
||||
}
|
||||
cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil)
|
||||
if cimgui.IsItemDeactivatedAfterEdit() {
|
||||
_, videoStr = normalizeFeedInput("", videoStr)
|
||||
}
|
||||
if videoActive {
|
||||
cimgui.SameLine()
|
||||
if cimgui.Button("Stop##video") {
|
||||
@@ -742,7 +782,13 @@ func main() {
|
||||
}
|
||||
cimgui.SeparatorText("Audio")
|
||||
cimgui.InputTextWithHint("Audio domain", "/dev/shm/mxl", &audioDomainStr, 0, nil)
|
||||
if cimgui.IsItemDeactivatedAfterEdit() {
|
||||
audioDomainStr, _ = normalizeFeedInput(audioDomainStr, "")
|
||||
}
|
||||
cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil)
|
||||
if cimgui.IsItemDeactivatedAfterEdit() {
|
||||
_, audioStr = normalizeFeedInput("", audioStr)
|
||||
}
|
||||
if audioActive {
|
||||
cimgui.SameLine()
|
||||
if cimgui.Button("Stop##audio") {
|
||||
@@ -811,6 +857,46 @@ func main() {
|
||||
} else {
|
||||
cimgui.Text("End behavior: stop")
|
||||
}
|
||||
cimgui.Text(fmt.Sprintf(
|
||||
"Failure behavior: %s",
|
||||
configuredPlaylist.OnFailure,
|
||||
))
|
||||
if retryPolicy.MaxAttempts == 0 {
|
||||
cimgui.Text("Retries: infinite")
|
||||
} else {
|
||||
cimgui.Text(fmt.Sprintf(
|
||||
"Attempts per entry: %d",
|
||||
retryPolicy.MaxAttempts,
|
||||
))
|
||||
}
|
||||
cimgui.Text(fmt.Sprintf(
|
||||
"Retry delay: %s to %s",
|
||||
retryPolicy.InitialDelay,
|
||||
retryPolicy.MaxDelay,
|
||||
))
|
||||
|
||||
if hasPlaylistSnapshot && playlistSnapshot.HasFailure {
|
||||
failure := playlistSnapshot.Failure
|
||||
name := failure.EntryName
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("Entry %d", failure.EntryIndex+1)
|
||||
}
|
||||
cimgui.SeparatorText("Last failure")
|
||||
cimgui.TextWrapped(fmt.Sprintf(
|
||||
"%s: %s failed",
|
||||
name,
|
||||
failure.Status.Unit,
|
||||
))
|
||||
cimgui.Text(fmt.Sprintf(
|
||||
"Attempts: %d | failed attempts: %d",
|
||||
failure.Status.Attempt,
|
||||
failure.Status.FailedAttempts,
|
||||
))
|
||||
cimgui.Text(fmt.Sprintf("Policy: %s", failure.Policy))
|
||||
if failure.Status.Err != nil {
|
||||
cimgui.TextWrapped(failure.Status.Err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
preview := "No entry selected"
|
||||
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
|
||||
@@ -1003,7 +1089,7 @@ func main() {
|
||||
}
|
||||
|
||||
if settingsWindowState {
|
||||
cimgui.SetNextWindowPos(cimgui.Vec2{X: float32(r.Extent().Width) - settingWindowWidth, Y: 0})
|
||||
cimgui.SetNextWindowPos(cimgui.Vec2{X: 0, Y: 0})
|
||||
cimgui.SetNextWindowSize(cimgui.Vec2{X: settingWindowWidth, Y: float32(r.Extent().Height)})
|
||||
if cimgui.BeginV("Settings & Info", &settingsWindowState, cimgui.WindowFlagsNone) {
|
||||
drawSettingsContents()
|
||||
@@ -1017,11 +1103,13 @@ func main() {
|
||||
displayedVideoWidth,
|
||||
displayedVideoHeight,
|
||||
displayedVideoStride,
|
||||
hasDisplayedVideo,
|
||||
)
|
||||
if errors.Is(err, renderer.ErrOutOfDate) {
|
||||
if rerr := r.RecreateSwapchain(); rerr != nil {
|
||||
if errors.Is(rerr, renderer.ErrMinimized) {
|
||||
resized = true
|
||||
paceFrame(frameStart)
|
||||
continue
|
||||
}
|
||||
panic(rerr)
|
||||
|
||||
@@ -14,6 +14,13 @@ type playlistFile struct {
|
||||
Entries []playlistFileEntry `json:"entries"`
|
||||
Loop bool `json:"loop"`
|
||||
OnFailure string `json:"on_failure"`
|
||||
Retry *playlistFileRetry `json:"retry"`
|
||||
}
|
||||
|
||||
type playlistFileRetry struct {
|
||||
MaxAttempts *int `json:"max_attempts"`
|
||||
InitialDelay string `json:"initial_delay"`
|
||||
MaxDelay string `json:"max_delay"`
|
||||
}
|
||||
|
||||
type playlistFileEntry struct {
|
||||
@@ -73,6 +80,13 @@ func decodePlaylistFile(reader io.Reader) (playback.Playlist, error) {
|
||||
"on_failure %q: %w", file.OnFailure, playback.ErrPlaylistFailurePolicy,
|
||||
)
|
||||
}
|
||||
if file.Retry != nil {
|
||||
retry, err := decodePlaylistRetry(*file.Retry)
|
||||
if err != nil {
|
||||
return playback.Playlist{}, err
|
||||
}
|
||||
playlist.Retry = &retry
|
||||
}
|
||||
for index, entry := range file.Entries {
|
||||
duration := time.Duration(0)
|
||||
if entry.Duration != "" {
|
||||
@@ -103,6 +117,37 @@ func decodePlaylistFile(reader io.Reader) (playback.Playlist, error) {
|
||||
return playlist, nil
|
||||
}
|
||||
|
||||
func decodePlaylistRetry(file playlistFileRetry) (playback.RetryPolicy, error) {
|
||||
retry := playback.RetryPolicy{
|
||||
InitialDelay: initialRetryDelay,
|
||||
MaxDelay: maxRetryDelay,
|
||||
}
|
||||
if file.MaxAttempts != nil {
|
||||
retry.MaxAttempts = *file.MaxAttempts
|
||||
}
|
||||
var err error
|
||||
if file.InitialDelay != "" {
|
||||
retry.InitialDelay, err = time.ParseDuration(file.InitialDelay)
|
||||
if err != nil {
|
||||
return playback.RetryPolicy{}, fmt.Errorf(
|
||||
"retry initial_delay %q: %w", file.InitialDelay, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
if file.MaxDelay != "" {
|
||||
retry.MaxDelay, err = time.ParseDuration(file.MaxDelay)
|
||||
if err != nil {
|
||||
return playback.RetryPolicy{}, fmt.Errorf(
|
||||
"retry max_delay %q: %w", file.MaxDelay, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err := retry.Validate(); err != nil {
|
||||
return playback.RetryPolicy{}, fmt.Errorf("validate playlist retry: %w", err)
|
||||
}
|
||||
return retry, nil
|
||||
}
|
||||
|
||||
func playlistFileFeedToPlayback(feed *playlistFileFeed) playback.PlaylistFeed {
|
||||
if feed == nil {
|
||||
return playback.PlaylistFeed{}
|
||||
|
||||
@@ -91,6 +91,41 @@ func TestDecodePlaylistFileAllowsEmptyPlaylist(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePlaylistFileRetry(t *testing.T) {
|
||||
input := `{
|
||||
"retry": {
|
||||
"max_attempts": 3,
|
||||
"initial_delay": "250ms",
|
||||
"max_delay": "2s"
|
||||
},
|
||||
"entries": []
|
||||
}`
|
||||
got, err := decodePlaylistFile(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("decodePlaylistFile() error = %v", err)
|
||||
}
|
||||
if got.Retry == nil {
|
||||
t.Fatal("decodePlaylistFile() retry = nil")
|
||||
}
|
||||
want := playback.RetryPolicy{
|
||||
MaxAttempts: 3, InitialDelay: 250 * time.Millisecond, MaxDelay: 2 * time.Second,
|
||||
}
|
||||
if *got.Retry != want {
|
||||
t.Fatalf("retry = %#v, want %#v", *got.Retry, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePlaylistFileRetryDefaults(t *testing.T) {
|
||||
got, err := decodePlaylistFile(strings.NewReader(`{"retry":{},"entries":[]}`))
|
||||
if err != nil {
|
||||
t.Fatalf("decodePlaylistFile() error = %v", err)
|
||||
}
|
||||
if got.Retry == nil || got.Retry.MaxAttempts != 0 ||
|
||||
got.Retry.InitialDelay != initialRetryDelay || got.Retry.MaxDelay != maxRetryDelay {
|
||||
t.Fatalf("retry = %#v", got.Retry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -103,6 +138,10 @@ func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) {
|
||||
{name: "unknown field", input: `{"unknown": true}`, wantText: "unknown field"},
|
||||
{name: "multiple roots", input: `{"entries": []} {"entries": []}`, wantText: "multiple root values"},
|
||||
{name: "invalid failure policy", input: `{"on_failure":"skip","entries":[]}`, wantErr: playback.ErrPlaylistFailurePolicy},
|
||||
{name: "negative retry attempts", input: `{"retry":{"max_attempts":-1},"entries":[]}`, wantErr: playback.ErrInvalidMaxAttempts},
|
||||
{name: "invalid initial delay", input: `{"retry":{"initial_delay":"soon"},"entries":[]}`, wantText: "retry initial_delay"},
|
||||
{name: "invalid maximum delay", input: `{"retry":{"max_delay":"later"},"entries":[]}`, wantText: "retry max_delay"},
|
||||
{name: "invalid retry range", input: `{"retry":{"initial_delay":"2s","max_delay":"1s"},"entries":[]}`, wantErr: playback.ErrInvalidRetryRange},
|
||||
{
|
||||
name: "invalid duration",
|
||||
input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"duration":"later"}]}`,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"mxl-player/internal/playback"
|
||||
)
|
||||
|
||||
const playlistReadinessInterval = 10 * time.Millisecond
|
||||
const playlistEventInterval = 10 * time.Millisecond
|
||||
|
||||
var (
|
||||
ErrPlayerPlaybackRequired = errors.New("player playback is required")
|
||||
@@ -19,9 +19,13 @@ var (
|
||||
|
||||
type playerPlaylist struct {
|
||||
Controller *playback.PlaylistController
|
||||
Coordinator *playback.PlaylistReadinessCoordinator
|
||||
Commands chan playback.PlaylistCommand
|
||||
Readiness chan playback.PlaylistReadiness
|
||||
|
||||
// commands is written by GUI-facing methods and consumed by Controller.
|
||||
// events is written by coordinator and consumed by Controller. Run owns
|
||||
// both goroutine lifecycles; cancellation replaces channel closing.
|
||||
coordinator *playback.PlaylistEventCoordinator
|
||||
commands chan playback.PlaylistCommand
|
||||
events chan playback.PlaylistEvent
|
||||
}
|
||||
|
||||
func newPlayerPlaylist(
|
||||
@@ -40,7 +44,7 @@ func newPlayerPlaylist(
|
||||
}
|
||||
|
||||
commands := make(chan playback.PlaylistCommand, 32)
|
||||
readiness := make(chan playback.PlaylistReadiness, 8)
|
||||
events := make(chan playback.PlaylistEvent, 8)
|
||||
controller, err := playback.NewPlaylistController(
|
||||
playlist,
|
||||
retry,
|
||||
@@ -49,12 +53,12 @@ func newPlayerPlaylist(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
coordinator, err := playback.NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := playback.NewPlaylistEventCoordinator(
|
||||
controller,
|
||||
player.Controller,
|
||||
player.Status,
|
||||
readiness,
|
||||
playlistReadinessInterval,
|
||||
events,
|
||||
playlistEventInterval,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -62,9 +66,9 @@ func newPlayerPlaylist(
|
||||
|
||||
return &playerPlaylist{
|
||||
Controller: controller,
|
||||
Coordinator: coordinator,
|
||||
Commands: commands,
|
||||
Readiness: readiness,
|
||||
coordinator: coordinator,
|
||||
commands: commands,
|
||||
events: events,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -74,10 +78,10 @@ func (p *playerPlaylist) Run(ctx context.Context) error {
|
||||
|
||||
results := make(chan error, 2)
|
||||
go func() {
|
||||
results <- p.Controller.Run(runCtx, p.Commands, p.Readiness)
|
||||
results <- p.Controller.Run(runCtx, p.commands, p.events)
|
||||
}()
|
||||
go func() {
|
||||
results <- p.Coordinator.Run(runCtx)
|
||||
results <- p.coordinator.Run(runCtx)
|
||||
}()
|
||||
|
||||
first := <-results
|
||||
@@ -121,7 +125,7 @@ func (p *playerPlaylist) Resume() bool {
|
||||
|
||||
func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool {
|
||||
select {
|
||||
case p.Commands <- command:
|
||||
case p.commands <- command:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -104,10 +104,10 @@ func TestNewPlayerPlaylistWiresComponents(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
||||
}
|
||||
if runtime.Controller == nil || runtime.Coordinator == nil {
|
||||
if runtime.Controller == nil || runtime.coordinator == nil {
|
||||
t.Fatalf("runtime components = %#v", runtime)
|
||||
}
|
||||
if runtime.Commands == nil || runtime.Readiness == nil {
|
||||
if runtime.commands == nil || runtime.events == nil {
|
||||
t.Fatalf("runtime channels = %#v", runtime)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func TestPlayerPlaylistNavigationHelpers(t *testing.T) {
|
||||
if !test.send() {
|
||||
t.Fatal("navigation helper returned false")
|
||||
}
|
||||
if got := <-runtime.Commands; got != test.want {
|
||||
if got := <-runtime.commands; got != test.want {
|
||||
t.Fatalf("navigation command = %#v, want %#v", got, test.want)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func TestPlayerPlaylistNavigationQueueFull(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
||||
}
|
||||
for range cap(runtime.Commands) {
|
||||
for range cap(runtime.commands) {
|
||||
if !runtime.Next() {
|
||||
t.Fatal("queue filled before reaching capacity")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import "mxl-player/internal/playback"
|
||||
|
||||
func desiredVideoFeed(
|
||||
snapshot playback.SessionSnapshot,
|
||||
available bool,
|
||||
) playback.FeedConfig {
|
||||
if !available {
|
||||
return playback.FeedConfig{}
|
||||
}
|
||||
switch snapshot.Plan.Topology {
|
||||
case playback.TopologyIndependent:
|
||||
if snapshot.Plan.Video.Active {
|
||||
return snapshot.Plan.Video
|
||||
}
|
||||
case playback.TopologySynchronized:
|
||||
if snapshot.Plan.Sync.Active() {
|
||||
return snapshot.Plan.Sync.Video
|
||||
}
|
||||
}
|
||||
return playback.FeedConfig{}
|
||||
}
|
||||
|
||||
func shouldShowVideo(
|
||||
desired playback.FeedConfig,
|
||||
delivered playback.FeedConfig,
|
||||
) bool {
|
||||
return desired.Active && sameVideoSource(desired, delivered)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"mxl-player/internal/playback"
|
||||
)
|
||||
|
||||
func TestDesiredVideoFeed(t *testing.T) {
|
||||
video := playback.FeedConfig{Domain: "/video", UUID: "video", Active: true}
|
||||
tests := []struct {
|
||||
name string
|
||||
available bool
|
||||
plan playback.SessionPlan
|
||||
want playback.FeedConfig
|
||||
}{
|
||||
{name: "snapshot unavailable"},
|
||||
{name: "idle", available: true, plan: playback.SessionPlan{Topology: playback.TopologyIdle}},
|
||||
{name: "audio only", available: true, plan: playback.SessionPlan{Topology: playback.TopologyIndependent}},
|
||||
{name: "independent video", available: true, plan: playback.SessionPlan{Topology: playback.TopologyIndependent, Video: video}, want: video},
|
||||
{name: "synchronized video", available: true, plan: playback.SessionPlan{Topology: playback.TopologySynchronized, Sync: playback.SyncPairConfig{Video: video, Audio: playback.FeedConfig{Active: true}}}, want: video},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := desiredVideoFeed(playback.SessionSnapshot{Plan: test.plan}, test.available)
|
||||
if got != test.want {
|
||||
t.Fatalf("desiredVideoFeed() = %#v, want %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldShowVideoRequiresDesiredSource(t *testing.T) {
|
||||
desired := playback.FeedConfig{Domain: "/video", UUID: "video", Active: true}
|
||||
if !shouldShowVideo(desired, desired) {
|
||||
t.Fatal("matching active video was hidden")
|
||||
}
|
||||
if shouldShowVideo(playback.FeedConfig{}, desired) {
|
||||
t.Fatal("video was shown without an active desired feed")
|
||||
}
|
||||
other := desired
|
||||
other.UUID = "other"
|
||||
if shouldShowVideo(desired, other) {
|
||||
t.Fatal("frame from old source was shown")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gstreamer1.0-x \
|
||||
gstreamer1.0-tools \
|
||||
gstreamer1.0-plugins-base \
|
||||
gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad \
|
||||
gstreamer1.0-libav \
|
||||
libgstreamer1.0-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
libstdc++6 \
|
||||
fontconfig \
|
||||
fonts-dejavu-core \
|
||||
&& fc-cache -f \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY build/Linux-GCC-Release /mxl
|
||||
|
||||
RUN cp -a /mxl/lib/libmxl.so* /usr/local/lib/ \
|
||||
&& ldconfig \
|
||||
&& install -m 0755 \
|
||||
/mxl/tools/mxl-gst/mxl-gst-testsrc \
|
||||
/mxl/tools/mxl-gst/mxl-gst-looping-filesrc \
|
||||
/usr/local/bin/ \
|
||||
&& mkdir -p /flow-configs /dev/shm/mxl \
|
||||
&& cp -a /mxl/lib/tests/data/. /flow-configs/ \
|
||||
&& useradd --uid 1000 --create-home --shell /usr/sbin/nologin mxl \
|
||||
&& mkdir -p /home/mxl/.cache/fontconfig \
|
||||
&& chown -R mxl:mxl /home/mxl /dev/shm/mxl
|
||||
|
||||
ENV HOME=/home/mxl \
|
||||
XDG_CACHE_HOME=/home/mxl/.cache
|
||||
|
||||
USER mxl
|
||||
WORKDIR /flow-configs
|
||||
|
||||
CMD ["mxl-gst-testsrc", "--help"]
|
||||
@@ -0,0 +1,4 @@
|
||||
## build
|
||||
docker build --no-cache -f ./docker/gst-tools/Dockerfile -t mxl-gst-tools ~/extra/mxl/
|
||||
## run
|
||||
docker run --rm -it --mount type=bind,src=/dev/shm/mxl-2,dst=/dev/shm/mxl mxl-gst-tools mxl-gst-testsrc -d /dev/shm/mxl -v /flow-configs/v210_flow.json -a /flow-configs/audio_flow.json
|
||||
@@ -0,0 +1,8 @@
|
||||
5fbec3b1-1b0f-417d-9059-8b94a47197ed
|
||||
5fbec3b1-1b0f-417d-9059-8b94a47197ec
|
||||
|
||||
5fbec3b1-1b0f-417d-9059-8b94a47197ef
|
||||
5fbec3b1-1b0f-417d-9059-8b94a47197eb
|
||||
|
||||
2618979d-76a5-45e0-83cb-0f192978d1cd
|
||||
9d2a041b-01cf-4ee4-bffa-188fe093c99b
|
||||
@@ -7,8 +7,10 @@ require (
|
||||
github.com/ebitengine/purego v0.10.2
|
||||
)
|
||||
|
||||
require github.com/qvest-digital/go-mxl v1.1.0-rc.1
|
||||
require github.com/qvest-digital/go-mxl v1.1.0-rc.2
|
||||
|
||||
require github.com/spf13/pflag v1.0.10
|
||||
|
||||
require github.com/AllenDang/cimgui-go v1.5.0
|
||||
|
||||
replace github.com/christerso/vulkan-go => github.com/JohannesItten/vulkan-go v0.0.0-20260903100450-2f1650464726
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
github.com/AllenDang/cimgui-go v1.5.0 h1:wnS4h1wWyE+3x59cHJnNSszka7pIR+pehim6ifOk9Rg=
|
||||
github.com/AllenDang/cimgui-go v1.5.0/go.mod h1:w7LE33Ex/ZOHqaFBWBHOIVMY4yLSwGLpJMUpX/fb5p4=
|
||||
github.com/christerso/vulkan-go v0.0.0-20260618152204-bff25e5b7646 h1:xfc6rFxs+U4XktxQa7jyPfg9g0xlb5STDsQn8sURsyo=
|
||||
github.com/christerso/vulkan-go v0.0.0-20260618152204-bff25e5b7646/go.mod h1:DIo6W2yFav1DflNKc0W0Oc7mecbdBg2btsaHhIYmwUA=
|
||||
github.com/JohannesItten/vulkan-go v0.0.0-20260903100450-2f1650464726 h1:4gRCj5RQNb9wczM4wmD/LORsHPq8EPZahikD6YGAbrQ=
|
||||
github.com/JohannesItten/vulkan-go v0.0.0-20260903100450-2f1650464726/go.mod h1:DIo6W2yFav1DflNKc0W0Oc7mecbdBg2btsaHhIYmwUA=
|
||||
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/qvest-digital/go-mxl v1.1.0-rc.1 h1:CUg29Q5c/JA6pqyA5gP3vu7JBr8lRXbOOxC+r0W572s=
|
||||
github.com/qvest-digital/go-mxl v1.1.0-rc.1/go.mod h1:cYzyT+S/AONytsKr/DozzFQP2OrNrg/JsQOSjvwn+Rk=
|
||||
github.com/qvest-digital/go-mxl v1.1.0-rc.2 h1:J1EiiOwy9cEZK1djv8uI3fFP2Oq228Y2Sft/y5sE6D4=
|
||||
github.com/qvest-digital/go-mxl v1.1.0-rc.2/go.mod h1:cYzyT+S/AONytsKr/DozzFQP2OrNrg/JsQOSjvwn+Rk=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Settings & Info]
|
||||
Pos=580,0
|
||||
Size=700,720
|
||||
Collapsed=0
|
||||
|
||||
[Window][Stats]
|
||||
Size=460,510
|
||||
Collapsed=0
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// uiFont contains the player UI font so the executable does not depend on a
|
||||
// machine-specific font installation.
|
||||
//
|
||||
//go:embed fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf
|
||||
var uiFont []byte
|
||||
|
||||
// PinUIFont returns the embedded font data and a release function. ImGui keeps
|
||||
// the supplied pointer until its font atlas is destroyed, so callers must keep
|
||||
// the data pinned and invoke release only after destroying the ImGui context.
|
||||
func PinUIFont() (data uintptr, size int32, release func()) {
|
||||
pinner := new(runtime.Pinner)
|
||||
pinner.Pin(&uiFont[0])
|
||||
|
||||
return uintptr(unsafe.Pointer(&uiFont[0])), int32(len(uiFont)), pinner.Unpin
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package assets
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEmbeddedUIFont(t *testing.T) {
|
||||
data, size, release := PinUIFont()
|
||||
defer release()
|
||||
|
||||
if data == 0 {
|
||||
t.Fatal("embedded UI font has a nil data pointer")
|
||||
}
|
||||
if size < 4 {
|
||||
t.Fatalf("embedded UI font is too small: %d bytes", size)
|
||||
}
|
||||
if string(uiFont[:4]) != "\x00\x01\x00\x00" {
|
||||
t.Fatalf("embedded UI font has unexpected TrueType signature % x", uiFont[:4])
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
# JetBrainsMonoNL Nerd Font Mono
|
||||
|
||||
`JetBrainsMonoNLNerdFontMono-Regular.ttf` is the regular, no-ligatures,
|
||||
monospaced variant of JetBrains Mono patched by the Nerd Fonts project.
|
||||
|
||||
Source: <https://github.com/ryanoasis/nerd-fonts>
|
||||
|
||||
The font is distributed under the SIL Open Font License 1.1. The complete
|
||||
license and copyright notice are included in `OFL.txt`.
|
||||
@@ -3,6 +3,8 @@ package imgui
|
||||
import (
|
||||
"time"
|
||||
|
||||
"mxl-player/internal/sdl"
|
||||
|
||||
cimgui "github.com/AllenDang/cimgui-go/imgui"
|
||||
)
|
||||
|
||||
@@ -17,9 +19,20 @@ func New() *Context {
|
||||
ctx := cimgui.CreateContext()
|
||||
cimgui.SetCurrentContext(ctx)
|
||||
io := cimgui.CurrentIO()
|
||||
cimgui.CurrentPlatformIO().SetClipboardHandler(sdlClipboardHandler{})
|
||||
return &Context{ctx: ctx, io: io}
|
||||
}
|
||||
|
||||
type sdlClipboardHandler struct{}
|
||||
|
||||
func (sdlClipboardHandler) GetClipboard() string {
|
||||
return sdl.GetClipboardText()
|
||||
}
|
||||
|
||||
func (sdlClipboardHandler) SetClipboard(text string) {
|
||||
sdl.SetClipboardText(text)
|
||||
}
|
||||
|
||||
func (c *Context) Destroy() {
|
||||
cimgui.DestroyContext()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ import (
|
||||
cimgui "github.com/AllenDang/cimgui-go/imgui"
|
||||
)
|
||||
|
||||
const (
|
||||
sdlKModShift uint16 = 0x0001 | 0x0002
|
||||
sdlKModCtrl uint16 = 0x0040 | 0x0080
|
||||
sdlKModAlt uint16 = 0x0100 | 0x0200
|
||||
sdlKModGUI uint16 = 0x0400 | 0x0800
|
||||
)
|
||||
|
||||
// SDL3 event (128 byte raw buffer) -> imgui
|
||||
func (c *Context) ProcessEvent(event *[128]byte) {
|
||||
eventType := *(*uint32)(unsafe.Pointer(&event[0]))
|
||||
@@ -15,7 +22,9 @@ func (c *Context) ProcessEvent(event *[128]byte) {
|
||||
switch eventType {
|
||||
case sdl.EventKeyDown, sdl.EventKeyUp:
|
||||
scancode := *(*uint32)(unsafe.Pointer(&event[24]))
|
||||
modifiers := *(*uint16)(unsafe.Pointer(&event[32]))
|
||||
down := eventType == sdl.EventKeyDown
|
||||
c.addKeyModifiers(modifiers)
|
||||
key := sdlScancodeToImGuiKey(scancode)
|
||||
if key >= 0 {
|
||||
c.io.AddKeyEvent(key, down)
|
||||
@@ -47,6 +56,13 @@ func (c *Context) ProcessEvent(event *[128]byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) addKeyModifiers(modifiers uint16) {
|
||||
c.io.AddKeyEvent(cimgui.ModCtrl, modifiers&sdlKModCtrl != 0)
|
||||
c.io.AddKeyEvent(cimgui.ModShift, modifiers&sdlKModShift != 0)
|
||||
c.io.AddKeyEvent(cimgui.ModAlt, modifiers&sdlKModAlt != 0)
|
||||
c.io.AddKeyEvent(cimgui.ModSuper, modifiers&sdlKModGUI != 0)
|
||||
}
|
||||
|
||||
func sdlScancodeToImGuiKey(scancode uint32) cimgui.Key {
|
||||
switch scancode {
|
||||
case 40: // SDL_SCANCODE_RETURN
|
||||
@@ -79,6 +95,10 @@ func sdlScancodeToImGuiKey(scancode uint32) cimgui.Key {
|
||||
return cimgui.KeyLeftAlt
|
||||
case 230: // SDL_SCANCODE_RALT
|
||||
return cimgui.KeyRightAlt
|
||||
case 227: // SDL_SCANCODE_LGUI
|
||||
return cimgui.KeyLeftSuper
|
||||
case 231: // SDL_SCANCODE_RGUI
|
||||
return cimgui.KeyRightSuper
|
||||
default:
|
||||
// Letters A-Z: scancodes 4-29 map to ImGui KeyA-KeyZ
|
||||
if scancode >= 4 && scancode <= 29 {
|
||||
|
||||
@@ -56,6 +56,7 @@ type Playlist struct {
|
||||
Entries []PlaylistEntry
|
||||
Loop bool
|
||||
OnFailure PlaylistFailurePolicy
|
||||
Retry *RetryPolicy
|
||||
}
|
||||
|
||||
func (f PlaylistFeed) IsConfigured() bool {
|
||||
@@ -95,6 +96,11 @@ func (p Playlist) Validate() error {
|
||||
if err := p.OnFailure.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.Retry != nil {
|
||||
if err := p.Retry.Validate(); err != nil {
|
||||
return fmt.Errorf("playlist retry: %w", err)
|
||||
}
|
||||
}
|
||||
for index, entry := range p.Entries {
|
||||
if err := entry.Validate(); err != nil {
|
||||
return fmt.Errorf("playlist entry %d: %w", index, err)
|
||||
|
||||
@@ -17,12 +17,9 @@ const (
|
||||
type PlaylistEvent struct {
|
||||
Revision uint64
|
||||
Kind PlaylistEventKind
|
||||
Failure Status
|
||||
}
|
||||
|
||||
// PlaylistReadiness is retained as an alias for callers that only publish
|
||||
// ready events. Its zero Kind is PlaylistEventReady.
|
||||
type PlaylistReadiness = PlaylistEvent
|
||||
|
||||
type playlistTimer interface {
|
||||
C() <-chan time.Time
|
||||
Stop() bool
|
||||
@@ -47,6 +44,15 @@ type PlaylistController struct {
|
||||
mu sync.RWMutex
|
||||
snapshot PlaylistSnapshot
|
||||
hasSnapshot bool
|
||||
failure PlaylistFailure
|
||||
}
|
||||
|
||||
type PlaylistFailure struct {
|
||||
EntryIndex int
|
||||
EntryName string
|
||||
Revision uint64
|
||||
Policy PlaylistFailurePolicy
|
||||
Status Status
|
||||
}
|
||||
|
||||
type PlaylistSnapshot struct {
|
||||
@@ -54,6 +60,8 @@ type PlaylistSnapshot struct {
|
||||
Entry PlaylistEntry
|
||||
Revision uint64
|
||||
Timing PlaylistTimingState
|
||||
Failure PlaylistFailure
|
||||
HasFailure bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -88,7 +96,7 @@ func NewPlaylistController(
|
||||
func (c *PlaylistController) Run(
|
||||
ctx context.Context,
|
||||
commands <-chan PlaylistCommand,
|
||||
readiness <-chan PlaylistReadiness,
|
||||
events <-chan PlaylistEvent,
|
||||
) error {
|
||||
state := PlaylistState{}
|
||||
revision := uint64(0)
|
||||
@@ -155,6 +163,7 @@ func (c *PlaylistController) Run(
|
||||
continue
|
||||
}
|
||||
if apply {
|
||||
c.clearFailure()
|
||||
stopTimer()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -169,16 +178,23 @@ func (c *PlaylistController) Run(
|
||||
state = next
|
||||
c.publish(state, revision, timing)
|
||||
|
||||
case ready, ok := <-readiness:
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
readiness = nil
|
||||
events = nil
|
||||
continue
|
||||
}
|
||||
if ready.Kind == PlaylistEventFailed {
|
||||
if ready.Revision != revision {
|
||||
if event.Kind == PlaylistEventFailed {
|
||||
if event.Revision != revision {
|
||||
continue
|
||||
}
|
||||
stopTimer()
|
||||
c.setFailure(PlaylistFailure{
|
||||
EntryIndex: state.CurrentIndex,
|
||||
EntryName: c.playlist.Entries[state.CurrentIndex].Name,
|
||||
Revision: revision,
|
||||
Policy: c.playlist.OnFailure,
|
||||
Status: event.Failure,
|
||||
})
|
||||
// A failed entry must not retain a live or apparently active
|
||||
// duration clock, even when the policy is to wait.
|
||||
timing = NewPlaylistTiming(revision, timing.Duration)
|
||||
@@ -208,8 +224,11 @@ func (c *PlaylistController) Run(
|
||||
c.publish(state, revision, timing)
|
||||
continue
|
||||
}
|
||||
if event.Kind != PlaylistEventReady {
|
||||
continue
|
||||
}
|
||||
if timing.Paused &&
|
||||
ready.Revision == timing.Revision &&
|
||||
event.Revision == timing.Revision &&
|
||||
timing.Duration > 0 &&
|
||||
!timing.Expired {
|
||||
timing.Ready = true
|
||||
@@ -218,7 +237,7 @@ func (c *PlaylistController) Run(
|
||||
}
|
||||
nextTiming, started := StartPlaylistTiming(
|
||||
timing,
|
||||
ready.Revision,
|
||||
event.Revision,
|
||||
c.now(),
|
||||
)
|
||||
if !started {
|
||||
@@ -289,11 +308,25 @@ func (c *PlaylistController) publish(
|
||||
Entry: entry,
|
||||
Revision: revision,
|
||||
Timing: timing,
|
||||
Failure: c.failure,
|
||||
HasFailure: c.failure.Revision != 0,
|
||||
}
|
||||
c.hasSnapshot = true
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *PlaylistController) setFailure(failure PlaylistFailure) {
|
||||
c.mu.Lock()
|
||||
c.failure = failure
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *PlaylistController) clearFailure() {
|
||||
c.mu.Lock()
|
||||
c.failure = PlaylistFailure{}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func stopPlaylistTimer(timer playlistTimer) {
|
||||
if timer == nil || timer.Stop() {
|
||||
return
|
||||
|
||||
@@ -68,9 +68,11 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
|
||||
return snapshot.Revision == 1
|
||||
})
|
||||
|
||||
readiness <- PlaylistReadiness{Revision: 0}
|
||||
readiness <- PlaylistEvent{Revision: 0}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1, Kind: PlaylistEventKind(99)}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
|
||||
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
@@ -79,7 +81,7 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
|
||||
if snapshot.Timing.Deadline != now.Add(10*time.Second) {
|
||||
t.Fatalf("deadline = %v, want %v", snapshot.Timing.Deadline, now.Add(10*time.Second))
|
||||
}
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
if timer.isStopped() {
|
||||
t.Fatal("timer stopped after duplicate readiness")
|
||||
@@ -101,7 +103,7 @@ func TestPlaylistControllerTimerExpiryAdvances(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
timer.fire(now.Add(10 * time.Second))
|
||||
|
||||
@@ -132,7 +134,7 @@ func TestPlaylistControllerLoopingTimerExpiryWraps(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Started
|
||||
@@ -160,7 +162,7 @@ func TestPlaylistControllerFinalExpiryStopsWithoutCommand(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Started
|
||||
@@ -192,7 +194,7 @@ func TestPlaylistControllerManualSelectionStopsOldTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
oldTimer := receiveFakePlaylistTimer(t, timers)
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
@@ -229,7 +231,7 @@ func TestPlaylistControllerZeroDurationDoesNotCreateTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
|
||||
close(commands)
|
||||
@@ -244,7 +246,7 @@ func TestPlaylistControllerCancellationStopsTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
cancel()
|
||||
if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) {
|
||||
@@ -262,7 +264,7 @@ func TestPlaylistControllerPauseAndResumeTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
oldTimer := receiveFakePlaylistTimer(t, timers)
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Started
|
||||
@@ -344,7 +346,7 @@ func TestPlaylistControllerRecordsQueuedReadinessWhilePaused(t *testing.T) {
|
||||
return snapshot.Timing.Paused
|
||||
})
|
||||
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Paused && snapshot.Timing.Ready
|
||||
})
|
||||
@@ -368,7 +370,7 @@ func startTimedPlaylistController(
|
||||
) (
|
||||
*PlaylistController,
|
||||
chan PlaylistCommand,
|
||||
chan PlaylistReadiness,
|
||||
chan PlaylistEvent,
|
||||
chan SessionCommand,
|
||||
chan *fakePlaylistTimer,
|
||||
time.Time,
|
||||
@@ -390,7 +392,7 @@ func startTimedPlaylistController(
|
||||
return timer
|
||||
}
|
||||
commands := make(chan PlaylistCommand, 16)
|
||||
readiness := make(chan PlaylistReadiness, 16)
|
||||
readiness := make(chan PlaylistEvent, 16)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- controller.Run(ctx, commands, readiness) }()
|
||||
|
||||
+43
-27
@@ -18,45 +18,45 @@ type PlaybackStatusSnapshotSource interface {
|
||||
SnapshotAll() PlaybackStatusSnapshot
|
||||
}
|
||||
|
||||
type playlistReadinessTicker interface {
|
||||
type playlistEventTicker interface {
|
||||
C() <-chan time.Time
|
||||
Stop()
|
||||
}
|
||||
|
||||
type playlistReadinessTickerFactory func(time.Duration) playlistReadinessTicker
|
||||
type playlistEventTickerFactory func(time.Duration) playlistEventTicker
|
||||
|
||||
type realPlaylistReadinessTicker struct {
|
||||
type realPlaylistEventTicker struct {
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func (t realPlaylistReadinessTicker) C() <-chan time.Time { return t.ticker.C }
|
||||
func (t realPlaylistReadinessTicker) Stop() { t.ticker.Stop() }
|
||||
func (t realPlaylistEventTicker) C() <-chan time.Time { return t.ticker.C }
|
||||
func (t realPlaylistEventTicker) Stop() { t.ticker.Stop() }
|
||||
|
||||
var (
|
||||
ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required")
|
||||
ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required")
|
||||
ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required")
|
||||
ErrPlaylistReadinessOutputRequired = errors.New("playlist readiness output channel is required")
|
||||
ErrPlaylistReadinessInterval = errors.New("playlist readiness interval must be positive")
|
||||
ErrPlaylistEventOutputRequired = errors.New("playlist event output channel is required")
|
||||
ErrPlaylistEventInterval = errors.New("playlist event interval must be positive")
|
||||
)
|
||||
|
||||
type PlaylistReadinessCoordinator struct {
|
||||
type PlaylistEventCoordinator struct {
|
||||
playlist PlaylistSnapshotSource
|
||||
session SessionSnapshotSource
|
||||
statuses PlaybackStatusSnapshotSource
|
||||
output chan<- PlaylistReadiness
|
||||
output chan<- PlaylistEvent
|
||||
interval time.Duration
|
||||
|
||||
newTicker playlistReadinessTickerFactory
|
||||
newTicker playlistEventTickerFactory
|
||||
}
|
||||
|
||||
func NewPlaylistReadinessCoordinator(
|
||||
func NewPlaylistEventCoordinator(
|
||||
playlist PlaylistSnapshotSource,
|
||||
session SessionSnapshotSource,
|
||||
statuses PlaybackStatusSnapshotSource,
|
||||
output chan<- PlaylistReadiness,
|
||||
output chan<- PlaylistEvent,
|
||||
interval time.Duration,
|
||||
) (*PlaylistReadinessCoordinator, error) {
|
||||
) (*PlaylistEventCoordinator, error) {
|
||||
if playlist == nil {
|
||||
return nil, ErrPlaylistSnapshotSourceRequired
|
||||
}
|
||||
@@ -67,25 +67,25 @@ func NewPlaylistReadinessCoordinator(
|
||||
return nil, ErrStatusSnapshotSourceRequired
|
||||
}
|
||||
if output == nil {
|
||||
return nil, ErrPlaylistReadinessOutputRequired
|
||||
return nil, ErrPlaylistEventOutputRequired
|
||||
}
|
||||
if interval <= 0 {
|
||||
return nil, ErrPlaylistReadinessInterval
|
||||
return nil, ErrPlaylistEventInterval
|
||||
}
|
||||
|
||||
return &PlaylistReadinessCoordinator{
|
||||
return &PlaylistEventCoordinator{
|
||||
playlist: playlist,
|
||||
session: session,
|
||||
statuses: statuses,
|
||||
output: output,
|
||||
interval: interval,
|
||||
newTicker: func(interval time.Duration) playlistReadinessTicker {
|
||||
return realPlaylistReadinessTicker{ticker: time.NewTicker(interval)}
|
||||
newTicker: func(interval time.Duration) playlistEventTicker {
|
||||
return realPlaylistEventTicker{ticker: time.NewTicker(interval)}
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
||||
func (c *PlaylistEventCoordinator) Run(ctx context.Context) error {
|
||||
ticker := c.newTicker(c.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -112,13 +112,14 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
statuses := c.statuses.SnapshotAll()
|
||||
if IsSessionFailed(sessionSnapshot, statuses) {
|
||||
if failure, failed := SessionFailureStatus(sessionSnapshot, statuses); failed {
|
||||
if playlistSnapshot.Revision == emittedFailedRevision {
|
||||
continue
|
||||
}
|
||||
failed := PlaylistEvent{
|
||||
Revision: playlistSnapshot.Revision,
|
||||
Kind: PlaylistEventFailed,
|
||||
Failure: failure,
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -138,7 +139,7 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
ready := PlaylistReadiness{Revision: playlistSnapshot.Revision}
|
||||
ready := PlaylistEvent{Revision: playlistSnapshot.Revision}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@@ -217,24 +218,39 @@ func IsSessionFailed(
|
||||
session SessionSnapshot,
|
||||
statuses PlaybackStatusSnapshot,
|
||||
) bool {
|
||||
_, failed := SessionFailureStatus(session, statuses)
|
||||
return failed
|
||||
}
|
||||
|
||||
func SessionFailureStatus(
|
||||
session SessionSnapshot,
|
||||
statuses PlaybackStatusSnapshot,
|
||||
) (Status, bool) {
|
||||
if statuses.Generation != session.Generation {
|
||||
return false
|
||||
return Status{}, false
|
||||
}
|
||||
|
||||
switch session.Plan.Topology {
|
||||
case TopologyIndependent:
|
||||
return (session.Plan.Video.Active && statusIsFailed(
|
||||
if session.Plan.Video.Active && statusIsFailed(
|
||||
statuses.Video, statuses.HasVideo, session.Generation, session.Plan.Video,
|
||||
)) || (session.Plan.Audio.Active && statusIsFailed(
|
||||
) {
|
||||
return statuses.Video, true
|
||||
}
|
||||
if session.Plan.Audio.Active && statusIsFailed(
|
||||
statuses.Audio, statuses.HasAudio, session.Generation, session.Plan.Audio,
|
||||
))
|
||||
) {
|
||||
return statuses.Audio, true
|
||||
}
|
||||
return Status{}, false
|
||||
case TopologySynchronized:
|
||||
return statuses.HasSync &&
|
||||
failed := statuses.HasSync &&
|
||||
statuses.Sync.Generation == session.Generation &&
|
||||
statuses.Sync.State == StateFailed &&
|
||||
sameSyncSource(statuses.Sync.Pair, session.Plan.Sync)
|
||||
return statuses.Sync, failed
|
||||
default:
|
||||
return false
|
||||
return Status{}, false
|
||||
}
|
||||
}
|
||||
|
||||
+37
-37
@@ -63,55 +63,55 @@ func (s *fakePlaybackStatusSnapshotSource) set(snapshot PlaybackStatusSnapshot)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
type fakePlaylistReadinessTicker struct {
|
||||
type fakePlaylistEventTicker struct {
|
||||
ch chan time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func newFakePlaylistReadinessTicker() *fakePlaylistReadinessTicker {
|
||||
return &fakePlaylistReadinessTicker{ch: make(chan time.Time, 16)}
|
||||
func newFakePlaylistEventTicker() *fakePlaylistEventTicker {
|
||||
return &fakePlaylistEventTicker{ch: make(chan time.Time, 16)}
|
||||
}
|
||||
|
||||
func (t *fakePlaylistReadinessTicker) C() <-chan time.Time { return t.ch }
|
||||
func (t *fakePlaylistReadinessTicker) Stop() {
|
||||
func (t *fakePlaylistEventTicker) C() <-chan time.Time { return t.ch }
|
||||
func (t *fakePlaylistEventTicker) Stop() {
|
||||
t.mu.Lock()
|
||||
t.stopped = true
|
||||
t.mu.Unlock()
|
||||
}
|
||||
func (t *fakePlaylistReadinessTicker) tick() { t.ch <- time.Now() }
|
||||
func (t *fakePlaylistReadinessTicker) isStopped() bool {
|
||||
func (t *fakePlaylistEventTicker) tick() { t.ch <- time.Now() }
|
||||
func (t *fakePlaylistEventTicker) isStopped() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.stopped
|
||||
}
|
||||
|
||||
func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
|
||||
func TestNewPlaylistEventCoordinatorValidatesDependencies(t *testing.T) {
|
||||
playlist := &fakePlaylistSnapshotSource{}
|
||||
session := &fakeSessionSnapshotSource{}
|
||||
statuses := &fakePlaybackStatusSnapshotSource{}
|
||||
output := make(chan PlaylistReadiness)
|
||||
output := make(chan PlaylistEvent)
|
||||
tests := []struct {
|
||||
name string
|
||||
playlist PlaylistSnapshotSource
|
||||
session SessionSnapshotSource
|
||||
statuses PlaybackStatusSnapshotSource
|
||||
output chan<- PlaylistReadiness
|
||||
output chan<- PlaylistEvent
|
||||
interval time.Duration
|
||||
wantErr error
|
||||
}{
|
||||
{name: "playlist", session: session, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrPlaylistSnapshotSourceRequired},
|
||||
{name: "session", playlist: playlist, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrSessionSnapshotSourceRequired},
|
||||
{name: "statuses", playlist: playlist, session: session, output: output, interval: time.Millisecond, wantErr: ErrStatusSnapshotSourceRequired},
|
||||
{name: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistReadinessOutputRequired},
|
||||
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistReadinessInterval},
|
||||
{name: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistEventOutputRequired},
|
||||
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistEventInterval},
|
||||
{name: "valid", playlist: playlist, session: session, statuses: statuses, output: output, interval: time.Millisecond},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
coordinator, err := NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := NewPlaylistEventCoordinator(
|
||||
test.playlist,
|
||||
test.session,
|
||||
test.statuses,
|
||||
@@ -119,7 +119,7 @@ func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
|
||||
test.interval,
|
||||
)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v, want %v", err, test.wantErr)
|
||||
t.Fatalf("NewPlaylistEventCoordinator() error = %v, want %v", err, test.wantErr)
|
||||
}
|
||||
if test.wantErr != nil && coordinator != nil {
|
||||
t.Fatalf("coordinator = %#v, want nil", coordinator)
|
||||
@@ -174,10 +174,10 @@ func TestPlaylistEntryMatchesSession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
func TestPlaylistEventCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
playlist, session, statuses := readyVideoSnapshots(1)
|
||||
output := make(chan PlaylistReadiness, 4)
|
||||
coordinator, ticker, cancel, result := startReadinessCoordinator(
|
||||
output := make(chan PlaylistEvent, 4)
|
||||
coordinator, ticker, cancel, result := startEventCoordinator(
|
||||
t,
|
||||
playlist,
|
||||
session,
|
||||
@@ -188,16 +188,16 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
ticker.tick()
|
||||
if got := receivePlaylistReadiness(t, output); got.Revision != 1 {
|
||||
if got := receivePlaylistEvent(t, output); got.Revision != 1 {
|
||||
t.Fatalf("readiness revision = %d, want 1", got.Revision)
|
||||
}
|
||||
ticker.tick()
|
||||
assertNoPlaylistReadiness(t, output)
|
||||
assertNoPlaylistEvent(t, output)
|
||||
|
||||
next := playlistSnapshotForVideo(2)
|
||||
playlist.set(next, true)
|
||||
ticker.tick()
|
||||
if got := receivePlaylistReadiness(t, output); got.Revision != 2 {
|
||||
if got := receivePlaylistEvent(t, output); got.Revision != 2 {
|
||||
t.Fatalf("readiness revision = %d, want 2", got.Revision)
|
||||
}
|
||||
|
||||
@@ -210,10 +210,10 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
func TestPlaylistEventCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
playlist, session, statuses := readyVideoSnapshots(1)
|
||||
output := make(chan PlaylistReadiness, 1)
|
||||
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output)
|
||||
output := make(chan PlaylistEvent, 1)
|
||||
_, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
@@ -256,7 +256,7 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
statuses.set(validStatuses.snapshot)
|
||||
test.mutate()
|
||||
ticker.tick()
|
||||
assertNoPlaylistReadiness(t, output)
|
||||
assertNoPlaylistEvent(t, output)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -264,10 +264,10 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
_ = waitForPlaylistResult(t, result)
|
||||
}
|
||||
|
||||
func TestPlaylistReadinessCoordinatorCancellationWhileBlockedSending(t *testing.T) {
|
||||
func TestPlaylistEventCoordinatorCancellationWhileBlockedSending(t *testing.T) {
|
||||
playlist, session, statuses := readyVideoSnapshots(1)
|
||||
output := make(chan PlaylistReadiness)
|
||||
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output)
|
||||
output := make(chan PlaylistEvent)
|
||||
_, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
|
||||
|
||||
ticker.tick()
|
||||
time.Sleep(time.Millisecond)
|
||||
@@ -322,15 +322,15 @@ func playlistSnapshotForVideo(revision uint64) PlaylistSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
func startReadinessCoordinator(
|
||||
func startEventCoordinator(
|
||||
t *testing.T,
|
||||
playlist PlaylistSnapshotSource,
|
||||
session SessionSnapshotSource,
|
||||
statuses PlaybackStatusSnapshotSource,
|
||||
output chan<- PlaylistReadiness,
|
||||
) (*PlaylistReadinessCoordinator, *fakePlaylistReadinessTicker, context.CancelFunc, <-chan error) {
|
||||
output chan<- PlaylistEvent,
|
||||
) (*PlaylistEventCoordinator, *fakePlaylistEventTicker, context.CancelFunc, <-chan error) {
|
||||
t.Helper()
|
||||
coordinator, err := NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := NewPlaylistEventCoordinator(
|
||||
playlist,
|
||||
session,
|
||||
statuses,
|
||||
@@ -338,28 +338,28 @@ func startReadinessCoordinator(
|
||||
time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v", err)
|
||||
t.Fatalf("NewPlaylistEventCoordinator() error = %v", err)
|
||||
}
|
||||
ticker := newFakePlaylistReadinessTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker }
|
||||
ticker := newFakePlaylistEventTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- coordinator.Run(ctx) }()
|
||||
return coordinator, ticker, cancel, result
|
||||
}
|
||||
|
||||
func receivePlaylistReadiness(t *testing.T, output <-chan PlaylistReadiness) PlaylistReadiness {
|
||||
func receivePlaylistEvent(t *testing.T, output <-chan PlaylistEvent) PlaylistEvent {
|
||||
t.Helper()
|
||||
select {
|
||||
case readiness := <-output:
|
||||
return readiness
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for playlist readiness")
|
||||
return PlaylistReadiness{}
|
||||
return PlaylistEvent{}
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoPlaylistReadiness(t *testing.T, output <-chan PlaylistReadiness) {
|
||||
func assertNoPlaylistEvent(t *testing.T, output <-chan PlaylistEvent) {
|
||||
t.Helper()
|
||||
select {
|
||||
case readiness := <-output:
|
||||
@@ -2,6 +2,7 @@ package playback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -145,14 +146,22 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
|
||||
t.Fatalf("NewPlaylistController() error = %v", err)
|
||||
}
|
||||
commands := make(chan PlaylistCommand, 2)
|
||||
events := make(chan PlaylistReadiness, 2)
|
||||
events := make(chan PlaylistEvent, 2)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- controller.Run(ctx, commands, events) }()
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
|
||||
<-sessions
|
||||
events <- PlaylistEvent{Revision: 1, Kind: PlaylistEventFailed}
|
||||
failureErr := errors.New("flow unavailable")
|
||||
events <- PlaylistEvent{
|
||||
Revision: 1,
|
||||
Kind: PlaylistEventFailed,
|
||||
Failure: Status{
|
||||
Unit: UnitVideo, State: StateFailed,
|
||||
Attempt: 2, FailedAttempts: 2, Err: failureErr,
|
||||
},
|
||||
}
|
||||
|
||||
if test.wantAdvance {
|
||||
select {
|
||||
@@ -160,16 +169,23 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("failure did not advance playlist")
|
||||
}
|
||||
snapshot, _ := controller.Snapshot()
|
||||
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.State.CurrentIndex == 1 && snapshot.Revision == 2
|
||||
})
|
||||
if snapshot.State.CurrentIndex != 1 || snapshot.Revision != 2 {
|
||||
t.Fatalf("snapshot = %#v, want index 1 revision 2", snapshot)
|
||||
}
|
||||
assertPlaylistFailure(t, snapshot, failureErr)
|
||||
} else {
|
||||
select {
|
||||
case command := <-sessions:
|
||||
t.Fatalf("unexpected session command: %#v", command)
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.HasFailure
|
||||
})
|
||||
assertPlaylistFailure(t, snapshot, failureErr)
|
||||
}
|
||||
|
||||
cancel()
|
||||
@@ -179,3 +195,16 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertPlaylistFailure(t *testing.T, snapshot PlaylistSnapshot, wantErr error) {
|
||||
t.Helper()
|
||||
if !snapshot.HasFailure {
|
||||
t.Fatal("snapshot has no playlist failure")
|
||||
}
|
||||
failure := snapshot.Failure
|
||||
if failure.EntryIndex != 0 || failure.EntryName != "first" ||
|
||||
failure.Status.Unit != UnitVideo || failure.Status.Attempt != 2 ||
|
||||
failure.Status.FailedAttempts != 2 || !errors.Is(failure.Status.Err, wantErr) {
|
||||
t.Fatalf("failure = %#v", failure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPlaylistFailurePipeline(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
policy PlaylistFailurePolicy
|
||||
wantAdvance bool
|
||||
}{
|
||||
{name: "wait retains failed entry", policy: PlaylistFailureWait},
|
||||
{name: "next advances failed entry", policy: PlaylistFailureNext, wantAdvance: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
playlist := Playlist{
|
||||
OnFailure: test.policy,
|
||||
Entries: []PlaylistEntry{
|
||||
{
|
||||
Name: "independent",
|
||||
Video: PlaylistFeed{Domain: "/video", UUID: "video"},
|
||||
Audio: PlaylistFeed{Domain: "/audio", UUID: "audio"},
|
||||
},
|
||||
{Name: "next", Video: PlaylistFeed{Domain: "/video", UUID: "next"}},
|
||||
},
|
||||
}
|
||||
h := startPlaylistPipeline(t, playlist)
|
||||
defer h.stop(t)
|
||||
|
||||
h.commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
|
||||
selected := receivePlaylistSession(t, h.sessions).Session
|
||||
waitForPlaylistSnapshot(t, h.controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Revision == 1
|
||||
})
|
||||
h.setSession(selected, 4)
|
||||
failureErr := errors.New("audio retries exhausted")
|
||||
h.statuses.set(PlaybackStatusSnapshot{
|
||||
Generation: 4,
|
||||
Audio: Status{
|
||||
Unit: UnitAudio, State: StateFailed, Generation: 4,
|
||||
Feed: selected.Audio, Attempt: 3, FailedAttempts: 3, Err: failureErr,
|
||||
},
|
||||
HasAudio: true,
|
||||
})
|
||||
h.ticker.tick()
|
||||
|
||||
if test.wantAdvance {
|
||||
next := receivePlaylistSession(t, h.sessions)
|
||||
if next.Session.Video.UUID != "next" || next.Session.Audio.IsConfigured() {
|
||||
t.Fatalf("advanced session = %#v", next.Session)
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case command := <-h.sessions:
|
||||
t.Fatalf("wait policy advanced with %#v", command)
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
snapshot := waitForPlaylistSnapshot(t, h.controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.HasFailure
|
||||
})
|
||||
if snapshot.Failure.Status.Unit != UnitAudio ||
|
||||
!errors.Is(snapshot.Failure.Status.Err, failureErr) {
|
||||
t.Fatalf("failure snapshot = %#v", snapshot.Failure)
|
||||
}
|
||||
wantIndex := 0
|
||||
if test.wantAdvance {
|
||||
wantIndex = 1
|
||||
}
|
||||
if snapshot.State.CurrentIndex != wantIndex {
|
||||
t.Fatalf("current index = %d, want %d", snapshot.State.CurrentIndex, wantIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistManualSelectionWhileFeedIsReconnecting(t *testing.T) {
|
||||
playlist := navigationPlaylist(false)
|
||||
h := startPlaylistPipeline(t, playlist)
|
||||
defer h.stop(t)
|
||||
|
||||
h.commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
|
||||
selected := receivePlaylistSession(t, h.sessions).Session
|
||||
waitForPlaylistSnapshot(t, h.controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Revision == 1
|
||||
})
|
||||
h.setSession(selected, 2)
|
||||
h.statuses.set(PlaybackStatusSnapshot{
|
||||
Generation: 2,
|
||||
Video: Status{
|
||||
Unit: UnitVideo, State: StateReconnecting, Generation: 2,
|
||||
Feed: selected.Video, Attempt: 2, FailedAttempts: 1,
|
||||
},
|
||||
HasVideo: true,
|
||||
})
|
||||
h.ticker.tick()
|
||||
|
||||
h.commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
next := receivePlaylistSession(t, h.sessions)
|
||||
if next.Session.Audio.UUID != "audio-2" || next.Session.Video.IsConfigured() {
|
||||
t.Fatalf("manual selection session = %#v", next.Session)
|
||||
}
|
||||
waitForPlaylistSnapshot(t, h.controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Revision == 2 && snapshot.State.CurrentIndex == 1
|
||||
})
|
||||
}
|
||||
|
||||
type playlistPipelineHarness struct {
|
||||
controller *PlaylistController
|
||||
commands chan PlaylistCommand
|
||||
sessions chan SessionCommand
|
||||
session *fakeSessionSnapshotSource
|
||||
statuses *fakePlaybackStatusSnapshotSource
|
||||
ticker *fakePlaylistEventTicker
|
||||
cancel context.CancelFunc
|
||||
results chan error
|
||||
}
|
||||
|
||||
func startPlaylistPipeline(t *testing.T, playlist Playlist) *playlistPipelineHarness {
|
||||
t.Helper()
|
||||
sessions := make(chan SessionCommand, 8)
|
||||
controller, err := NewPlaylistController(playlist, validPlaylistRetryPolicy(), sessions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events := make(chan PlaylistEvent, 8)
|
||||
session := &fakeSessionSnapshotSource{}
|
||||
statuses := &fakePlaybackStatusSnapshotSource{}
|
||||
coordinator, err := NewPlaylistEventCoordinator(
|
||||
controller, session, statuses, events, time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ticker := newFakePlaylistEventTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
|
||||
commands := make(chan PlaylistCommand, 8)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
results := make(chan error, 2)
|
||||
go func() { results <- controller.Run(ctx, commands, events) }()
|
||||
go func() { results <- coordinator.Run(ctx) }()
|
||||
return &playlistPipelineHarness{
|
||||
controller: controller, commands: commands, sessions: sessions,
|
||||
session: session, statuses: statuses, ticker: ticker,
|
||||
cancel: cancel, results: results,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *playlistPipelineHarness) setSession(desired SessionConfig, generation uint64) {
|
||||
plan, err := BuildSessionPlan(desired, func(FeedConfig, FeedConfig) bool { return false })
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.session.set(SessionSnapshot{Desired: desired, Plan: plan, Generation: generation}, true)
|
||||
}
|
||||
|
||||
func (h *playlistPipelineHarness) stop(t *testing.T) {
|
||||
t.Helper()
|
||||
h.cancel()
|
||||
for range 2 {
|
||||
select {
|
||||
case err := <-h.results:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("pipeline Run() error = %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("playlist pipeline did not stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,38 @@ func TestSessionControllerCancellationStopsAndJoinsRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionControllerStopAllWhileSessionIsActive(t *testing.T) {
|
||||
events := make(chan controllerEvent, 32)
|
||||
controller := newRecordingController(t, events)
|
||||
initial := validCommandSession()
|
||||
initial.SyncRequested = false
|
||||
commands := make(chan SessionCommand)
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- controller.Run(context.Background(), initial, commands) }()
|
||||
receiveIndependentStarts(t, events)
|
||||
|
||||
commands <- SessionCommand{Kind: CommandStopAll}
|
||||
stopped := map[Unit]bool{}
|
||||
for len(stopped) < 2 {
|
||||
event := receiveControllerEvent(t, events)
|
||||
if event.action != "stop" || (event.unit != UnitVideo && event.unit != UnitAudio) {
|
||||
t.Fatalf("unexpected stop event: %+v", event)
|
||||
}
|
||||
stopped[event.unit] = true
|
||||
}
|
||||
snapshot := waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
|
||||
return snapshot.Plan.Topology == TopologyIdle
|
||||
})
|
||||
if snapshot.Desired.Video.Active || snapshot.Desired.Audio.Active {
|
||||
t.Fatalf("stopped desired session = %#v", snapshot.Desired)
|
||||
}
|
||||
|
||||
close(commands)
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionControllerSnapshotTracksDesiredPlanAndGeneration(t *testing.T) {
|
||||
events := make(chan controllerEvent, 64)
|
||||
controller := newRecordingController(t, events)
|
||||
|
||||
@@ -57,6 +57,19 @@ func (b *VideoBridge) Next(
|
||||
}
|
||||
}
|
||||
|
||||
// TryNext returns a frame only when a producer is already waiting. It never
|
||||
// blocks the caller, allowing UI/render loops to run independently of video
|
||||
// frame cadence. A returned frame has the same completion requirements as
|
||||
// one returned by Next.
|
||||
func (b *VideoBridge) TryNext() (*PendingVideoFrame, bool) {
|
||||
select {
|
||||
case pending := <-b.requests:
|
||||
return pending, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (f *PendingVideoFrame) Complete(err error) {
|
||||
f.completeOnce.Do(func() {
|
||||
f.result <- err
|
||||
|
||||
@@ -144,3 +144,52 @@ func TestVideoBridgeNextHonorsCancellation(t *testing.T) {
|
||||
t.Fatalf("Next() error = %v, want %v", err, context.Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoBridgeTryNextReturnsImmediatelyWhenEmpty(t *testing.T) {
|
||||
bridge := NewVideoBridge()
|
||||
if pending, ok := bridge.TryNext(); ok || pending != nil {
|
||||
t.Fatalf("TryNext() = %#v, %t; want nil, false", pending, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoBridgeTryNextDeliversWithoutCopyAndRequiresCompletion(t *testing.T) {
|
||||
bridge := NewVideoBridge()
|
||||
frame := VideoFrame{Index: 9, Payload: []byte{1, 2, 3}}
|
||||
consumeResult := make(chan error, 1)
|
||||
started := make(chan struct{})
|
||||
go func() {
|
||||
close(started)
|
||||
consumeResult <- bridge.ConsumeVideo(context.Background(), frame)
|
||||
}()
|
||||
<-started
|
||||
|
||||
deadline := time.Now().Add(videoBridgeTestTimeout)
|
||||
var pending *PendingVideoFrame
|
||||
for pending == nil && time.Now().Before(deadline) {
|
||||
pending, _ = bridge.TryNext()
|
||||
if pending == nil {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
if pending == nil {
|
||||
t.Fatal("TryNext() did not receive waiting producer")
|
||||
}
|
||||
if &pending.Frame.Payload[0] != &frame.Payload[0] {
|
||||
t.Fatal("TryNext() copied borrowed payload")
|
||||
}
|
||||
select {
|
||||
case err := <-consumeResult:
|
||||
t.Fatalf("ConsumeVideo() returned before completion: %v", err)
|
||||
default:
|
||||
}
|
||||
|
||||
pending.Complete(nil)
|
||||
select {
|
||||
case err := <-consumeResult:
|
||||
if err != nil {
|
||||
t.Fatalf("ConsumeVideo() error = %v", err)
|
||||
}
|
||||
case <-time.After(videoBridgeTestTimeout):
|
||||
t.Fatal("ConsumeVideo() did not return after completion")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +455,12 @@ func validateFramePayload(
|
||||
|
||||
// DrawFrame acquires an image, records commands, submits, and presents.
|
||||
// Returns ErrOutOfDate if the swapchain needs recreation
|
||||
func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
||||
func (r *Renderer) DrawFrame(
|
||||
videoW uint32,
|
||||
videoH uint32,
|
||||
stride uint32,
|
||||
showVideo bool,
|
||||
) error {
|
||||
imageIndex, res := r.dev.AcquireNextImage(r.swapchain, r.imageAvailable, ^uint64(0))
|
||||
if res == vk.ErrorOutOfDateKHR || res == vk.SuboptimalKHR {
|
||||
return ErrOutOfDate
|
||||
@@ -483,7 +488,7 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
||||
r.fbs[imageIndex],
|
||||
vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent},
|
||||
[]vk.ClearValue{
|
||||
vk.ClearColor(0.0, 0.0, 0.0, 1.0),
|
||||
vk.ClearColor(0.025, 0.03, 0.04, 1.0),
|
||||
vk.ClearDepthStencil(1.0, 0),
|
||||
},
|
||||
)
|
||||
@@ -494,6 +499,7 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
||||
MinDepth: 0, MaxDepth: 1,
|
||||
})
|
||||
cmd.SetScissor(vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent})
|
||||
if showVideo {
|
||||
cmd.BindPipeline(r.decodePipeline)
|
||||
cmd.BindDescriptorSet(r.decodeLayout, 0, r.decodeSet)
|
||||
pc := PushConstants{
|
||||
@@ -505,6 +511,7 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
||||
}
|
||||
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
|
||||
cmd.Draw(3, 1, 0, 0)
|
||||
}
|
||||
if r.ImGuiDraw != nil {
|
||||
r.ImGuiDraw(cmd)
|
||||
}
|
||||
|
||||
+110
-2
@@ -1,7 +1,9 @@
|
||||
package sdl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
@@ -47,6 +49,8 @@ var (
|
||||
sdlCreateWindow func(title *byte, w, h int32, flags uint64) uintptr
|
||||
sdlDestroyWindow func(window uintptr)
|
||||
|
||||
sdlVulkanLoadLibrary func(path *byte) bool
|
||||
sdlVulkanUnloadLibrary func()
|
||||
sdlVulkanGetInstanceExtensions func(count *uint32) uintptr
|
||||
sdlVulkanCreateSurface func(window, instance, allocator uintptr, surface *uint64) bool
|
||||
|
||||
@@ -65,25 +69,99 @@ var (
|
||||
|
||||
sdlStartTextInput func(window uintptr)
|
||||
sdlStopTextInput func(window uintptr)
|
||||
sdlGetClipboardText func() uintptr
|
||||
sdlSetClipboardText func(text *byte) bool
|
||||
sdlFree func(memory uintptr)
|
||||
)
|
||||
|
||||
func libraryCandidates() ([]string, error) {
|
||||
return libraryCandidatesForOS(runtime.GOOS)
|
||||
}
|
||||
|
||||
func libraryCandidatesForOS(goos string) ([]string, error) {
|
||||
switch goos {
|
||||
case "linux":
|
||||
return []string{
|
||||
"libSDL3.so.0",
|
||||
"libSDL3.so",
|
||||
}, nil
|
||||
|
||||
case "darwin":
|
||||
return []string{
|
||||
"libSDL3.0.dylib",
|
||||
"libSDL3.dylib",
|
||||
}, nil
|
||||
|
||||
case "windows":
|
||||
return []string{
|
||||
"SDL3.dll",
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("SDL3: unsupported operating system %q", goos)
|
||||
}
|
||||
}
|
||||
|
||||
func openLibrary() (uintptr, error) {
|
||||
var candidates []string
|
||||
|
||||
if explicit := os.Getenv("SDL3_LIBRARY"); explicit != "" {
|
||||
candidates = append(candidates, explicit)
|
||||
}
|
||||
|
||||
defaults, err := libraryCandidates()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
candidates = append(candidates, defaults...)
|
||||
|
||||
var attempts []error
|
||||
|
||||
for _, name := range candidates {
|
||||
handle, err := purego.Dlopen(
|
||||
name,
|
||||
purego.RTLD_NOW|purego.RTLD_GLOBAL,
|
||||
)
|
||||
if err == nil && handle != 0 {
|
||||
return handle, nil
|
||||
}
|
||||
if err == nil {
|
||||
err = errors.New("loader returned a zero handle")
|
||||
}
|
||||
|
||||
attempts = append(attempts, fmt.Errorf("%s: %w", name, err))
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf(
|
||||
"SDL3: unable to load shared library: %w",
|
||||
errors.Join(attempts...),
|
||||
)
|
||||
}
|
||||
|
||||
var loaded = false
|
||||
|
||||
func Load() error {
|
||||
if loaded {
|
||||
return nil
|
||||
}
|
||||
h, err := purego.Dlopen("libSDL3.so.0", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||
|
||||
h, err := openLibrary()
|
||||
if err != nil {
|
||||
return fmt.Errorf("win: load SDL3: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
purego.RegisterLibFunc(&sdlInit, h, "SDL_Init")
|
||||
purego.RegisterLibFunc(&sdlQuit, h, "SDL_Quit")
|
||||
purego.RegisterLibFunc(&sdlGetError, h, "SDL_GetError")
|
||||
purego.RegisterLibFunc(&sdlCreateWindow, h, "SDL_CreateWindow")
|
||||
purego.RegisterLibFunc(&sdlDestroyWindow, h, "SDL_DestroyWindow")
|
||||
|
||||
// vulkan
|
||||
purego.RegisterLibFunc(&sdlVulkanLoadLibrary, h, "SDL_Vulkan_LoadLibrary")
|
||||
purego.RegisterLibFunc(&sdlVulkanUnloadLibrary, h, "SDL_Vulkan_UnloadLibrary")
|
||||
purego.RegisterLibFunc(&sdlVulkanGetInstanceExtensions, h, "SDL_Vulkan_GetInstanceExtensions")
|
||||
purego.RegisterLibFunc(&sdlVulkanCreateSurface, h, "SDL_Vulkan_CreateSurface")
|
||||
|
||||
purego.RegisterLibFunc(&sdlPollEvent, h, "SDL_PollEvent")
|
||||
purego.RegisterLibFunc(&sdlGetWindowSizeInPixels, h, "SDL_GetWindowSizeInPixels")
|
||||
purego.RegisterLibFunc(&sdlSetWindowFullscreen, h, "SDL_SetWindowFullscreen")
|
||||
@@ -98,6 +176,10 @@ func Load() error {
|
||||
// input
|
||||
purego.RegisterLibFunc(&sdlStartTextInput, h, "SDL_StartTextInput")
|
||||
purego.RegisterLibFunc(&sdlStopTextInput, h, "SDL_StopTextInput")
|
||||
purego.RegisterLibFunc(&sdlGetClipboardText, h, "SDL_GetClipboardText")
|
||||
purego.RegisterLibFunc(&sdlSetClipboardText, h, "SDL_SetClipboardText")
|
||||
purego.RegisterLibFunc(&sdlFree, h, "SDL_free")
|
||||
|
||||
loaded = true
|
||||
return nil
|
||||
}
|
||||
@@ -111,6 +193,14 @@ func CreateWindow(title string, w, h int32, flags uint64) uintptr {
|
||||
}
|
||||
func DestroyWindow(window uintptr) { sdlDestroyWindow(window) }
|
||||
|
||||
func VulkanLoadLibrary(path string) bool {
|
||||
return sdlVulkanLoadLibrary(cbytes(path))
|
||||
}
|
||||
|
||||
func VulkanUnloadLibrary() {
|
||||
sdlVulkanUnloadLibrary()
|
||||
}
|
||||
|
||||
func VulkanGetInstanceExtensions() []string {
|
||||
var count uint32
|
||||
vkExts := sdlVulkanGetInstanceExtensions(&count)
|
||||
@@ -197,3 +287,21 @@ func GetAudioPlaybackDevices() []AudioDevice {
|
||||
// Input wrappers
|
||||
func StartTextInput(window uintptr) { sdlStartTextInput(window) }
|
||||
func StopTextInput(window uintptr) { sdlStopTextInput(window) }
|
||||
|
||||
func GetClipboardText() string {
|
||||
text := sdlGetClipboardText()
|
||||
if text == 0 {
|
||||
return ""
|
||||
}
|
||||
result := cstr(text)
|
||||
sdlFree(text)
|
||||
return result
|
||||
}
|
||||
|
||||
func SetClipboardText(text string) bool {
|
||||
bytes := make([]byte, len(text)+1)
|
||||
copy(bytes, text)
|
||||
result := sdlSetClipboardText(&bytes[0])
|
||||
runtime.KeepAlive(bytes)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdl
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLibraryCandidatesForOS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "Linux",
|
||||
goos: "linux",
|
||||
want: []string{"libSDL3.so.0", "libSDL3.so"},
|
||||
},
|
||||
{
|
||||
name: "macOS",
|
||||
goos: "darwin",
|
||||
want: []string{"libSDL3.0.dylib", "libSDL3.dylib"},
|
||||
},
|
||||
{
|
||||
name: "Windows",
|
||||
goos: "windows",
|
||||
want: []string{"SDL3.dll"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := libraryCandidatesForOS(tt.goos)
|
||||
if err != nil {
|
||||
t.Fatalf("libraryCandidatesForOS(%q): %v", tt.goos, err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("libraryCandidatesForOS(%q) = %v, want %v", tt.goos, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryCandidatesForOSRejectsUnsupportedOS(t *testing.T) {
|
||||
got, err := libraryCandidatesForOS("plan9")
|
||||
if err == nil {
|
||||
t.Fatal("libraryCandidatesForOS(plan9) returned no error")
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("libraryCandidatesForOS(plan9) = %v, want nil", got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), `"plan9"`) {
|
||||
t.Fatalf("error %q does not identify the unsupported OS", err)
|
||||
}
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
VIDEO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197ed"
|
||||
AUDIO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197ec"
|
||||
VIDEO_URI=$1
|
||||
if [[ -z "${VIDEO_URI}" ]] then
|
||||
VIDEO_URI="${HOME}/Videos/test-vid/timelapse_spb.ts"
|
||||
fi
|
||||
export GST_PLUGIN_PATH="${HOME}/.gst-plugin:${GST_PLUGIN_PATH}"
|
||||
mxl-gst-looping-filesrc -d /dev/shm/mxl-2 -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null
|
||||
# sleep 5
|
||||
# kill -9 $(pidof "mxl-gst-looping-filesrc")
|
||||
# echo -e "\ngst-looping-filesrc killed"
|
||||
@@ -11,8 +11,20 @@ at any time. Synchronization is a runtime relationship between the slots, not a
|
||||
startup mode.
|
||||
|
||||
The design must leave a clean extension point for an `mxlfabrics` reader after
|
||||
the local MXL player is stable. It must also support a later playlist layer
|
||||
without moving playlist timing or selection into media readers.
|
||||
the local MXL player is stable. Playlist timing and selection must remain above
|
||||
media readers and playback workers.
|
||||
|
||||
## Current status
|
||||
|
||||
- Stages 0 through 11 are complete.
|
||||
- The reader-factory extension point from Stage 12 is complete; the
|
||||
`mxlfabrics` implementation remains future work.
|
||||
- Stage 13 is complete, including manual/timed navigation, looping,
|
||||
pause/resume, failure policies, playlist-level retry configuration, and GUI
|
||||
diagnostics.
|
||||
- Playlist lifecycle messages are explicit `PlaylistEvent` values produced by
|
||||
`PlaylistEventCoordinator`; runtime cancellation owns shutdown, so shared
|
||||
event channels are not closed by either endpoint.
|
||||
|
||||
## Required behaviour
|
||||
|
||||
@@ -61,7 +73,7 @@ without moving playlist timing or selection into media readers.
|
||||
- Stopping both members of a synchronized group closes the group atomically.
|
||||
- Stop commands cancel active reads and retry backoff promptly.
|
||||
|
||||
### Future playlist behaviour
|
||||
### Playlist behaviour
|
||||
|
||||
A playlist is an ordered list of playback entries. Each entry describes a
|
||||
complete desired session state and may contain:
|
||||
@@ -244,15 +256,15 @@ transition.
|
||||
4. Start independent workers for both configured slots.
|
||||
5. Let either worker begin playing without waiting for the other.
|
||||
|
||||
## Future playlist model
|
||||
## Playlist model
|
||||
|
||||
The exact public types can be chosen later, but the intended model is:
|
||||
The implemented model is conceptually:
|
||||
|
||||
```go
|
||||
type PlaylistEntry struct {
|
||||
Name string
|
||||
VideoUUID string
|
||||
AudioUUID string
|
||||
Video PlaylistFeed // domain + UUID, optional
|
||||
Audio PlaylistFeed // domain + UUID, optional
|
||||
SyncRequested bool
|
||||
Duration time.Duration
|
||||
}
|
||||
@@ -260,6 +272,8 @@ type PlaylistEntry struct {
|
||||
type Playlist struct {
|
||||
Entries []PlaylistEntry
|
||||
Loop bool
|
||||
OnFailure PlaylistFailurePolicy // wait or next
|
||||
Retry *RetryPolicy // optional playlist-wide override
|
||||
}
|
||||
```
|
||||
|
||||
@@ -505,7 +519,7 @@ Acceptance criteria:
|
||||
- Fake readers can drive all controller and supervisor tests.
|
||||
- Local MXL remains the reference implementation.
|
||||
|
||||
### Stage 13 — Simple playlist
|
||||
### Stage 13 — Simple playlist (complete)
|
||||
|
||||
Work:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
03.09.26
|
||||
|
||||
## Distrobox ubuntu:latest
|
||||
0) check that libmxl installed, PKG_CONFIG_PATH leads to valid libmxl.pc
|
||||
1) git clone repo
|
||||
2) install golang libsdl3-dev spdlog-dev (for some reasons sdplog required in libmxl.pc)
|
||||
3) cd go-mxl-player
|
||||
4) go build -o ./build/mxl-player ./cmd/mxl-player/
|
||||
|
||||
## Distrobox fedora:latest
|
||||
0) check that libmxl installed, PKG_CONFIG_PATH leads to valid libmxl.pc
|
||||
1) git clone repo
|
||||
2) dnf install golang SDL3-devel spdlog-devel g++
|
||||
3) cd go-mxl-player
|
||||
4) go build -o ./build/mxl-player ./cmd/mxl-player/
|
||||
|
||||
## MacOS test. Real hardware
|
||||
|
||||
0) check libmxl installed
|
||||
1) git clone repo
|
||||
2)
|
||||
3) cd go-mxl-player
|
||||
4) go build -o ./build/mxl-player ./cmd/mxl-player
|
||||
5) install_name_tool -add_rpath $PATH_TO_MXL_LIB ./build/mxl-player
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"loop": true,
|
||||
"on_failure": "next",
|
||||
"retry": {
|
||||
"max_attempts": 3,
|
||||
"initial_delay": "500ms",
|
||||
"max_delay": "5s"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"name": "timelapse",
|
||||
|
||||
@@ -15,19 +15,6 @@
|
||||
"sync": true,
|
||||
"duration": "10s"
|
||||
},
|
||||
{
|
||||
"name": "fail",
|
||||
"video": {
|
||||
"domain": "/dev/shm/mxl",
|
||||
"uuid": "6fbec3b1-1b0f-417d-9059-8b94a47197ed"
|
||||
},
|
||||
"audio": {
|
||||
"domain": "/dev/shm/mxl",
|
||||
"uuid": "6fbec3b1-1b0f-417d-9059-8b94a47197ec"
|
||||
},
|
||||
"sync": true,
|
||||
"duration": "10s"
|
||||
},
|
||||
{
|
||||
"name": "F1 Highlights",
|
||||
"video": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple/DTD PLIST 1.0//EN"
|
||||
"https://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>MXL Player</string>
|
||||
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>MXL Player</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.github.jitten.mxl-player</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>mxl-player</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.video</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,18 @@
|
||||
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>Resources/AppIcon.icns</key>
|
||||
<data>
|
||||
aOS6hffTaTtxQc0zhZI1xOsNz0I=
|
||||
</data>
|
||||
</dict>
|
||||
<key>files2</key>
|
||||
<dict>
|
||||
<key>Frameworks/libMoltenVK.dylib</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
ETd5I/t5G4xenebaPwHV0Q99lls=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"11377923fb791b8c5e9de6da3f01d5d10f7d965b"</string>
|
||||
</dict>
|
||||
<key>Frameworks/libSDL3.0.dylib</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
NNHvv93cCnUaCS7Jl7MJkRtCOKs=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"34d1efbfdddc0a751a092ec997b309911b4238ab"</string>
|
||||
</dict>
|
||||
<key>Frameworks/libmxl.1.dylib</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
MbqPNJYzYRFJ+VJyBwbTvnpRM0M=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"31ba8f349633611149f952720706d3be7a513343"</string>
|
||||
</dict>
|
||||
<key>Resources/AppIcon.icns</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
30ycN9D9rWpkLESaFOwycI8ufPkx8qO20cW46sVar2Q=
|
||||
</data>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>^Resources/</key>
|
||||
<true/>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^version.plist$</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>rules2</key>
|
||||
<dict>
|
||||
<key>.*\.dSYM($|/)</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>11</real>
|
||||
</dict>
|
||||
<key>^(.*/)?\.DS_Store$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>2000</real>
|
||||
</dict>
|
||||
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^Info\.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^PkgInfo$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^[^/]+$</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^embedded\.provisionprofile$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^version\.plist$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
5477ed0a585167d309a1c34c1985b0d7251bc72cc4f1a9b930c0936371e6c72e mxl-player-0.1.0-macos-arm64.zip
|
||||
Binary file not shown.
Reference in New Issue
Block a user