diff --git a/COPYING b/COPYING deleted file mode 100644 index f288702d..00000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - 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. - - - Copyright (C) - - 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 . - -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: - - Copyright (C) - 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 -. - - 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 -. diff --git a/lib/gst/COPYING b/COPYING-LGPL similarity index 99% rename from lib/gst/COPYING rename to COPYING-LGPL index 4362b491..8000a6fa 100644 --- a/lib/gst/COPYING +++ b/COPYING-LGPL @@ -55,7 +55,7 @@ modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - + Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a @@ -111,7 +111,7 @@ modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - + GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION @@ -158,7 +158,7 @@ Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - + 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 @@ -216,7 +216,7 @@ instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - + Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. @@ -267,7 +267,7 @@ Library will still fall under Section 6.) distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - + 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work @@ -329,7 +329,7 @@ restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - + 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined @@ -370,7 +370,7 @@ subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - + 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or @@ -422,7 +422,7 @@ conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - + 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is @@ -456,7 +456,7 @@ SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS - + How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest @@ -485,7 +485,8 @@ convey the exclusion of warranty; and each file should have at least the You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA Also add information on how to contact you by electronic and paper mail. @@ -494,7 +495,8 @@ school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. , 1 April 1990 Ty Coon, President of Vice diff --git a/bin/com.github.rafostar.Clapper.in b/bin/com.github.rafostar.Clapper.in deleted file mode 100644 index c0c65aab..00000000 --- a/bin/com.github.rafostar.Clapper.in +++ /dev/null @@ -1,9 +0,0 @@ -#!@GJS@ - -imports.package.init({ - name: '@PACKAGE_NAME@', - version: '@PACKAGE_VERSION@', - prefix: '@prefix@', - libdir: '@libdir@', -}); -imports.package.run(imports.src.main); diff --git a/bin/meson.build b/bin/meson.build deleted file mode 100644 index 03558745..00000000 --- a/bin/meson.build +++ /dev/null @@ -1,22 +0,0 @@ -bin_conf = configuration_data() - -bin_conf.set('GJS', find_program('gjs').path()) -bin_conf.set('PACKAGE_NAME', meson.project_name()) -bin_conf.set('PACKAGE_VERSION', meson.project_version()) -bin_conf.set('prefix', get_option('prefix')) -bin_conf.set('libdir', libdir) - -configure_file( - input: 'com.github.rafostar.Clapper.in', - output: 'com.github.rafostar.Clapper', - configuration: bin_conf, - install: true, - install_dir: bindir, - install_mode: 'rwxr-xr-x' -) - -clapper_symlink_cmd = 'ln -fs @0@ $DESTDIR@1@'.format( - 'com.github.rafostar.Clapper', - join_paths(bindir, 'clapper') -) -meson.add_install_script('sh', '-c', clapper_symlink_cmd) diff --git a/build-aux/meson/postinstall.py b/build-aux/meson/postinstall.py deleted file mode 100755 index 851743ad..00000000 --- a/build-aux/meson/postinstall.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -from os import environ, path -from subprocess import call - -prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') -sharedir = path.join(prefix, 'share') -destdir = environ.get('DESTDIR', '') - -# Package managers set this so we don't need to run -if not destdir: - print('Updating icon cache...') - call(['gtk4-update-icon-cache', '-qtf', path.join(sharedir, 'icons', 'hicolor')]) - - print('Updating mime database...') - call(['update-mime-database', path.join(sharedir, 'mime')]) - - print('Updating desktop database...') - call(['update-desktop-database', '-q', path.join(sharedir, 'applications')]) - - print('Compiling GSettings schemas...') - call(['glib-compile-schemas', path.join(sharedir, 'glib-2.0', 'schemas')]) diff --git a/css/styles.css b/css/styles.css deleted file mode 100644 index 26737a1c..00000000 --- a/css/styles.css +++ /dev/null @@ -1,426 +0,0 @@ -/* Defaults */ -scale marks { - color: currentColor; -} -radio { - margin-left: -2px; -} -scrolledwindow scrollbar.vertical slider { - min-height: 16px; -} - -/* Consistent scales color */ -scale trough highlight { - color: @accent_fg_color; - background-color: @accent_bg_color; -} -/* Consistent radio buttons color */ -.osd radio { - color: @accent_fg_color; - background-color: transparent; - background-image: none; -} -.osd radio:hover, -.osd radio:checked { - background-image: image(rgba(255,255,255,0.1)); -} -.osd radio:active { - background-image: image(rgba(255,255,255,0.3)); -} - -/* Adwaita OSD background color is unacceptable: - * https://gitlab.gnome.org/GNOME/libadwaita/-/issues/454 */ -box.osd, -.osd popover contents, -.osd popover arrow, -.osdheaderbar button { - background-color: rgba(38,38,38,0.78); -} -.osdheaderbar button:hover, -.osdheaderbar button:checked { - background-color: rgba(63,63,63,0.78); -} -.osdheaderbar button:active { - background-color: rgba(82,82,82,0.78); -} - -/* Adwaita is missing osd ListBox */ -.clapperplaylist { - background: none; -} -.clapperplaylist row { - border-radius: 5px; -} -.clapperplaylist row { - color: @theme_fg_color; -} -.clapperplaylist row button { - margin: 0px; - padding: 0px; - min-width: 28px; - min-height: 28px; -} -.fullscreen.tvmode .clapperplaylist row button { - min-width: 36px; - min-height: 36px; - margin-left: 2px; - margin-right: 2px; -} -.osd .clapperplaylist row image { - -gtk-icon-shadow: none; -} -.osdheaderbar { - background: transparent; -} -.osdheaderbar button { - border: transparent; -} -.linkseparator { - background: rgba(24,24,24,0.72); - min-width: 1px; -} -.linkedleft image { - margin-left: 2px; -} -.linkedright image { - margin-right: 2px; -} - -/* Flat popovers */ -popover arrow, -popover contents { - border-color: transparent; - box-shadow: none; -} -.popoverseparator separator { - background-color: @insensitive_fg_color; - margin-left: 3px; - margin-right: 3px; -} - -/* Rounded corners */ -.adwrounded.csd { - border-radius: 8px; -} -.adwrounded.fullscreen, -.adwrounded.maximized, -.adwrounded.tiled, -.adwrounded.tiled-top, -.adwrounded.tiled-left, -.adwrounded.tiled-right, -.adwrounded.tiled-bottom { - border-radius: 0px; -} -.roundedcorners { - border-radius: 8px; -} - -/* Reduce sliders size */ -scale trough slider { - min-height: 18px; - min-width: 18px; -} -.fullscreen.tvmode scale trough slider { - min-height: 20px; - min-width: 20px; -} - -.videowidget { - min-width: 320px; - min-height: 180px; -} - -.fullscreen.tvmode popover box { - text-shadow: none; - font-size: 21px; - font-weight: 500; -} -.clappercontrols { - margin-left: 2px; - margin-right: 2px; -} -.fullscreen.tvmode .clappercontrols { - margin-left: 1px; - margin-right: 1px; -} -.clappercontrolsbutton { - margin: 3px; - margin-left: 1px; - margin-right: 1px; -} -.fullscreen.tvmode .clappercontrolsbutton { - min-width: 32px; - min-height: 32px; - margin: 5px; - margin-left: 4px; - margin-right: 4px; -} -.clappercontrolsbutton.text-button { - padding-left: 4px; - padding-right: 4px; -} -.fullscreen.tvmode button image { - -gtk-icon-shadow: none; -} -.fullscreen.tvmode radio { - margin-left: 0px; - margin-right: 4px; - border: 2px solid; - min-width: 16px; - min-height: 16px; - box-shadow: none; -} - -/* Also affects popover buttons */ -.fullscreen.tvmode .clappercontrols button image { - -gtk-icon-size: 26px; -} -.clappercontrolsbutton.text-button label { - font-family: 'Cantarell', sans-serif; - font-variant-numeric: tabular-nums; - font-weight: 600; -} -.fullscreen.tvmode .clappercontrolsbutton.text-button label { - font-size: 22px; - text-shadow: none; -} - -/* Top Revealer */ -.fullscreen.tvmode .revealertopgrid { - font-family: 'Cantarell', sans-serif; -} -.fullscreen.tvmode .tvtitle { - font-size: 28px; - font-weight: 500; - text-shadow: none; -} -.fullscreen.tvmode .tvtime { - margin-top: -2px; - margin-bottom: -2px; - min-height: 4px; - font-size: 38px; - font-weight: 700; - font-variant-numeric: tabular-nums; -} -.fullscreen.tvmode .tvendtime { - margin-top: -4px; - margin-bottom: 2px; - min-height: 6px; - font-size: 24px; - font-weight: 600; - font-variant-numeric: tabular-nums; -} - -/* Position Scale */ -.positionscale { - margin: -2px; -} -.positionscale trough highlight { - min-height: 6px; -} -.fullscreen.tvmode .positionscale { - padding-left: 12px; - padding-right: 12px; -} -.fullscreen.tvmode .positionscale.fine-tune { - padding-left: 12px; - padding-right: 12px; -} -.fullscreen.tvmode .positionscale trough slider { - color: transparent; - background: transparent; - border-color: transparent; - box-shadow: none; - outline: none; -} -.positionscale mark indicator { - min-height: 6px; -} -.positionscale.fine-tune mark indicator { - min-height: 6px; -} -.fullscreen.tvmode .positionscale mark indicator { - min-height: 7px; - min-width: 2px; -} -.fullscreen.tvmode .positionscale.fine-tune mark indicator { - min-height: 7px; - min-width: 2px; -} -.positionscale marks.top { - margin-top: -6px; - margin-bottom: 4px; -} -.positionscale marks.bottom { - margin-top: 4px; - margin-bottom: -6px; -} -.fullscreen.tvmode .positionscale marks.top { - margin-bottom: 2px; -} -.fullscreen.tvmode .positionscale marks.bottom { - margin-top: 2px; -} -.fullscreen.tvmode .positionscale trough { - border-radius: 3px; -} -.fullscreen.tvmode .positionscale trough highlight { - border-radius: 3px; - min-height: 20px; -} -.fullscreen.tvmode .positionscale.fine-tune trough highlight { - border-radius: 3px; - min-height: 20px; -} - -/* Volume Scale */ -.volumescale { - margin: -2px; - margin-left: -8px; - margin-right: -6px; - min-height: 180px; -} -.fullscreen.tvmode .volumescale { - margin: 2px; - margin-left: -6px; - margin-right: -4px; - min-height: 260px; -} -.volumescale marks label { - margin-right: 4px; - margin-top: -4px; - margin-bottom: -6px; -} -.volumescale trough highlight { - min-width: 4px; -} -.fullscreen.tvmode .volumescale trough highlight { - min-width: 6px; -} -.overamp trough highlight { - color: @error_fg_color; - background-color: @error_bg_color; -} - -/* Elapsed Popover */ -.elapsedpopover { - min-width: 326px; -} -.fullscreen.tvmode .elapsedpopover { - min-width: 448px; -} -.elapsedpopover contents { - padding-bottom: 0px; -} -.speedscale { - margin-left: 4px; - margin-right: 4px; -} -.speedscale trough highlight { - min-height: 4px; -} -.fullscreen.tvmode .speedscale trough highlight { - min-height: 6px; -} - -.narrowbutton { - min-width: 8px; -} -@keyframes halfrotation { - to { transform: rotate(0.5turn); } -} -.halfrotate { - animation-name: halfrotation; - animation-duration: 200ms; - animation-delay: 280ms; - animation-timing-function: linear; - animation-fill-mode: forwards; - animation-iteration-count: 1; -} - -/* Chapters */ -.chapterlabel { - min-width: 32px; -} -.fullscreen.tvmode .chapterlabel { - min-width: 40px; - text-shadow: none; - font-size: 22px; - font-weight: 600; -} - -/* Open URI Dialog */ -.uridialogbox { - margin: 10px; -} - -/* Tweaks */ -.nobackground { - background: none; -} -.noborder { - border: none; -} -.controlsbox { - background: @popover_bg_color; -} -.gpufriendly { - box-shadow: -8px -8px transparent, 8px 8px transparent; -} -.fullscreen.gpufriendlyfs { - box-shadow: none; -} - -/* Error BG */ -.blackbackground { - background: black; -} - -/** SCALING LOW-RES **/ -.fullscreen.tvmode.lowres .clappercontrols button image { - -gtk-icon-size: 22px; -} -.fullscreen.tvmode.lowres .clappercontrolsbutton { - min-width: 28px; - min-height: 28px; -} -.fullscreen.tvmode.lowres .clappercontrolsbutton.text-button label { - font-size: 21px; -} -.fullscreen.tvmode.lowres .positionscale trough highlight { - min-height: 18px; -} -.fullscreen.tvmode.lowres .positionscale.fine-tune trough highlight { - min-height: 18px; -} -.fullscreen.tvmode.lowres popover box { - font-size: 19px; -} -.fullscreen.tvmode.lowres radio { - min-width: 15px; - min-height: 15px; -} -.fullscreen.tvmode.lowres .clapperplaylist row button { - min-width: 32px; - min-height: 32px; -} -.fullscreen.tvmode.lowres .tvtitle { - font-size: 26px; -} -.fullscreen.tvmode.lowres .tvtime { - font-size: 34px; -} -.fullscreen.tvmode.lowres .tvendtime { - font-size: 21px; -} -.fullscreen.tvmode.lowres .elapsedpopover { - min-width: 410px; -} -.fullscreen.tvmode.lowres .chapterlabel { - font-size: 21px; -} - -/** SCALING HI-RES **/ -.fullscreen.tvmode.hires .clappercontrols button image { - -gtk-icon-size: 24px; /* Sharpest on 2160p with scaling 2x */ -} diff --git a/data/com.github.rafostar.Clapper-symbolic.svg b/data/com.github.rafostar.Clapper-symbolic.svg deleted file mode 100644 index 67d4a6a3..00000000 --- a/data/com.github.rafostar.Clapper-symbolic.svg +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/data/com.github.rafostar.Clapper.data.gresource.xml b/data/com.github.rafostar.Clapper.data.gresource.xml deleted file mode 100644 index 354d467e..00000000 --- a/data/com.github.rafostar.Clapper.data.gresource.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - icons/play-symbolic.svg - icons/pause-symbolic.svg - icons/pip-in-symbolic.svg - icons/pip-out-symbolic.svg - - diff --git a/data/com.github.rafostar.Clapper.desktop b/data/com.github.rafostar.Clapper.desktop deleted file mode 100644 index d780a30f..00000000 --- a/data/com.github.rafostar.Clapper.desktop +++ /dev/null @@ -1,16 +0,0 @@ -[Desktop Entry] -Name=Clapper -GenericName=Multimedia Player -Comment=Play videos and music -Categories=GTK;GNOME;AudioVideo;Player;Video;TV; -MimeType=application/claps;application/mpeg4-iod;application/mpeg4-muxcodetable;application/mxf;application/ogg;application/ram;application/sdp;application/streamingmedia;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;application/x-extension-m4a;application/x-extension-mp4;application/x-flac;application/x-flash-video;application/x-matroska;application/x-ogg;application/x-streamingmedia;audio/3gpp;audio/3gpp2;audio/aac;audio/ac3;audio/amr;audio/amr-wb;audio/basic;audio/dv;audio/eac3;audio/flac;audio/m4a;audio/midi;audio/mp1;audio/mp2;audio/mp3;audio/mp4;audio/mpeg;audio/mpegurl;audio/mpg;audio/ogg;audio/opus;audio/scpls;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.rn-realaudio;audio/wav;audio/webm;audio/x-aac;audio/x-aiff;audio/x-ape;audio/x-flac;audio/x-gsm;audio/x-it;audio/x-m4a;audio/x-matroska;audio/x-mod;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-mpg;audio/x-ms-asf;audio/x-ms-wma;audio/x-musepack;audio/x-pn-aiff;audio/x-pn-au;audio/x-pn-realaudio;audio/x-pn-wav;audio/x-real-audio;audio/x-realaudio;audio/x-s3m;audio/x-scpls;audio/x-shorten;audio/x-speex;audio/x-tta;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;audio/x-wavpack;audio/x-xm;video/3gp;video/3gpp;video/3gpp2;video/divx;video/dv;video/fli;video/flv;video/mp2t;video/mp4;video/mp4v-es;video/mpeg;video/mpeg-system;video/msvideo;video/ogg;video/quicktime;video/vnd.mpegurl;video/vnd.rn-realvideo;video/webm;video/x-avi;video/x-flc;video/x-fli;video/x-flv;video/x-m4v;video/x-matroska;video/x-mpeg;video/x-mpeg-system;video/x-mpeg2;video/x-ms-asf;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-msvideo;video/x-nsv;video/x-ogm+ogg;video/x-theora;video/x-theora+ogg;x-content/audio-cdda;x-content/audio-player;x-content/video-dvd;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtmp;x-scheme-handler/rtp;x-scheme-handler/rtsp; -Exec=com.github.rafostar.Clapper %U -Icon=com.github.rafostar.Clapper -DBusActivatable=true -StartupNotify=true -Terminal=false -Type=Application -# Translators: Search terms to find this application. Do NOT translate the semicolons! -Keywords=Video;Movie;Film;Clip;Series;Player;Playlist;DVD;TV;Disc;Album;Music;GNOME;Clapper; -# Translators: Do NOT translate or transliterate this text (these are enum types)! -X-Purism-FormFactor=Workstation;Mobile; diff --git a/data/com.github.rafostar.Clapper.gschema.xml b/data/com.github.rafostar.Clapper.gschema.xml deleted file mode 100644 index ae6961ac..00000000 --- a/data/com.github.rafostar.Clapper.gschema.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - false - Automatically enter fullscreen when first file is loaded - - - false - Set custom volume value at startup - - - 100 - Custom initial volume value in percentage after startup - - - 0 - What to do after playback finishes - - - - - 0 - Mode used for seeking - - - 10 - Time amount to seek with single press of arrow keys - - - 0 - Unit ID to use with seeking value - - - false - Ask to resume unfinished video - - - '[]' - Data storing unfinished videos resume info - - - false - Auto stick floating window to all workspaces - - - - - 0 - Offset time for audio tracks relative to video (milliseconds) - - - - - "Sans 12" - The subtitles font description - - - - - false - Enable WebSocket server for remote playback control - - - 6446 - Listening port to use for incoming WebSocket connections - - - - - true - Enable to force the app to use dark theme variant - - - true - Enable rendering window shadows (only if theme has them) - - - - - '{}' - Custom values for GStreamer plugin ranking - - - false - Use playbin3 element instead of playbin2 - - - false - Use PipeWire for audio output - - - 1559 - Set PlayFlags for playbin - - - - - '[800, 490]' - Stores window size to restore on next launch - - - 1 - Stores last linear volume value to apply on startup - - - diff --git a/data/com.github.rafostar.Clapper.metainfo.xml b/data/com.github.rafostar.Clapper.metainfo.xml deleted file mode 100644 index c98ab6e0..00000000 --- a/data/com.github.rafostar.Clapper.metainfo.xml +++ /dev/null @@ -1,240 +0,0 @@ - - - com.github.rafostar.Clapper - CC0-1.0 - GPL-3.0-or-later - Clapper - Simple and modern GNOME media player - com.github.rafostar.Clapper - com.github.rafostar.Clapper.desktop - -

- Clapper is a GNOME media player built using GJS with GTK4 toolkit. - The media player is using GStreamer as a media backend and renders - everything via OpenGL. Player works natively on both Xorg and Wayland. - It also supports hardware acceleration through VA-API on AMD/Intel GPUs, - NVDEC on Nvidia and V4L2 on mobile devices. -

-

- The media player has an adaptive GUI. When viewing videos in "Windowed Mode", - Clapper will use mostly unmodified GTK widgets to match your OS look nicely. - When player enters "Fullscreen Mode" all GUI elements will become darker, bigger - and semi-transparent for your viewing comfort. It also has a "Floating Mode" which - displays only video on top of all other windows for a PiP-like viewing experience. - Mobile friendly transitions are also supported. -

-
- Rafał Dzięgiel - https://rafostar.github.io/clapper - https://github.com/Rafostar/clapper/issues - https://liberapay.com/Clapper - https://github.com/Rafostar/clapper/wiki - - AudioVideo - Video - - - - https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-windowed.png - - - https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-fullscreen.png - - - https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-floating.png - - - https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-mobile.png - - - - - -

Fixes:

-
    -
  • Fix time labels display on RTL languages
  • -
  • Improved GL/GLES context automatic selection
  • -
-

New translations:

-
    -
  • Hebrew
  • -
-
-
- - -

- A quick hotfix release. Fixes problems with new video sink on displays with non-100% scaling applied. - See 0.5.0 version release notes for full recent changelog. -

-
-
- - -

Changes:

-
    -
  • Includes and uses new, improved GStreamer video sink
  • -
  • All networking ported to libsoup3
  • -
  • A lot of cleanup, including removal of unfinished web application and old YT code
  • -
  • App now supports D-Bus launching (DBusActivatable)
  • -
  • Other small fixes
  • -
-

New translations:

-
    -
  • Arabic
  • -
  • Basque
  • -
  • French
  • -
  • Japanese
  • -
  • Swedish
  • -
  • Turkish
  • -
-
-
- - -

Fixes:

-
    -
  • Compatibility with more recent libadwaita versions
  • -
  • Toggle mute with M button alone
  • -
  • Allow handling YouTube with external GStreamer plugins
  • -
  • Fix catching errors when reading clipboard
  • -
  • Fix missing translator-credits
  • -
  • Fix missing gio-unix-2.0 dep
  • -
  • Fix playback pausing when entering fullscreen with touchscreen
  • -
  • Fix GST_PLUGIN_FEATURE_RANK env usage
  • -
  • Fix video/audio decoder change detection
  • -
  • Merge global video tags instead replacing them
  • -
  • Few other misc bug fixes
  • -
-

New translations:

-
    -
  • Chinese Simplified
  • -
  • Czech
  • -
  • Hungarian
  • -
  • Portuguese
  • -
  • Portuguese, Brazilian
  • -
  • Russian
  • -
  • Spanish
  • -
-
-
- - -

Changes:

-
    -
  • Now uses libadwaita
  • -
  • New and adaptive preferences window
  • -
  • Improved open URI dialog
  • -
  • Few small tweaks to fullscreen UI design
  • -
  • Show current video and audio decoders in popovers (easy way to check if HW accel is used)
  • -
  • Enabled NVDEC hardware acceleration by default (requires Nvidia proprietary drivers)
  • -
  • Added option to use PipeWire for audio output (experimental)
  • -
  • Added option to use playbin3 element (experimental)
  • -
  • New PiP icon from icon development kit
  • -
  • Improved performance on devices running OpenGL ES
  • -
  • Translations support
  • -
  • Various bug fixes
  • -
-

New keyboard shortcuts:

-
    -
  • Leave fullscreen with Escape key
  • -
  • Toggle mute with Ctrl+M
  • -
-

More touchscreen gestures:

-
    -
  • Toggle playback with a long press
  • -
  • Switch playlist items via double tap on screen side
  • -
-

New translations:

-
    -
  • Catalan
  • -
  • Dutch
  • -
  • German
  • -
  • Italian
  • -
  • Polish
  • -
-
-
- - -

Changes:

-
    -
  • Added MPRIS support
  • -
  • Added repeat modes: single video, whole playlist and shuffle
  • -
  • Support opening folders with media files
  • -
  • Append playlist items by holding Ctrl while doing Drag and Drop
  • -
  • Improved handling of keyboard shortcuts
  • -
  • Added more keyboard shortcuts
  • -
  • Added window that shows available keyboard shortcuts
  • -
  • Show black screen by default after playback (make showing last frame optional instead)
  • -
  • Added ability to export playlist to file
  • -
  • Improve handling of changing displays with different resolutions
  • -
  • Added support for EGL under x11 with GTK 4.3.1 or later
  • -
  • Added missing symbolic app icon
  • -
  • Some misc bug fixes and code cleanups
  • -
-
-
- - -

Player:

-
    -
  • Fix missing top left menu buttons on some system configurations
  • -
  • Fix potential video sink deadlock
  • -
  • Do not show mobile controls transition on launch
  • -
  • Show tooltip with full playlist item text on hover
  • -
-

YouTube:

-
    -
  • Auto select best matching resolution for used monitor
  • -
  • Added some YouTube related preferences
  • -
  • Added support for live HLS videos
  • -
  • Added support for non-adaptive live HLS streaming
  • -
-
-
- - -

New features:

-
    -
  • YouTube support - drag and drop videos from youtube or use open URI dialog to play them
  • -
  • Added convenient ways of opening external subtitles
  • -
-

Changes:

-
    -
  • Few GUI layout improvements
  • -
  • Simplified video sink code
  • -
  • Fixed missing Ctrl+O common keybinding
  • -
  • Fixed error when playback finishes during controls reveal animation
  • -
  • Fixed startup window size on Xorg
  • -
  • Fixed top time not showing up on fullscreen startup
  • -
  • Fixed missing file extensions in online URIs
  • -
  • Fixed some error messages not being displayed
  • -
-
-
- - -

First stable release

-
-
- - -

GitHub version

-
-
-
- - - keyboard - pointing - touch - - - small - - - workstation - mobile - -
diff --git a/data/com.github.rafostar.Clapper.service.in b/data/com.github.rafostar.Clapper.service.in deleted file mode 100644 index d8057116..00000000 --- a/data/com.github.rafostar.Clapper.service.in +++ /dev/null @@ -1,3 +0,0 @@ -[D-BUS Service] -Name=@app_id@ -Exec=@bindir@/@app_id@ --gapplication-service diff --git a/data/com.github.rafostar.Clapper.xml b/data/com.github.rafostar.Clapper.xml deleted file mode 100644 index 121ece0f..00000000 --- a/data/com.github.rafostar.Clapper.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - Clapper Playlist - - - - diff --git a/data/icons/pause-symbolic.svg b/data/icons/pause-symbolic.svg deleted file mode 100644 index 873edcae..00000000 --- a/data/icons/pause-symbolic.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/data/icons/pip-in-symbolic.svg b/data/icons/pip-in-symbolic.svg deleted file mode 100644 index c3d488ad..00000000 --- a/data/icons/pip-in-symbolic.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/icons/pip-out-symbolic.svg b/data/icons/pip-out-symbolic.svg deleted file mode 100644 index 6e9c10d0..00000000 --- a/data/icons/pip-out-symbolic.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/icons/play-symbolic.svg b/data/icons/play-symbolic.svg deleted file mode 100644 index c3499170..00000000 --- a/data/icons/play-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/data/meson.build b/data/meson.build deleted file mode 100644 index d9353bd4..00000000 --- a/data/meson.build +++ /dev/null @@ -1,46 +0,0 @@ -iconsdir = join_paths(datadir, 'icons', 'hicolor') - -appstream_util = find_program('appstream-util', required: false) -if appstream_util.found() - test('Validate appstream file', appstream_util, args: [ - 'validate-relax', '--nonet', - join_paths(meson.current_source_dir(), 'com.github.rafostar.Clapper.metainfo.xml') - ]) -endif - -install_data('com.github.rafostar.Clapper.svg', - install_dir: join_paths(iconsdir, 'scalable', 'apps') -) -install_data('com.github.rafostar.Clapper-symbolic.svg', - install_dir: join_paths(iconsdir, 'symbolic', 'apps') -) -install_data('com.github.rafostar.Clapper.gschema.xml', - install_dir: join_paths(datadir, 'glib-2.0', 'schemas') -) -install_data('com.github.rafostar.Clapper.xml', - install_dir: join_paths(datadir, 'mime', 'packages') -) -install_data('com.github.rafostar.Clapper.desktop', - install_dir: join_paths(datadir, 'applications') -) -install_data('com.github.rafostar.Clapper.metainfo.xml', - install_dir: join_paths(datadir, 'metainfo') -) -gnome.compile_resources('com.github.rafostar.Clapper.data', - 'com.github.rafostar.Clapper.data.gresource.xml', - gresource_bundle: true, - install: true, - install_dir: pkgdatadir, -) - -dbus_conf = configuration_data() -dbus_conf.set('app_id', meson.project_name()) -dbus_conf.set('bindir', bindir) - -configure_file( - input: 'com.github.rafostar.Clapper.service.in', - output: 'com.github.rafostar.Clapper.service', - configuration: dbus_conf, - install: true, - install_dir: join_paths(datadir, 'dbus-1', 'services'), -) diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 00000000..dcd75202 --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,10 @@ +gi_docgen = find_program('gi-docgen', required: get_option('doc')) +dot = find_program('dot', required: get_option('doc')) # Class hierarchy generation +build_doc = (gi_docgen.found() and dot.found() and get_option('doc')) + +if build_doc + if not build_gir + error('Building documentation requires introspection to be compiled') + endif + subdir('reference') +endif diff --git a/doc/reference/clapper/clapper.toml.in b/doc/reference/clapper/clapper.toml.in new file mode 100644 index 00000000..27972bfb --- /dev/null +++ b/doc/reference/clapper/clapper.toml.in @@ -0,0 +1,73 @@ +[library] +version = "@CLAPPER_VERSION@" +browse_url = "https://github.com/Rafostar/clapper/" +repository_url = "https://github.com/Rafostar/clapper.git" +website_url = "https://rafostar.github.io/clapper/" +docs_url = "https://rafostar.github.io/clapper/doc/clapper/" +authors = "Rafał Dzięgiel" +logo_url = "clapper-logo.svg" +license = "LGPL-2.1-or-later" +description = "Clapper playback library" +devhelp = true +search_index = true + +dependencies = ["GLib-2.0", "GObject-2.0", "Gio-2.0", "Gst-1.0", "GstBase-1.0", "GstAudio-1.0", "GstTag-1.0", "GstPbutils-1.0"] + + [dependencies."GLib-2.0"] + name = "GLib" + description = "A general-purpose, portable utility library" + docs_url = "https://docs.gtk.org/glib/" + + [dependencies."GObject-2.0"] + name = "GObject" + description = "The base type system library" + docs_url = "https://docs.gtk.org/gobject/" + + [dependencies."Gio-2.0"] + name = "Gio" + description = "GObject Interfaces and Objects, Networking, IPC, and I/O" + docs_url = "https://docs.gtk.org/gio/" + + [dependencies."Gst-1.0"] + name = "Gst" + description = "GStreamer core library" + docs_url = "https://gstreamer.freedesktop.org/documentation/gstreamer/gi-index.html" + + [dependencies."GstBase-1.0"] + name = "GstBase" + description = "GStreamer base and utility classes" + docs_url = "https://gstreamer.freedesktop.org/documentation/base/" + + [dependencies."GstAudio-1.0"] + name = "GstAudio" + description = "GStreamer audio library" + docs_url = "https://gstreamer.freedesktop.org/documentation/audio/" + + [dependencies."GstTag-1.0"] + name = "GstTag" + description = "GStreamer tag support library" + docs_url = "https://gstreamer.freedesktop.org/documentation/tag/" + + [dependencies."GstPbutils-1.0"] + name = "GstPbutils" + description = "GStreamer base utils library" + docs_url = "https://gstreamer.freedesktop.org/documentation/pbutils/" + +related = [] + +[theme] +name = "basic" +show_index_summary = true +show_class_hierarchy = true + +[source-location] +base_url = "https://github.com/Rafostar/clapper/tree/master/" + +[extra] +# The same order will be used when generating the index +content_files = [ +] +content_images = [ + "images/clapper-logo.svg", +] +urlmap_file = "urlmap.js" diff --git a/data/com.github.rafostar.Clapper.svg b/doc/reference/clapper/images/clapper-logo.svg similarity index 100% rename from data/com.github.rafostar.Clapper.svg rename to doc/reference/clapper/images/clapper-logo.svg diff --git a/doc/reference/clapper/meson.build b/doc/reference/clapper/meson.build new file mode 100644 index 00000000..fdf3df71 --- /dev/null +++ b/doc/reference/clapper/meson.build @@ -0,0 +1,29 @@ +clapper_toml = configure_file( + input: 'clapper.toml.in', + output: 'clapper.toml', + configuration: doc_version_conf, + install: true, + install_dir: join_paths(datadir, 'doc', 'clapper'), +) + +custom_target('clapper-doc', + input: [ + clapper_toml, + clapper_gir[0], + ], + output: 'clapper', + command: [ + gi_docgen, + 'generate', + gi_docgen_common_args, + '--add-include-path=@0@'.format(join_paths(meson.project_build_root(), 'src', 'lib', 'clapper')), + '--config=@INPUT0@', + '--output-dir=@OUTPUT@', + '--content-dir=@0@'.format(meson.current_build_dir()), + '--content-dir=@0@'.format(meson.current_source_dir()), + '@INPUT1@', + ], + build_by_default: true, + install: true, + install_dir: join_paths(datadir, 'doc'), +) diff --git a/doc/reference/clapper/urlmap.js b/doc/reference/clapper/urlmap.js new file mode 100644 index 00000000..74946a9f --- /dev/null +++ b/doc/reference/clapper/urlmap.js @@ -0,0 +1,10 @@ +baseURLs = [ + ['GLib', 'https://docs.gtk.org/glib/'], + ['GObject', 'https://docs.gtk.org/gobject/'], + ['Gio', 'https://docs.gtk.org/gio/'], + ['Gst', 'https://gstreamer.freedesktop.org/documentation/gstreamer/gi-index.html?'], + ['GstBase', 'https://gstreamer.freedesktop.org/documentation/base/?'], + ['GstAudio', 'https://gstreamer.freedesktop.org/documentation/audio/?'], + ['GstTag', 'https://gstreamer.freedesktop.org/documentation/tag/?'], + ['GstPbutils', 'https://gstreamer.freedesktop.org/documentation/pbutils/?'], +] diff --git a/doc/reference/meson.build b/doc/reference/meson.build new file mode 100644 index 00000000..ccf2941c --- /dev/null +++ b/doc/reference/meson.build @@ -0,0 +1,15 @@ +doc_version_conf = configuration_data() +doc_version_conf.set('CLAPPER_VERSION', meson.project_version()) +doc_version_conf.set('CLAPPER_VERSION_SUFFIX', clapper_version_suffix) + +gi_docgen_common_args = [ + '--quiet', + '--no-namespace-dir', +] +if get_option('werror') + gi_docgen_common_args += ['--fatal-warnings'] +endif + +if build_clapper + subdir('clapper') +endif diff --git a/extras/debug/Debug.js b/extras/debug/Debug.js deleted file mode 100644 index 98e9a7a0..00000000 --- a/extras/debug/Debug.js +++ /dev/null @@ -1,183 +0,0 @@ -const { GLib } = imports.gi; - -let ink = { Ink: null }; -try { - ink = imports.ink; -} catch(e) {} -const { Ink } = ink; - -const DEBUG_ENV = GLib.getenv('DEBUG'); - -var Debugger = class -{ - constructor(name, opts) - { - opts = (opts && typeof opts === 'object') - ? opts : {}; - - this.name = (name && typeof name === 'string') - ? name : 'GJS'; - - this.print_state = (opts.print_state) - ? true : false; - - this.json_space = (typeof opts.json_space === 'number') - ? opts.json_space : 2; - - this.name_printer = opts.name_printer || this._getInkPrinter(true); - this.message_printer = opts.message_printer || this._getDefaultPrinter(); - this.time_printer = opts.time_printer || this._getInkPrinter(); - this.high_precision = opts.high_precision || false; - - if(typeof opts.color !== 'undefined') - this.color = opts.color; - - this._isEnabled = false; - this._lastDebug = GLib.get_monotonic_time(); - - this.enabled = (typeof opts.enabled !== 'undefined') - ? opts.enabled : this._enabledAtStart; - } - - get enabled() - { - return this._isEnabled; - } - - set enabled(value) - { - if(this._isEnabled === value) - return; - - this._isEnabled = (value) ? true : false; - - if(!this.print_state) - return; - - let state = (this.enabled) ? 'en' : 'dis'; - this._runDebug(`debug ${state}abled`); - } - - get color() - { - return this.name_printer.color; - } - - set color(value) - { - this.name_printer.color = value; - this.time_printer.color = this.name_printer.color; - } - - get debug() - { - return message => this._debug(message); - } - - get _enabledAtStart() - { - if(!DEBUG_ENV) - return false; - - let envArr = DEBUG_ENV.split(','); - - return envArr.some(el => { - if(el === this.name || el === '*') - return true; - - let searchType; - let offset = 0; - - if(el.startsWith('*')) { - searchType = 'ends'; - } else if(el.endsWith('*')) { - searchType = 'starts'; - offset = 1; - } - - if(!searchType) - return false; - - return this.name[searchType + 'With']( - el.substring(1 - offset, el.length - offset) - ); - }); - } - - _getInkPrinter(isBold) - { - if(!Ink) - return this._getDefaultPrinter(); - - let printer = new Ink.Printer({ - color: Ink.colorFromText(this.name) - }); - - if(isBold) - printer.font = Ink.Font.BOLD; - - return printer; - } - - _getDefaultPrinter() - { - return { - getPainted: function() { - return Object.values(arguments); - } - }; - } - - _debug(message) - { - if(!this.enabled) - return; - - this._runDebug(message); - } - - _runDebug(message) - { - switch(typeof message) { - case 'string': - break; - case 'object': - if( - message !== null - && (message.constructor === Object - || message.constructor === Array) - ) { - message = JSON.stringify(message, null, this.json_space); - break; - } - default: - message = String(message); - break; - } - - let time = GLib.get_monotonic_time() - this._lastDebug; - - if(!this.high_precision) { - time = (time < 1000) - ? '+0ms' - : (time < 1000000) - ? '+' + Math.floor(time / 1000) + 'ms' - : '+' + Math.floor(time / 1000000) + 's'; - } - else { - time = (time < 1000) - ? '+' + time + 'µs' - : (time < 1000000) - ? '+' + (time / 1000).toFixed(3) + 'ms' - : '+' + (time / 1000000).toFixed(3) + 's'; - } - - printerr( - this.name_printer.getPainted(this.name), - this.message_printer.getPainted(message), - this.time_printer.getPainted(time) - ); - - this._lastDebug = GLib.get_monotonic_time(); - } -} diff --git a/extras/ink/Ink.js b/extras/ink/Ink.js deleted file mode 100644 index 75a40a6c..00000000 --- a/extras/ink/Ink.js +++ /dev/null @@ -1,322 +0,0 @@ -const TERM_ESC = '\x1B['; -const TERM_RESET = '0m'; - -var maxTransparency = 128; - -var Font = { - VARIOUS: null, - REGULAR: 0, - BOLD: 1, - DIM: 2, - ITALIC: 3, - UNDERLINE: 4, - BLINK: 5, - REVERSE: 7, - HIDDEN: 8, - STRIKEOUT: 9, -}; - -var Color = { - VARIOUS: null, - DEFAULT: 39, - BLACK: 30, - RED: 31, - GREEN: 32, - YELLOW: 33, - BLUE: 34, - MAGENTA: 35, - CYAN: 36, - LIGHT_GRAY: 37, - DARK_GRAY: 90, - LIGHT_RED: 91, - LIGHT_GREEN: 92, - LIGHT_YELLOW: 93, - LIGHT_BLUE: 94, - LIGHT_MAGENTA: 95, - LIGHT_CYAN: 96, - WHITE: 97, - BROWN: colorFrom256(52), - LIGHT_BROWN: colorFrom256(130), - PINK: colorFrom256(205), - LIGHT_PINK: colorFrom256(211), - ORANGE: colorFrom256(208), - LIGHT_ORANGE: colorFrom256(214), - SALMON: colorFrom256(209), - LIGHT_SALMON: colorFrom256(216), -}; - -function colorFrom256(number) -{ - if(typeof number === 'undefined') - number = Math.floor(Math.random() * 256) + 1; - - return `38;5;${number || 0}`; -} - -function colorFromRGB(R, G, B, A) -{ - if(typeof R === 'undefined') { - R = Math.floor(Math.random() * 256); - G = Math.floor(Math.random() * 256); - B = Math.floor(Math.random() * 256); - } - else if(typeof G === 'undefined' && Array.isArray(R)) { - A = (R.length > 3) ? R[3] : 255; - B = (R.length > 2) ? R[2] : 0; - G = (R.length > 1) ? R[1] : 0; - R = (R.length > 0) ? R[0] : 0; - } - - if(_getIsTransparent(A)) - return Color.DEFAULT; - - R = R || 0; - G = G || 0; - B = B || 0; - - return `38;2;${R};${G};${B}`; -} - -function colorFromHex(R, G, B, A) -{ - if((Array.isArray(R))) - R = R.join(''); - - let str = (typeof G === 'undefined') - ? String(R) - : (typeof A !== 'undefined') - ? String(R) + String(G) + String(B) + String(A) - : (typeof B !== 'undefined') - ? String(R) + String(G) + String(B) - : String(R) + String(G); - - let offset = (str[0] === '#') ? 1 : 0; - let alphaIndex = 6 + offset; - - while(str.length < alphaIndex) - str += '0'; - - A = (str.length > alphaIndex) - ? parseInt(str.substring(alphaIndex, alphaIndex + 2), 16) - : 255; - str = str.substring(offset, alphaIndex); - - let colorInt = parseInt(str, 16); - let u8arr = new Uint8Array(3); - - u8arr[2] = colorInt; - u8arr[1] = colorInt >> 8; - u8arr[0] = colorInt >> 16; - - return colorFromRGB(u8arr[0], u8arr[1], u8arr[2], A); -} - -function colorFromText(text) -{ - let value = _stringToDec(text); - - /* Returns color from 1 to 221 every 10 */ - return colorFrom256((value % 23) * 10 + 1); -} - -function fontFromText(text) -{ - let arr = Object.keys(Font); - let value = _stringToDec(text); - - /* Return a font excluding first (null) */ - return Font[arr[value % (arr.length - 1) + 1]]; -} - -function _getIsImage(args) -{ - if(args.length !== 1) - return false; - - let arg = args[0]; - let argType = (typeof arg); - - if(argType === 'string' || argType === 'number') - return false; - - if(!Array.isArray(arg)) - return false; - - let depth = 2; - while(depth--) { - arg = arg[0]; - if(!Array.isArray(arg)) - return false; - } - - return arg.some(val => val !== 'number'); -} - -function _getIsTransparent(A) -{ - return (typeof A !== 'undefined' && A <= maxTransparency); -} - -function _stringToDec(str) -{ - str = str || ''; - - let len = str.length; - let total = 0; - - while(len--) - total += Number(str.charCodeAt(len).toString(10)); - - return total; -} - -var Printer = class -{ - constructor(opts) - { - opts = opts || {}; - - const defaults = { - font: Font.REGULAR, - color: Color.DEFAULT, - background: Color.DEFAULT - }; - - for(let def in defaults) { - this[def] = (typeof opts[def] !== 'undefined') - ? opts[def] : defaults[def]; - } - } - - print() - { - (_getIsImage(arguments)) - ? this._printImage(arguments[0], 'stdout') - : print(this._getPaintedArgs(arguments)); - } - - printerr() - { - (_getIsImage(arguments)) - ? this._printImage(arguments[0], 'stderr') - : printerr(this._getPaintedArgs(arguments)); - } - - getPainted() - { - return (_getIsImage(arguments)) - ? this._printImage(arguments[0], 'return') - : this._getPaintedArgs(arguments); - } - - get background() - { - return this._background; - } - - set background(value) - { - let valueType = (typeof value); - - if(valueType === 'string') { - value = (value[2] === ';') - ? '4' + value.substring(1) - : Number(value); - } - this._background = (valueType === 'object') - ? null - : (value < 40 || value >= 90 && value < 100) - ? value + 10 - : value; - } - - _getPaintedArgs(args) - { - let str = ''; - - for(let arg of args) { - if(Array.isArray(arg)) - arg = arg.join(','); - - let painted = this._getPaintedString(arg); - str += (str.length) ? ' ' + painted : painted; - } - - return str; - } - - _getPaintedString(text, noReset) - { - let str = TERM_ESC; - - for(let option of ['font', 'color', '_background']) { - let optionType = (typeof this[option]); - str += (optionType === 'number' || optionType === 'string') - ? this[option] - : (option === 'font' && Array.isArray(this[option])) - ? this[option].join(';') - : (option === 'font') - ? fontFromText(text) - : colorFromText(text); - - str += (option !== '_background') ? ';' : 'm'; - } - str += text; - - return (noReset) - ? str - : (str + TERM_ESC + TERM_RESET); - } - - _printImage(pixelsArr, output) - { - let total = ''; - let prevColor = this.color; - let prevBackground = this._background; - - for(let row of pixelsArr) { - let paintedLine = ''; - let block = ' '; - - for(let i = 0; i < row.length; i++) { - let pixel = row[i]; - let nextPixel = (i < row.length - 1) ? row[i + 1] : null; - - if(nextPixel && pixel.every((value, index) => - value === nextPixel[index] - )) { - block += ' '; - continue; - } - /* Do not use predefined functions here (it would impact performance) */ - let isTransparent = (pixel.length >= 3) ? _getIsTransparent(pixel[3]) : false; - this.color = (isTransparent) - ? Color.DEFAULT - : `38;2;${pixel[0]};${pixel[1]};${pixel[2]}`; - this._background = (isTransparent) - ? Color.DEFAULT - : `48;2;${pixel[0]};${pixel[1]};${pixel[2]}`; - paintedLine += `${TERM_ESC}0;${this.color};${this._background}m${block}`; - block = ' '; - } - paintedLine += TERM_ESC + TERM_RESET; - - switch(output) { - case 'stderr': - printerr(paintedLine); - break; - case 'return': - total += paintedLine + '\n'; - break; - default: - print(paintedLine); - break; - } - } - - this.color = prevColor; - this._background = prevBackground; - - return total; - } -} diff --git a/lib/gst/clapper/clapper-prelude.h b/lib/gst/clapper/clapper-prelude.h deleted file mode 100644 index 56380aa4..00000000 --- a/lib/gst/clapper/clapper-prelude.h +++ /dev/null @@ -1,42 +0,0 @@ -/* GStreamer - * Copyright (C) 2018 GStreamer developers - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_PRELUDE_H__ -#define __GST_CLAPPER_PRELUDE_H__ - -#include - -#ifndef GST_CLAPPER_API -# ifdef BUILDING_GST_CLAPPER -# define GST_CLAPPER_API GST_API_EXPORT /* from config.h */ -# else -# define GST_CLAPPER_API GST_API_IMPORT -# endif -#endif - -#ifndef GST_DISABLE_DEPRECATED -#define GST_CLAPPER_DEPRECATED GST_CLAPPER_API -#define GST_CLAPPER_DEPRECATED_FOR(f) GST_CLAPPER_API -#else -#define GST_CLAPPER_DEPRECATED G_DEPRECATED GST_CLAPPER_API -#define GST_CLAPPER_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) GST_CLAPPER_API -#endif - -#endif /* __GST_CLAPPER_PRELUDE_H__ */ diff --git a/lib/gst/clapper/clapper.h b/lib/gst/clapper/clapper.h deleted file mode 100644 index ec3b5c40..00000000 --- a/lib/gst/clapper/clapper.h +++ /dev/null @@ -1,34 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __CLAPPER_H__ -#define __CLAPPER_H__ - -#include -#include -#include -#include -#include -#include -#include -#include - -#endif /* __CLAPPER_H__ */ diff --git a/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.c b/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.c deleted file mode 100644 index f2e8419b..00000000 --- a/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.c +++ /dev/null @@ -1,214 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapper-gmaincontextsignaldispatcher - * @title: GstClapperGMainContextSignalDispatcher - * @short_description: Clapper GLib MainContext dispatcher - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-g-main-context-signal-dispatcher.h" - -struct _GstClapperGMainContextSignalDispatcher -{ - GObject parent; - GMainContext *application_context; -}; - -struct _GstClapperGMainContextSignalDispatcherClass -{ - GObjectClass parent_class; -}; - -static void - gst_clapper_g_main_context_signal_dispatcher_interface_init - (GstClapperSignalDispatcherInterface * iface); - -enum -{ - G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_0, - G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_APPLICATION_CONTEXT, - G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_LAST -}; - -G_DEFINE_TYPE_WITH_CODE (GstClapperGMainContextSignalDispatcher, - gst_clapper_g_main_context_signal_dispatcher, G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE (GST_TYPE_CLAPPER_SIGNAL_DISPATCHER, - gst_clapper_g_main_context_signal_dispatcher_interface_init)); - -static GParamSpec - * g_main_context_signal_dispatcher_param_specs - [G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_LAST] = { NULL, }; - -static void -gst_clapper_g_main_context_signal_dispatcher_finalize (GObject * object) -{ - GstClapperGMainContextSignalDispatcher *self = - GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER (object); - - if (self->application_context) - g_main_context_unref (self->application_context); - - G_OBJECT_CLASS - (gst_clapper_g_main_context_signal_dispatcher_parent_class)->finalize - (object); -} - -static void -gst_clapper_g_main_context_signal_dispatcher_set_property (GObject * object, - guint prop_id, const GValue * value, GParamSpec * pspec) -{ - GstClapperGMainContextSignalDispatcher *self = - GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER (object); - - switch (prop_id) { - case G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_APPLICATION_CONTEXT: - self->application_context = g_value_dup_boxed (value); - if (!self->application_context) - self->application_context = g_main_context_ref_thread_default (); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_g_main_context_signal_dispatcher_get_property (GObject * object, - guint prop_id, GValue * value, GParamSpec * pspec) -{ - GstClapperGMainContextSignalDispatcher *self = - GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER (object); - - switch (prop_id) { - case G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_APPLICATION_CONTEXT: - g_value_set_boxed (value, self->application_context); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void - gst_clapper_g_main_context_signal_dispatcher_class_init - (GstClapperGMainContextSignalDispatcherClass * klass) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - - gobject_class->finalize = - gst_clapper_g_main_context_signal_dispatcher_finalize; - gobject_class->set_property = - gst_clapper_g_main_context_signal_dispatcher_set_property; - gobject_class->get_property = - gst_clapper_g_main_context_signal_dispatcher_get_property; - - g_main_context_signal_dispatcher_param_specs - [G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_APPLICATION_CONTEXT] = - g_param_spec_boxed ("application-context", "Application Context", - "Application GMainContext to dispatch signals to", G_TYPE_MAIN_CONTEXT, - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); - - g_object_class_install_properties (gobject_class, - G_MAIN_CONTEXT_SIGNAL_DISPATCHER_PROP_LAST, - g_main_context_signal_dispatcher_param_specs); -} - -static void - gst_clapper_g_main_context_signal_dispatcher_init - (G_GNUC_UNUSED GstClapperGMainContextSignalDispatcher * self) -{ -} - -typedef struct -{ - void (*emitter) (gpointer data); - gpointer data; - GDestroyNotify destroy; -} GMainContextSignalDispatcherData; - -static gboolean -g_main_context_signal_dispatcher_dispatch_gsourcefunc (gpointer user_data) -{ - GMainContextSignalDispatcherData *data = user_data; - - data->emitter (data->data); - - return G_SOURCE_REMOVE; -} - -static void -g_main_context_signal_dispatcher_dispatch_destroy (gpointer user_data) -{ - GMainContextSignalDispatcherData *data = user_data; - - if (data->destroy) - data->destroy (data->data); - g_free (data); -} - -static void -gst_clapper_g_main_context_signal_dispatcher_dispatch (GstClapperSignalDispatcher - * iface, G_GNUC_UNUSED GstClapper * clapper, void (*emitter) (gpointer data), - gpointer data, GDestroyNotify destroy) -{ - GstClapperGMainContextSignalDispatcher *self = - GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER (iface); - GMainContextSignalDispatcherData *gsourcefunc_data = - g_new (GMainContextSignalDispatcherData, 1); - - gsourcefunc_data->emitter = emitter; - gsourcefunc_data->data = data; - gsourcefunc_data->destroy = destroy; - - g_main_context_invoke_full (self->application_context, - G_PRIORITY_DEFAULT, g_main_context_signal_dispatcher_dispatch_gsourcefunc, - gsourcefunc_data, g_main_context_signal_dispatcher_dispatch_destroy); -} - -static void - gst_clapper_g_main_context_signal_dispatcher_interface_init - (GstClapperSignalDispatcherInterface * iface) -{ - iface->dispatch = gst_clapper_g_main_context_signal_dispatcher_dispatch; -} - -/** - * gst_clapper_g_main_context_signal_dispatcher_new: - * @application_context: (allow-none): GMainContext to use or %NULL - * - * Creates a new GstClapperSignalDispatcher that uses @application_context, - * or the thread default one if %NULL is used. See gst_clapper_new(). - * - * Returns: (transfer full): the new GstClapperSignalDispatcher - */ -GstClapperSignalDispatcher * -gst_clapper_g_main_context_signal_dispatcher_new (GMainContext * - application_context) -{ - return g_object_new (GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER, - "application-context", application_context, NULL); -} diff --git a/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.h b/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.h deleted file mode 100644 index a423b2d0..00000000 --- a/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.h +++ /dev/null @@ -1,51 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_H__ -#define __GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_H__ - -#include -#include - -G_BEGIN_DECLS - -typedef struct _GstClapperGMainContextSignalDispatcher - GstClapperGMainContextSignalDispatcher; -typedef struct _GstClapperGMainContextSignalDispatcherClass - GstClapperGMainContextSignalDispatcherClass; - -#define GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER (gst_clapper_g_main_context_signal_dispatcher_get_type ()) -#define GST_IS_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER)) -#define GST_IS_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER)) -#define GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER, GstClapperGMainContextSignalDispatcherClass)) -#define GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER, GstClapperGMainContextSignalDispatcher)) -#define GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER, GstClapperGMainContextSignalDispatcherClass)) -#define GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_CAST(obj) ((GstClapperGMainContextSignalDispatcher*)(obj)) - -GST_CLAPPER_API -GType gst_clapper_g_main_context_signal_dispatcher_get_type (void); - -GST_CLAPPER_API -GstClapperSignalDispatcher * gst_clapper_g_main_context_signal_dispatcher_new (GMainContext * application_context); - -G_END_DECLS - -#endif /* __GST_CLAPPER_G_MAIN_CONTEXT_SIGNAL_DISPATCHER_H__ */ diff --git a/lib/gst/clapper/gstclapper-gtk4-plugin.c b/lib/gst/clapper/gstclapper-gtk4-plugin.c deleted file mode 100644 index 18c14375..00000000 --- a/lib/gst/clapper/gstclapper-gtk4-plugin.c +++ /dev/null @@ -1,123 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapper-gtk4plugin - * @title: GstClapperGtk4Plugin - * @short_description: Clapper GTK4 plugin - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-gtk4-plugin.h" -#include "gtk4/gstclapperglsink.h" - -enum -{ - PROP_0, - PROP_VIDEO_SINK, - PROP_LAST -}; - -#define parent_class gst_clapper_gtk4_plugin_parent_class -G_DEFINE_TYPE_WITH_CODE (GstClapperGtk4Plugin, gst_clapper_gtk4_plugin, - G_TYPE_OBJECT, NULL); - -static GParamSpec *param_specs[PROP_LAST] = { NULL, }; - -static void gst_clapper_gtk4_plugin_constructed (GObject * object); -static void gst_clapper_gtk4_plugin_finalize (GObject * object); -static void gst_clapper_gtk4_plugin_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec); - -static void gst_clapper_gtk4_plugin_init - (G_GNUC_UNUSED GstClapperGtk4Plugin * self) -{ -} - -static void gst_clapper_gtk4_plugin_class_init - (G_GNUC_UNUSED GstClapperGtk4PluginClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->constructed = gst_clapper_gtk4_plugin_constructed; - gobject_class->get_property = gst_clapper_gtk4_plugin_get_property; - gobject_class->finalize = gst_clapper_gtk4_plugin_finalize; - - param_specs[PROP_VIDEO_SINK] = - g_param_spec_object ("video-sink", - "Video Sink", "Video sink to use with video renderer", - GST_TYPE_ELEMENT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); - - g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); -} - -static void -gst_clapper_gtk4_plugin_constructed (GObject * object) -{ - GstClapperGtk4Plugin *self = GST_CLAPPER_GTK4_PLUGIN (object); - - self->video_sink = g_object_new (GST_TYPE_CLAPPER_GL_SINK, NULL); - gst_object_ref_sink (self->video_sink); - - G_OBJECT_CLASS (parent_class)->constructed (object); -} - -static void -gst_clapper_gtk4_plugin_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec) -{ - GstClapperGtk4Plugin *self = GST_CLAPPER_GTK4_PLUGIN (object); - - switch (prop_id) { - case PROP_VIDEO_SINK: - g_value_set_object (value, self->video_sink); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_gtk4_plugin_finalize (GObject * object) -{ - GstClapperGtk4Plugin *self = GST_CLAPPER_GTK4_PLUGIN (object); - - gst_object_unref (self->video_sink); - - G_OBJECT_CLASS (parent_class)->finalize (object); -} - -/** - * gst_clapper_gtk4_plugin_new: - * - * Creates a new GTK4 plugin. - * - * Returns: (transfer full): the new GstClapperGtk4Plugin - */ -GstClapperGtk4Plugin * -gst_clapper_gtk4_plugin_new (void) -{ - return g_object_new (GST_TYPE_CLAPPER_GTK4_PLUGIN, NULL); -} diff --git a/lib/gst/clapper/gstclapper-gtk4-plugin.h b/lib/gst/clapper/gstclapper-gtk4-plugin.h deleted file mode 100644 index 448045dc..00000000 --- a/lib/gst/clapper/gstclapper-gtk4-plugin.h +++ /dev/null @@ -1,72 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_GTK4_PLUGIN_H__ -#define __GST_CLAPPER_GTK4_PLUGIN_H__ - -#include -#include - -G_BEGIN_DECLS - -#define GST_TYPE_CLAPPER_GTK4_PLUGIN (gst_clapper_gtk4_plugin_get_type ()) -#define GST_IS_CLAPPER_GTK4_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_GTK4_PLUGIN)) -#define GST_IS_CLAPPER_GTK4_PLUGIN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_GTK4_PLUGIN)) -#define GST_CLAPPER_GTK4_PLUGIN_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_GTK4_PLUGIN, GstClapperGtk4PluginClass)) -#define GST_CLAPPER_GTK4_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_GTK4_PLUGIN, GstClapperGtk4Plugin)) -#define GST_CLAPPER_GTK4_PLUGIN_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_GTK4_PLUGIN, GstClapperGtk4PluginClass)) -#define GST_CLAPPER_GTK4_PLUGIN_CAST(obj) ((GstClapperGtk4Plugin*)(obj)) - -typedef struct _GstClapperGtk4Plugin GstClapperGtk4Plugin; -typedef struct _GstClapperGtk4PluginClass GstClapperGtk4PluginClass; - -/** - * GstClapperGtk4Plugin: - * - * Opaque #GstClapperGtk4Plugin object - */ -struct _GstClapperGtk4Plugin -{ - /* */ - GObject parent; - - GstElement *video_sink; -}; - -/** - * GstClapperGtk4PluginClass: - * - * The #GstClapperGtk4PluginClass struct only contains private data - */ -struct _GstClapperGtk4PluginClass -{ - /* */ - GstElementClass parent_class; -}; - -GST_CLAPPER_API -GType gst_clapper_gtk4_plugin_get_type (void); - -GST_CLAPPER_API -GstClapperGtk4Plugin * gst_clapper_gtk4_plugin_new (void); - -G_END_DECLS - -#endif /* __GST_CLAPPER_GTK4_PLUGIN__ */ diff --git a/lib/gst/clapper/gstclapper-media-info-private.h b/lib/gst/clapper/gstclapper-media-info-private.h deleted file mode 100644 index e89d4514..00000000 --- a/lib/gst/clapper/gstclapper-media-info-private.h +++ /dev/null @@ -1,125 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2015 Brijesh Singh - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#include "gstclapper-media-info.h" - -#ifndef __GST_CLAPPER_MEDIA_INFO_PRIVATE_H__ -#define __GST_CLAPPER_MEDIA_INFO_PRIVATE_H__ - -struct _GstClapperStreamInfo -{ - GObject parent; - - gchar *codec; - - GstCaps *caps; - gint stream_index; - GstTagList *tags; - gchar *stream_id; -}; - -struct _GstClapperStreamInfoClass -{ - GObjectClass parent_class; -}; - -struct _GstClapperSubtitleInfo -{ - GstClapperStreamInfo parent; - - gchar *title; - gchar *language; -}; - -struct _GstClapperSubtitleInfoClass -{ - GstClapperStreamInfoClass parent_class; -}; - -struct _GstClapperAudioInfo -{ - GstClapperStreamInfo parent; - - gint channels; - gint sample_rate; - - guint bitrate; - guint max_bitrate; - - gchar *language; -}; - -struct _GstClapperAudioInfoClass -{ - GstClapperStreamInfoClass parent_class; -}; - -struct _GstClapperVideoInfo -{ - GstClapperStreamInfo parent; - - gint width; - gint height; - gint framerate_num; - gint framerate_denom; - gint par_num; - gint par_denom; - - guint bitrate; - guint max_bitrate; -}; - -struct _GstClapperVideoInfoClass -{ - GstClapperStreamInfoClass parent_class; -}; - -struct _GstClapperMediaInfo -{ - GObject parent; - - gchar *uri; - gchar *title; - gchar *container; - gboolean seekable, is_live; - GstTagList *tags; - GstToc *toc; - GstSample *image_sample; - - GList *stream_list; - GList *audio_stream_list; - GList *video_stream_list; - GList *subtitle_stream_list; - - GstClockTime duration; -}; - -struct _GstClapperMediaInfoClass -{ - GObjectClass parent_class; -}; - -G_GNUC_INTERNAL GstClapperMediaInfo * gst_clapper_media_info_new (const gchar *uri); -G_GNUC_INTERNAL GstClapperMediaInfo * gst_clapper_media_info_copy (GstClapperMediaInfo *ref); -G_GNUC_INTERNAL GstClapperStreamInfo * gst_clapper_stream_info_new (gint stream_index, GType type); -G_GNUC_INTERNAL GstClapperStreamInfo * gst_clapper_stream_info_copy (GstClapperStreamInfo *ref); - -#endif /* __GST_CLAPPER_MEDIA_INFO_PRIVATE_H__ */ diff --git a/lib/gst/clapper/gstclapper-media-info.c b/lib/gst/clapper/gstclapper-media-info.c deleted file mode 100644 index 98374266..00000000 --- a/lib/gst/clapper/gstclapper-media-info.c +++ /dev/null @@ -1,885 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2015 Brijesh Singh - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapper-mediainfo - * @title: GstClapperMediaInfo - * @short_description: Clapper Media Information - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-media-info.h" -#include "gstclapper-media-info-private.h" - -/* Per-stream information */ -G_DEFINE_ABSTRACT_TYPE (GstClapperStreamInfo, gst_clapper_stream_info, - G_TYPE_OBJECT); - -static void -gst_clapper_stream_info_init (GstClapperStreamInfo * sinfo) -{ - sinfo->stream_index = -1; -} - -static void -gst_clapper_stream_info_finalize (GObject * object) -{ - GstClapperStreamInfo *sinfo = GST_CLAPPER_STREAM_INFO (object); - - g_free (sinfo->codec); - g_free (sinfo->stream_id); - - if (sinfo->caps) - gst_caps_unref (sinfo->caps); - - if (sinfo->tags) - gst_tag_list_unref (sinfo->tags); - - G_OBJECT_CLASS (gst_clapper_stream_info_parent_class)->finalize (object); -} - -static void -gst_clapper_stream_info_class_init (GstClapperStreamInfoClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->finalize = gst_clapper_stream_info_finalize; -} - -/** - * gst_clapper_stream_info_get_index: - * @info: a #GstClapperStreamInfo - * - * Function to get stream index from #GstClapperStreamInfo instance. - * - * Returns: the stream index of this stream. - */ -gint -gst_clapper_stream_info_get_index (const GstClapperStreamInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_STREAM_INFO (info), -1); - - return info->stream_index; -} - -/** - * gst_clapper_stream_info_get_stream_type: - * @info: a #GstClapperStreamInfo - * - * Function to return human readable name for the stream type - * of the given @info (ex: "audio", "video", "subtitle") - * - * Returns: a human readable name - */ -const gchar * -gst_clapper_stream_info_get_stream_type (const GstClapperStreamInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_STREAM_INFO (info), NULL); - - if (GST_IS_CLAPPER_VIDEO_INFO (info)) - return "video"; - else if (GST_IS_CLAPPER_AUDIO_INFO (info)) - return "audio"; - else - return "subtitle"; -} - -/** - * gst_clapper_stream_info_get_tags: - * @info: a #GstClapperStreamInfo - * - * Returns: (transfer none): the tags contained in this stream. - */ -GstTagList * -gst_clapper_stream_info_get_tags (const GstClapperStreamInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_STREAM_INFO (info), NULL); - - return info->tags; -} - -/** - * gst_clapper_stream_info_get_codec: - * @info: a #GstClapperStreamInfo - * - * A string describing codec used in #GstClapperStreamInfo. - * - * Returns: codec string or NULL on unknown. - */ -const gchar * -gst_clapper_stream_info_get_codec (const GstClapperStreamInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_STREAM_INFO (info), NULL); - - return info->codec; -} - -/** - * gst_clapper_stream_info_get_caps: - * @info: a #GstClapperStreamInfo - * - * Returns: (transfer none): the #GstCaps of the stream. - */ -GstCaps * -gst_clapper_stream_info_get_caps (const GstClapperStreamInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_STREAM_INFO (info), NULL); - - return info->caps; -} - -/* Video information */ -G_DEFINE_TYPE (GstClapperVideoInfo, gst_clapper_video_info, - GST_TYPE_CLAPPER_STREAM_INFO); - -static void -gst_clapper_video_info_init (GstClapperVideoInfo * info) -{ - info->width = -1; - info->height = -1; - info->framerate_num = 0; - info->framerate_denom = 1; - info->par_num = 1; - info->par_denom = 1; -} - -static void -gst_clapper_video_info_class_init (G_GNUC_UNUSED GstClapperVideoInfoClass * klass) -{ - /* nothing to do here */ -} - -/** - * gst_clapper_video_info_get_width: - * @info: a #GstClapperVideoInfo - * - * Returns: the width of video in #GstClapperVideoInfo. - */ -gint -gst_clapper_video_info_get_width (const GstClapperVideoInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_VIDEO_INFO (info), -1); - - return info->width; -} - -/** - * gst_clapper_video_info_get_height: - * @info: a #GstClapperVideoInfo - * - * Returns: the height of video in #GstClapperVideoInfo. - */ -gint -gst_clapper_video_info_get_height (const GstClapperVideoInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_VIDEO_INFO (info), -1); - - return info->height; -} - -/** - * gst_clapper_video_info_get_framerate: - * @info: a #GstClapperVideoInfo - * @fps_n: (out): Numerator of frame rate - * @fps_d: (out): Denominator of frame rate - * - */ -void -gst_clapper_video_info_get_framerate (const GstClapperVideoInfo * info, - gint * fps_n, gint * fps_d) -{ - g_return_if_fail (GST_IS_CLAPPER_VIDEO_INFO (info)); - - *fps_n = info->framerate_num; - *fps_d = info->framerate_denom; -} - -/** - * gst_clapper_video_info_get_pixel_aspect_ratio: - * @info: a #GstClapperVideoInfo - * @par_n: (out): numerator - * @par_d: (out): denominator - * - * Returns the pixel aspect ratio in @par_n and @par_d - * - */ -void -gst_clapper_video_info_get_pixel_aspect_ratio (const GstClapperVideoInfo * info, - guint * par_n, guint * par_d) -{ - g_return_if_fail (GST_IS_CLAPPER_VIDEO_INFO (info)); - - *par_n = info->par_num; - *par_d = info->par_denom; -} - -/** - * gst_clapper_video_info_get_bitrate: - * @info: a #GstClapperVideoInfo - * - * Returns: the current bitrate of video in #GstClapperVideoInfo. - */ -gint -gst_clapper_video_info_get_bitrate (const GstClapperVideoInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_VIDEO_INFO (info), -1); - - return info->bitrate; -} - -/** - * gst_clapper_video_info_get_max_bitrate: - * @info: a #GstClapperVideoInfo - * - * Returns: the maximum bitrate of video in #GstClapperVideoInfo. - */ -gint -gst_clapper_video_info_get_max_bitrate (const GstClapperVideoInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_VIDEO_INFO (info), -1); - - return info->max_bitrate; -} - -/* Audio information */ -G_DEFINE_TYPE (GstClapperAudioInfo, gst_clapper_audio_info, - GST_TYPE_CLAPPER_STREAM_INFO); - -static void -gst_clapper_audio_info_init (GstClapperAudioInfo * info) -{ - info->channels = 0; - info->sample_rate = 0; - info->bitrate = -1; - info->max_bitrate = -1; -} - -static void -gst_clapper_audio_info_finalize (GObject * object) -{ - GstClapperAudioInfo *info = GST_CLAPPER_AUDIO_INFO (object); - - g_free (info->language); - - G_OBJECT_CLASS (gst_clapper_audio_info_parent_class)->finalize (object); -} - -static void -gst_clapper_audio_info_class_init (GstClapperAudioInfoClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->finalize = gst_clapper_audio_info_finalize; -} - -/** - * gst_clapper_audio_info_get_language: - * @info: a #GstClapperAudioInfo - * - * Returns: the language of the stream, or NULL if unknown. - */ -const gchar * -gst_clapper_audio_info_get_language (const GstClapperAudioInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_AUDIO_INFO (info), NULL); - - return info->language; -} - -/** - * gst_clapper_audio_info_get_channels: - * @info: a #GstClapperAudioInfo - * - * Returns: the number of audio channels in #GstClapperAudioInfo. - */ -gint -gst_clapper_audio_info_get_channels (const GstClapperAudioInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_AUDIO_INFO (info), 0); - - return info->channels; -} - -/** - * gst_clapper_audio_info_get_sample_rate: - * @info: a #GstClapperAudioInfo - * - * Returns: the audio sample rate in #GstClapperAudioInfo. - */ -gint -gst_clapper_audio_info_get_sample_rate (const GstClapperAudioInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_AUDIO_INFO (info), 0); - - return info->sample_rate; -} - -/** - * gst_clapper_audio_info_get_bitrate: - * @info: a #GstClapperAudioInfo - * - * Returns: the audio bitrate in #GstClapperAudioInfo. - */ -gint -gst_clapper_audio_info_get_bitrate (const GstClapperAudioInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_AUDIO_INFO (info), -1); - - return info->bitrate; -} - -/** - * gst_clapper_audio_info_get_max_bitrate: - * @info: a #GstClapperAudioInfo - * - * Returns: the audio maximum bitrate in #GstClapperAudioInfo. - */ -gint -gst_clapper_audio_info_get_max_bitrate (const GstClapperAudioInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_AUDIO_INFO (info), -1); - - return info->max_bitrate; -} - -/* Subtitle information */ -G_DEFINE_TYPE (GstClapperSubtitleInfo, gst_clapper_subtitle_info, - GST_TYPE_CLAPPER_STREAM_INFO); - -static void -gst_clapper_subtitle_info_init (G_GNUC_UNUSED GstClapperSubtitleInfo * info) -{ - /* nothing to do */ -} - -static void -gst_clapper_subtitle_info_finalize (GObject * object) -{ - GstClapperSubtitleInfo *info = GST_CLAPPER_SUBTITLE_INFO (object); - - g_free (info->title); - g_free (info->language); - - G_OBJECT_CLASS (gst_clapper_subtitle_info_parent_class)->finalize (object); -} - -static void -gst_clapper_subtitle_info_class_init (GstClapperSubtitleInfoClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->finalize = gst_clapper_subtitle_info_finalize; -} - -/** - * gst_clapper_subtitle_info_get_title: - * @info: a #GstClapperSubtitleInfo - * - * Returns: the title of the stream, or NULL if unknown. - */ -const gchar * -gst_clapper_subtitle_info_get_title (const GstClapperSubtitleInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_SUBTITLE_INFO (info), NULL); - - return info->title; -} - -/** - * gst_clapper_subtitle_info_get_language: - * @info: a #GstClapperSubtitleInfo - * - * Returns: the language of the stream, or NULL if unknown. - */ -const gchar * -gst_clapper_subtitle_info_get_language (const GstClapperSubtitleInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_SUBTITLE_INFO (info), NULL); - - return info->language; -} - -/* Global media information */ -G_DEFINE_TYPE (GstClapperMediaInfo, gst_clapper_media_info, G_TYPE_OBJECT); - -static void -gst_clapper_media_info_init (GstClapperMediaInfo * info) -{ - info->duration = -1; - info->is_live = FALSE; - info->seekable = FALSE; -} - -static void -gst_clapper_media_info_finalize (GObject * object) -{ - GstClapperMediaInfo *info = GST_CLAPPER_MEDIA_INFO (object); - - g_free (info->uri); - g_free (info->title); - g_free (info->container); - - if (info->tags) - gst_tag_list_unref (info->tags); - if (info->toc) - gst_toc_unref (info->toc); - if (info->image_sample) - gst_sample_unref (info->image_sample); - if (info->audio_stream_list) - g_list_free (info->audio_stream_list); - if (info->video_stream_list) - g_list_free (info->video_stream_list); - if (info->subtitle_stream_list) - g_list_free (info->subtitle_stream_list); - if (info->stream_list) - g_list_free_full (info->stream_list, g_object_unref); - - G_OBJECT_CLASS (gst_clapper_media_info_parent_class)->finalize (object); -} - -static void -gst_clapper_media_info_class_init (GstClapperMediaInfoClass * klass) -{ - GObjectClass *oclass = (GObjectClass *) klass; - - oclass->finalize = gst_clapper_media_info_finalize; -} - -static GstClapperVideoInfo * -gst_clapper_video_info_new (void) -{ - return g_object_new (GST_TYPE_CLAPPER_VIDEO_INFO, NULL); -} - -static GstClapperAudioInfo * -gst_clapper_audio_info_new (void) -{ - return g_object_new (GST_TYPE_CLAPPER_AUDIO_INFO, NULL); -} - -static GstClapperSubtitleInfo * -gst_clapper_subtitle_info_new (void) -{ - return g_object_new (GST_TYPE_CLAPPER_SUBTITLE_INFO, NULL); -} - -static GstClapperStreamInfo * -gst_clapper_video_info_copy (GstClapperVideoInfo * ref) -{ - GstClapperVideoInfo *ret; - - ret = gst_clapper_video_info_new (); - - ret->width = ref->width; - ret->height = ref->height; - ret->framerate_num = ref->framerate_num; - ret->framerate_denom = ref->framerate_denom; - ret->par_num = ref->par_num; - ret->par_denom = ref->par_denom; - ret->bitrate = ref->bitrate; - ret->max_bitrate = ref->max_bitrate; - - return (GstClapperStreamInfo *) ret; -} - -static GstClapperStreamInfo * -gst_clapper_audio_info_copy (GstClapperAudioInfo * ref) -{ - GstClapperAudioInfo *ret; - - ret = gst_clapper_audio_info_new (); - - ret->sample_rate = ref->sample_rate; - ret->channels = ref->channels; - ret->bitrate = ref->bitrate; - ret->max_bitrate = ref->max_bitrate; - - if (ref->language) - ret->language = g_strdup (ref->language); - - return (GstClapperStreamInfo *) ret; -} - -static GstClapperStreamInfo * -gst_clapper_subtitle_info_copy (GstClapperSubtitleInfo * ref) -{ - GstClapperSubtitleInfo *ret; - - ret = gst_clapper_subtitle_info_new (); - if (ref->title) - ret->title = g_strdup (ref->title); - if (ref->language) - ret->language = g_strdup (ref->language); - - return (GstClapperStreamInfo *) ret; -} - -GstClapperStreamInfo * -gst_clapper_stream_info_copy (GstClapperStreamInfo * ref) -{ - GstClapperStreamInfo *info = NULL; - - if (!ref) - return NULL; - - if (GST_IS_CLAPPER_VIDEO_INFO (ref)) - info = gst_clapper_video_info_copy ((GstClapperVideoInfo *) ref); - else if (GST_IS_CLAPPER_AUDIO_INFO (ref)) - info = gst_clapper_audio_info_copy ((GstClapperAudioInfo *) ref); - else - info = gst_clapper_subtitle_info_copy ((GstClapperSubtitleInfo *) ref); - - info->stream_index = ref->stream_index; - if (ref->tags) - info->tags = gst_tag_list_ref (ref->tags); - if (ref->caps) - info->caps = gst_caps_copy (ref->caps); - if (ref->codec) - info->codec = g_strdup (ref->codec); - if (ref->stream_id) - info->stream_id = g_strdup (ref->stream_id); - - return info; -} - -GstClapperMediaInfo * -gst_clapper_media_info_copy (GstClapperMediaInfo * ref) -{ - GList *l; - GstClapperMediaInfo *info; - - if (!ref) - return NULL; - - info = gst_clapper_media_info_new (ref->uri); - info->duration = ref->duration; - info->seekable = ref->seekable; - info->is_live = ref->is_live; - if (ref->tags) - info->tags = gst_tag_list_ref (ref->tags); - if (ref->toc) - info->toc = gst_toc_ref (ref->toc); - if (ref->title) - info->title = g_strdup (ref->title); - if (ref->container) - info->container = g_strdup (ref->container); - if (ref->image_sample) - info->image_sample = gst_sample_ref (ref->image_sample); - - for (l = ref->stream_list; l != NULL; l = l->next) { - GstClapperStreamInfo *s; - - s = gst_clapper_stream_info_copy ((GstClapperStreamInfo *) l->data); - info->stream_list = g_list_append (info->stream_list, s); - - if (GST_IS_CLAPPER_AUDIO_INFO (s)) - info->audio_stream_list = g_list_append (info->audio_stream_list, s); - else if (GST_IS_CLAPPER_VIDEO_INFO (s)) - info->video_stream_list = g_list_append (info->video_stream_list, s); - else - info->subtitle_stream_list = - g_list_append (info->subtitle_stream_list, s); - } - - return info; -} - -GstClapperStreamInfo * -gst_clapper_stream_info_new (gint stream_index, GType type) -{ - GstClapperStreamInfo *info = NULL; - - if (type == GST_TYPE_CLAPPER_AUDIO_INFO) - info = (GstClapperStreamInfo *) gst_clapper_audio_info_new (); - else if (type == GST_TYPE_CLAPPER_VIDEO_INFO) - info = (GstClapperStreamInfo *) gst_clapper_video_info_new (); - else - info = (GstClapperStreamInfo *) gst_clapper_subtitle_info_new (); - - info->stream_index = stream_index; - - return info; -} - -GstClapperMediaInfo * -gst_clapper_media_info_new (const gchar * uri) -{ - GstClapperMediaInfo *info; - - g_return_val_if_fail (uri != NULL, NULL); - - info = g_object_new (GST_TYPE_CLAPPER_MEDIA_INFO, NULL); - info->uri = g_strdup (uri); - - return info; -} - -/** - * gst_clapper_media_info_get_uri: - * @info: a #GstClapperMediaInfo - * - * Returns: the URI associated with #GstClapperMediaInfo. - */ -const gchar * -gst_clapper_media_info_get_uri (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->uri; -} - -/** - * gst_clapper_media_info_is_seekable: - * @info: a #GstClapperMediaInfo - * - * Returns: %TRUE if the media is seekable. - */ -gboolean -gst_clapper_media_info_is_seekable (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), FALSE); - - return info->seekable; -} - -/** - * gst_clapper_media_info_is_live: - * @info: a #GstClapperMediaInfo - * - * Returns: %TRUE if the media is live. - */ -gboolean -gst_clapper_media_info_is_live (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), FALSE); - - return info->is_live; -} - -/** - * gst_clapper_media_info_get_stream_list: - * @info: a #GstClapperMediaInfo - * - * Returns: (transfer none) (element-type GstClapperStreamInfo): A #GList of - * matching #GstClapperStreamInfo. - */ -GList * -gst_clapper_media_info_get_stream_list (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->stream_list; -} - -/** - * gst_clapper_media_info_get_video_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: (transfer none) (element-type GstClapperVideoInfo): A #GList of - * matching #GstClapperVideoInfo. - */ -GList * -gst_clapper_media_info_get_video_streams (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->video_stream_list; -} - -/** - * gst_clapper_media_info_get_subtitle_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: (transfer none) (element-type GstClapperSubtitleInfo): A #GList of - * matching #GstClapperSubtitleInfo. - */ -GList * -gst_clapper_media_info_get_subtitle_streams (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->subtitle_stream_list; -} - -/** - * gst_clapper_media_info_get_audio_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: (transfer none) (element-type GstClapperAudioInfo): A #GList of - * matching #GstClapperAudioInfo. - */ -GList * -gst_clapper_media_info_get_audio_streams (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->audio_stream_list; -} - -/** - * gst_clapper_media_info_get_duration: - * @info: a #GstClapperMediaInfo - * - * Returns: duration of the media. - */ -GstClockTime -gst_clapper_media_info_get_duration (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), -1); - - return info->duration; -} - -/** - * gst_clapper_media_info_get_tags: - * @info: a #GstClapperMediaInfo - * - * Returns: (transfer none): the tags contained in media info. - */ -GstTagList * -gst_clapper_media_info_get_tags (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->tags; -} - -/** - * gst_clapper_media_info_get_toc: - * @info: a #GstClapperMediaInfo - * - * Returns: (transfer none): the toc contained in media info. - */ -GstToc * -gst_clapper_media_info_get_toc (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->toc; -} - -/** - * gst_clapper_media_info_get_title: - * @info: a #GstClapperMediaInfo - * - * Returns: the media title. When metadata does not contain title, - * returns title parsed from URI. - */ -const gchar * -gst_clapper_media_info_get_title (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->title; -} - -/** - * gst_clapper_media_info_get_container_format: - * @info: a #GstClapperMediaInfo - * - * Returns: the container format. - */ -const gchar * -gst_clapper_media_info_get_container_format (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->container; -} - -/** - * gst_clapper_media_info_get_image_sample: - * @info: a #GstClapperMediaInfo - * - * Function to get the image (or preview-image) stored in taglist. - * Application can use `gst_sample_*_()` API's to get caps, buffer etc. - * - * Returns: (transfer none): GstSample or NULL. - */ -GstSample * -gst_clapper_media_info_get_image_sample (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), NULL); - - return info->image_sample; -} - -/** - * gst_clapper_media_info_get_number_of_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: number of total streams. - */ -guint -gst_clapper_media_info_get_number_of_streams (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), 0); - - return g_list_length (info->stream_list); -} - -/** - * gst_clapper_media_info_get_number_of_video_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: number of video streams. - */ -guint -gst_clapper_media_info_get_number_of_video_streams (const GstClapperMediaInfo * - info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), 0); - - return g_list_length (info->video_stream_list); -} - -/** - * gst_clapper_media_info_get_number_of_audio_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: number of audio streams. - */ -guint -gst_clapper_media_info_get_number_of_audio_streams (const GstClapperMediaInfo * - info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), 0); - - return g_list_length (info->audio_stream_list); -} - -/** - * gst_clapper_media_info_get_number_of_subtitle_streams: - * @info: a #GstClapperMediaInfo - * - * Returns: number of subtitle streams. - */ -guint gst_clapper_media_info_get_number_of_subtitle_streams - (const GstClapperMediaInfo * info) -{ - g_return_val_if_fail (GST_IS_CLAPPER_MEDIA_INFO (info), 0); - - return g_list_length (info->subtitle_stream_list); -} diff --git a/lib/gst/clapper/gstclapper-media-info.h b/lib/gst/clapper/gstclapper-media-info.h deleted file mode 100644 index e0359d35..00000000 --- a/lib/gst/clapper/gstclapper-media-info.h +++ /dev/null @@ -1,253 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2015 Brijesh Singh - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_MEDIA_INFO_H__ -#define __GST_CLAPPER_MEDIA_INFO_H__ - -#include -#include - -G_BEGIN_DECLS - -#define GST_TYPE_CLAPPER_STREAM_INFO \ - (gst_clapper_stream_info_get_type ()) -#define GST_CLAPPER_STREAM_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_STREAM_INFO,GstClapperStreamInfo)) -#define GST_CLAPPER_STREAM_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_CLAPPER_STREAM_INFO,GstClapperStreamInfo)) -#define GST_IS_CLAPPER_STREAM_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_STREAM_INFO)) -#define GST_IS_CLAPPER_STREAM_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_CLAPPER_STREAM_INFO)) - -/** - * GstClapperStreamInfo: - * - * Base structure for information concerning a media stream. Depending on - * the stream type, one can find more media-specific information in - * #GstClapperVideoInfo, #GstClapperAudioInfo, #GstClapperSubtitleInfo. - */ -typedef struct _GstClapperStreamInfo GstClapperStreamInfo; -typedef struct _GstClapperStreamInfoClass GstClapperStreamInfoClass; - -GST_CLAPPER_API -GType gst_clapper_stream_info_get_type (void); - -GST_CLAPPER_API -gint gst_clapper_stream_info_get_index (const GstClapperStreamInfo *info); - -GST_CLAPPER_API -const gchar* gst_clapper_stream_info_get_stream_type (const GstClapperStreamInfo *info); - -GST_CLAPPER_API -GstTagList* gst_clapper_stream_info_get_tags (const GstClapperStreamInfo *info); - -GST_CLAPPER_API -GstCaps* gst_clapper_stream_info_get_caps (const GstClapperStreamInfo *info); - -GST_CLAPPER_API -const gchar* gst_clapper_stream_info_get_codec (const GstClapperStreamInfo *info); - -#define GST_TYPE_CLAPPER_VIDEO_INFO \ - (gst_clapper_video_info_get_type ()) -#define GST_CLAPPER_VIDEO_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_VIDEO_INFO, GstClapperVideoInfo)) -#define GST_CLAPPER_VIDEO_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((obj),GST_TYPE_CLAPPER_VIDEO_INFO, GstClapperVideoInfoClass)) -#define GST_IS_CLAPPER_VIDEO_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_VIDEO_INFO)) -#define GST_IS_CLAPPER_VIDEO_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE((obj),GST_TYPE_CLAPPER_VIDEO_INFO)) - -/** - * GstClapperVideoInfo: - * - * #GstClapperStreamInfo specific to video streams. - */ -typedef struct _GstClapperVideoInfo GstClapperVideoInfo; -typedef struct _GstClapperVideoInfoClass GstClapperVideoInfoClass; - -GST_CLAPPER_API -GType gst_clapper_video_info_get_type (void); - -GST_CLAPPER_API -gint gst_clapper_video_info_get_bitrate (const GstClapperVideoInfo * info); - -GST_CLAPPER_API -gint gst_clapper_video_info_get_max_bitrate (const GstClapperVideoInfo * info); - -GST_CLAPPER_API -gint gst_clapper_video_info_get_width (const GstClapperVideoInfo * info); - -GST_CLAPPER_API -gint gst_clapper_video_info_get_height (const GstClapperVideoInfo * info); - -GST_CLAPPER_API -void gst_clapper_video_info_get_framerate (const GstClapperVideoInfo * info, - gint * fps_n, - gint * fps_d); - -GST_CLAPPER_API -void gst_clapper_video_info_get_pixel_aspect_ratio (const GstClapperVideoInfo * info, - guint * par_n, - guint * par_d); - -#define GST_TYPE_CLAPPER_AUDIO_INFO \ - (gst_clapper_audio_info_get_type ()) -#define GST_CLAPPER_AUDIO_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_AUDIO_INFO, GstClapperAudioInfo)) -#define GST_CLAPPER_AUDIO_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_CLAPPER_AUDIO_INFO, GstClapperAudioInfoClass)) -#define GST_IS_CLAPPER_AUDIO_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_AUDIO_INFO)) -#define GST_IS_CLAPPER_AUDIO_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_CLAPPER_AUDIO_INFO)) - -/** - * GstClapperAudioInfo: - * - * #GstClapperStreamInfo specific to audio streams. - */ -typedef struct _GstClapperAudioInfo GstClapperAudioInfo; -typedef struct _GstClapperAudioInfoClass GstClapperAudioInfoClass; - -GST_CLAPPER_API -GType gst_clapper_audio_info_get_type (void); - -GST_CLAPPER_API -gint gst_clapper_audio_info_get_channels (const GstClapperAudioInfo* info); - -GST_CLAPPER_API -gint gst_clapper_audio_info_get_sample_rate (const GstClapperAudioInfo* info); - -GST_CLAPPER_API -gint gst_clapper_audio_info_get_bitrate (const GstClapperAudioInfo* info); - -GST_CLAPPER_API -gint gst_clapper_audio_info_get_max_bitrate (const GstClapperAudioInfo* info); - -GST_CLAPPER_API -const gchar* gst_clapper_audio_info_get_language (const GstClapperAudioInfo* info); - -#define GST_TYPE_CLAPPER_SUBTITLE_INFO \ - (gst_clapper_subtitle_info_get_type ()) -#define GST_CLAPPER_SUBTITLE_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_SUBTITLE_INFO, GstClapperSubtitleInfo)) -#define GST_CLAPPER_SUBTITLE_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_CLAPPER_SUBTITLE_INFO,GstClapperSubtitleInfoClass)) -#define GST_IS_CLAPPER_SUBTITLE_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_SUBTITLE_INFO)) -#define GST_IS_CLAPPER_SUBTITLE_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_CLAPPER_SUBTITLE_INFO)) - -/** - * GstClapperSubtitleInfo: - * - * #GstClapperStreamInfo specific to subtitle streams. - */ -typedef struct _GstClapperSubtitleInfo GstClapperSubtitleInfo; -typedef struct _GstClapperSubtitleInfoClass GstClapperSubtitleInfoClass; - -GST_CLAPPER_API -GType gst_clapper_subtitle_info_get_type (void); - -GST_CLAPPER_API -const gchar * gst_clapper_subtitle_info_get_title (const GstClapperSubtitleInfo *info); - -GST_CLAPPER_API -const gchar * gst_clapper_subtitle_info_get_language (const GstClapperSubtitleInfo *info); - -#define GST_TYPE_CLAPPER_MEDIA_INFO \ - (gst_clapper_media_info_get_type()) -#define GST_CLAPPER_MEDIA_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_MEDIA_INFO,GstClapperMediaInfo)) -#define GST_CLAPPER_MEDIA_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_CLAPPER_MEDIA_INFO,GstClapperMediaInfoClass)) -#define GST_IS_CLAPPER_MEDIA_INFO(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_MEDIA_INFO)) -#define GST_IS_CLAPPER_MEDIA_INFO_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_CLAPPER_MEDIA_INFO)) - -/** - * GstClapperMediaInfo: - * - * Structure containing the media information of a URI. - */ -typedef struct _GstClapperMediaInfo GstClapperMediaInfo; -typedef struct _GstClapperMediaInfoClass GstClapperMediaInfoClass; - -GST_CLAPPER_API -GType gst_clapper_media_info_get_type (void); - -GST_CLAPPER_API -const gchar * gst_clapper_media_info_get_uri (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -gboolean gst_clapper_media_info_is_seekable (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -gboolean gst_clapper_media_info_is_live (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GstClockTime gst_clapper_media_info_get_duration (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GList * gst_clapper_media_info_get_stream_list (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -guint gst_clapper_media_info_get_number_of_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GList * gst_clapper_media_info_get_video_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -guint gst_clapper_media_info_get_number_of_video_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GList * gst_clapper_media_info_get_audio_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -guint gst_clapper_media_info_get_number_of_audio_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GList * gst_clapper_media_info_get_subtitle_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -guint gst_clapper_media_info_get_number_of_subtitle_streams (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GstTagList * gst_clapper_media_info_get_tags (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GstToc * gst_clapper_media_info_get_toc (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -const gchar * gst_clapper_media_info_get_title (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -const gchar * gst_clapper_media_info_get_container_format (const GstClapperMediaInfo *info); - -GST_CLAPPER_API -GstSample * gst_clapper_media_info_get_image_sample (const GstClapperMediaInfo *info); - -G_END_DECLS - -#endif /* __GST_CLAPPER_MEDIA_INFO_H */ diff --git a/lib/gst/clapper/gstclapper-mpris-private.h b/lib/gst/clapper/gstclapper-mpris-private.h deleted file mode 100644 index 1a16d94c..00000000 --- a/lib/gst/clapper/gstclapper-mpris-private.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_MPRIS_PRIVATE_H__ -#define __GST_CLAPPER_MPRIS_PRIVATE_H__ - -#include -#include - -G_BEGIN_DECLS - -G_GNUC_INTERNAL -void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstClapper *clapper, - GstClapperSignalDispatcher *signal_dispatcher); - -G_GNUC_INTERNAL -void gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info); - -G_GNUC_INTERNAL -void gst_clapper_mpris_set_playback_status (GstClapperMpris *self, const gchar *status); - -G_GNUC_INTERNAL -void gst_clapper_mpris_set_position (GstClapperMpris *self, gint64 position); - -G_END_DECLS - -#endif /* __GST_CLAPPER_MPRIS_PRIVATE_H__ */ diff --git a/lib/gst/clapper/gstclapper-mpris.c b/lib/gst/clapper/gstclapper-mpris.c deleted file mode 100644 index 66dcb688..00000000 --- a/lib/gst/clapper/gstclapper-mpris.c +++ /dev/null @@ -1,793 +0,0 @@ -/* - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-mpris-gdbus.h" -#include "gstclapper-mpris.h" -#include "gstclapper-mpris-private.h" -#include "gstclapper-signal-dispatcher-private.h" - -GST_DEBUG_CATEGORY_STATIC (gst_clapper_mpris_debug); -#define GST_CAT_DEFAULT gst_clapper_mpris_debug - -#define MPRIS_DEFAULT_VOLUME 1.0 - -enum -{ - PROP_0, - PROP_OWN_NAME, - PROP_ID_PATH, - PROP_IDENTITY, - PROP_DESKTOP_ENTRY, - PROP_DEFAULT_ART_URL, - PROP_VOLUME, - PROP_LAST -}; - -struct _GstClapperMpris -{ - GObject parent; - - GstClapperMprisMediaPlayer2 *base_skeleton; - GstClapperMprisMediaPlayer2Player *player_skeleton; - - GstClapperSignalDispatcher *signal_dispatcher; - GstClapperMediaInfo *media_info; - - guint name_id; - - /* Properties */ - gchar *own_name; - gchar *id_path; - gchar *identity; - gchar *desktop_entry; - gchar *default_art_url; - - gboolean parse_media_info; - - /* Current status */ - gchar *playback_status; - gboolean can_play; - guint64 position; - - GThread *thread; - GMutex lock; - GCond cond; - GMainContext *context; - GMainLoop *loop; -}; - -struct _GstClapperMprisClass -{ - GObjectClass parent_class; -}; - -#define parent_class gst_clapper_mpris_parent_class -G_DEFINE_TYPE (GstClapperMpris, gst_clapper_mpris, G_TYPE_OBJECT); - -static GParamSpec *param_specs[PROP_LAST] = { NULL, }; - -static void gst_clapper_mpris_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec); -static void gst_clapper_mpris_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec); -static void gst_clapper_mpris_dispose (GObject * object); -static void gst_clapper_mpris_finalize (GObject * object); -static void gst_clapper_mpris_constructed (GObject * object); -static gpointer gst_clapper_mpris_main (gpointer data); - -static void unregister (GstClapperMpris * self); - -static void -gst_clapper_mpris_init (GstClapperMpris * self) -{ - GST_DEBUG_CATEGORY_INIT (gst_clapper_mpris_debug, "ClapperMpris", 0, - "GstClapperMpris"); - GST_TRACE_OBJECT (self, "Initializing"); - - self = gst_clapper_mpris_get_instance_private (self); - - g_mutex_init (&self->lock); - g_cond_init (&self->cond); - - self->context = g_main_context_new (); - self->loop = g_main_loop_new (self->context, FALSE); - - self->base_skeleton = gst_clapper_mpris_media_player2_skeleton_new (); - self->player_skeleton = gst_clapper_mpris_media_player2_player_skeleton_new (); - - self->name_id = 0; - self->own_name = NULL; - self->id_path = NULL; - self->identity = NULL; - self->desktop_entry = NULL; - self->default_art_url = NULL; - - self->signal_dispatcher = NULL; - self->media_info = NULL; - self->parse_media_info = FALSE; - - self->playback_status = g_strdup ("Stopped"); - self->can_play = FALSE; - self->position = 0; - - GST_TRACE_OBJECT (self, "Initialized"); -} - -static void -gst_clapper_mpris_class_init (GstClapperMprisClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->set_property = gst_clapper_mpris_set_property; - gobject_class->get_property = gst_clapper_mpris_get_property; - gobject_class->dispose = gst_clapper_mpris_dispose; - gobject_class->finalize = gst_clapper_mpris_finalize; - gobject_class->constructed = gst_clapper_mpris_constructed; - - param_specs[PROP_OWN_NAME] = - g_param_spec_string ("own-name", "DBus own name", - "DBus name to own on connection", - NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_ID_PATH] = - g_param_spec_string ("id-path", "DBus id path", - "A valid D-Bus path describing this player", - NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_IDENTITY] = - g_param_spec_string ("identity", "Player name", - "A friendly name to identify the media player", - NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_DESKTOP_ENTRY] = - g_param_spec_string ("desktop-entry", "Desktop entry filename", - "The basename of an installed .desktop file", - NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_DEFAULT_ART_URL] = - g_param_spec_string ("default-art-url", "Default Art URL", - "Default art to show when media does not provide one", - NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_VOLUME] = - g_param_spec_double ("volume", "Volume", "Volume", - 0, 1.5, MPRIS_DEFAULT_VOLUME, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); -} - -static void -gst_clapper_mpris_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (object); - - switch (prop_id) { - case PROP_OWN_NAME: - self->own_name = g_value_dup_string (value); - break; - case PROP_ID_PATH: - self->id_path = g_value_dup_string (value); - break; - case PROP_IDENTITY: - self->identity = g_value_dup_string (value); - break; - case PROP_DESKTOP_ENTRY: - self->desktop_entry = g_value_dup_string (value); - break; - case PROP_DEFAULT_ART_URL: - self->default_art_url = g_value_dup_string (value); - break; - case PROP_VOLUME: - g_object_set_property (G_OBJECT (self->player_skeleton), "volume", value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_mpris_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (object); - - switch (prop_id) { - case PROP_OWN_NAME: - g_value_set_string (value, self->own_name); - break; - case PROP_ID_PATH: - g_value_set_string (value, self->id_path); - break; - case PROP_IDENTITY: - g_value_set_string (value, self->identity); - break; - case PROP_DESKTOP_ENTRY: - g_value_set_string (value, self->desktop_entry); - break; - case PROP_DEFAULT_ART_URL: - g_value_set_string (value, self->default_art_url); - break; - case PROP_VOLUME: - g_object_get_property (G_OBJECT (self->player_skeleton), "volume", value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_mpris_dispose (GObject * object) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (object); - - GST_TRACE_OBJECT (self, "Stopping main thread"); - - if (self->loop) { - g_main_loop_quit (self->loop); - - if (self->thread != g_thread_self ()) - g_thread_join (self->thread); - else - g_thread_unref (self->thread); - self->thread = NULL; - - g_main_loop_unref (self->loop); - self->loop = NULL; - - g_main_context_unref (self->context); - self->context = NULL; - } - - G_OBJECT_CLASS (parent_class)->dispose (object); -} - -static void -gst_clapper_mpris_finalize (GObject * object) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (object); - - GST_TRACE_OBJECT (self, "Finalize"); - - g_free (self->own_name); - g_free (self->id_path); - g_free (self->identity); - g_free (self->desktop_entry); - g_free (self->default_art_url); - g_free (self->playback_status); - - if (self->base_skeleton) - g_object_unref (self->base_skeleton); - if (self->player_skeleton) - g_object_unref (self->player_skeleton); - if (self->signal_dispatcher) - g_object_unref (self->signal_dispatcher); - if (self->media_info) - g_object_unref (self->media_info); - - g_mutex_clear (&self->lock); - g_cond_clear (&self->cond); - - G_OBJECT_CLASS (parent_class)->finalize (object); -} - -static void -gst_clapper_mpris_constructed (GObject * object) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (object); - - GST_TRACE_OBJECT (self, "Constructed"); - - g_mutex_lock (&self->lock); - self->thread = g_thread_new ("GstClapperMpris", - gst_clapper_mpris_main, self); - while (!self->loop || !g_main_loop_is_running (self->loop)) - g_cond_wait (&self->cond, &self->lock); - g_mutex_unlock (&self->lock); - - G_OBJECT_CLASS (parent_class)->constructed (object); -} - -static gboolean -main_loop_running_cb (gpointer user_data) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); - - GST_TRACE_OBJECT (self, "Main loop running now"); - - g_mutex_lock (&self->lock); - g_cond_signal (&self->cond); - g_mutex_unlock (&self->lock); - - return G_SOURCE_REMOVE; -} - -static gboolean -handle_play_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, - GDBusMethodInvocation * invocation, gpointer user_data) -{ - GstClapper *clapper = GST_CLAPPER (user_data); - - GST_DEBUG ("Handle Play"); - - gst_clapper_play (clapper); - gst_clapper_mpris_media_player2_player_complete_play (player_skeleton, invocation); - - return TRUE; -} - -static gboolean -handle_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, - GDBusMethodInvocation * invocation, gpointer user_data) -{ - GstClapper *clapper = GST_CLAPPER (user_data); - - GST_DEBUG ("Handle Pause"); - - gst_clapper_pause (clapper); - gst_clapper_mpris_media_player2_player_complete_pause (player_skeleton, invocation); - - return TRUE; -} - -static gboolean -handle_play_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, - GDBusMethodInvocation * invocation, gpointer user_data) -{ - GstClapper *clapper = GST_CLAPPER (user_data); - - GST_DEBUG ("Handle PlayPause"); - - gst_clapper_toggle_play (clapper); - gst_clapper_mpris_media_player2_player_complete_play_pause (player_skeleton, invocation); - - return TRUE; -} - -static gboolean -handle_seek_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, - GDBusMethodInvocation * invocation, gint64 offset, gpointer user_data) -{ - GstClapper *clapper = GST_CLAPPER (user_data); - - GST_DEBUG ("Handle Seek"); - - gst_clapper_seek_offset (clapper, offset * GST_USECOND); - gst_clapper_mpris_media_player2_player_complete_seek (player_skeleton, invocation); - - return TRUE; -} - -static gboolean -handle_set_position_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, - GDBusMethodInvocation * invocation, const gchar * track_id, - gint64 position, gpointer user_data) -{ - GstClapper *clapper = GST_CLAPPER (user_data); - - GST_DEBUG ("Handle SetPosition"); - - gst_clapper_seek (clapper, position * GST_USECOND); - gst_clapper_mpris_media_player2_player_complete_set_position (player_skeleton, invocation); - - return TRUE; -} - -static gboolean -handle_open_uri_cb (GstClapperMprisMediaPlayer2Player * player_skeleton, - GDBusMethodInvocation * invocation, const gchar * uri, - gpointer user_data) -{ - GstClapper *clapper = GST_CLAPPER (user_data); - - GST_DEBUG ("Handle OpenUri"); - - /* FIXME: set one item playlist instead */ - gst_clapper_set_uri (clapper, uri); - gst_clapper_mpris_media_player2_player_complete_open_uri (player_skeleton, invocation); - - return TRUE; -} - -static void -volume_notify_dispatch (gpointer user_data) -{ - GstClapperMpris *self = user_data; - - g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_VOLUME]); -} - -static void -handle_volume_notify_cb (G_GNUC_UNUSED GObject * obj, - G_GNUC_UNUSED GParamSpec * pspec, GstClapperMpris * self) -{ - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, NULL, - volume_notify_dispatch, g_object_ref (self), - (GDestroyNotify) g_object_unref); -} - -static void -unregister (GstClapperMpris * self) -{ - if (!self->name_id) - return; - - GST_DEBUG_OBJECT (self, "Unregister"); - g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->base_skeleton)); - g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->player_skeleton)); - g_bus_unown_name (self->name_id); - self->name_id = 0; -} - -static const gchar * -_get_mpris_trackid (GstClapperMpris * self) -{ - /* TODO: Support more tracks */ - return g_strdup_printf ("%s%s%i", self->id_path, "/Track/", 0); -} - -static void -_set_supported_uri_schemes (GstClapperMpris * self) -{ - const gchar *uri_schemes[96] = {}; - GList *elements, *el; - guint index = 0; - - elements = gst_element_factory_list_get_elements ( - GST_ELEMENT_FACTORY_TYPE_SRC, GST_RANK_NONE); - - for (el = elements; el != NULL; el = el->next) { - const gchar *const *protocols; - GstElementFactory *factory = GST_ELEMENT_FACTORY (el->data); - - if (gst_element_factory_get_uri_type (factory) != GST_URI_SRC) - continue; - - protocols = gst_element_factory_get_uri_protocols (factory); - if (protocols == NULL || *protocols == NULL) - continue; - - while (*protocols != NULL) { - guint j = index; - - while (j--) { - if (strcmp (uri_schemes[j], *protocols) == 0) - goto next; - } - uri_schemes[index] = *protocols; - GST_DEBUG_OBJECT (self, "Added supported URI scheme: %s", *protocols); - ++index; -next: - ++protocols; - } - } - gst_plugin_feature_list_free (elements); - - gst_clapper_mpris_media_player2_set_supported_uri_schemes ( - self->base_skeleton, uri_schemes); -} - -static void -name_acquired_cb (GDBusConnection * connection, - const gchar *name, gpointer user_data) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); - GVariantBuilder builder; - - g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->base_skeleton), - connection, "/org/mpris/MediaPlayer2", NULL); - g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->player_skeleton), - connection, "/org/mpris/MediaPlayer2", NULL); - - if (self->identity) - gst_clapper_mpris_media_player2_set_identity (self->base_skeleton, self->identity); - if (self->desktop_entry) - gst_clapper_mpris_media_player2_set_desktop_entry (self->base_skeleton, self->desktop_entry); - - _set_supported_uri_schemes (self); - - gst_clapper_mpris_media_player2_player_set_playback_status (self->player_skeleton, "Stopped"); - gst_clapper_mpris_media_player2_player_set_minimum_rate (self->player_skeleton, 0.01); - gst_clapper_mpris_media_player2_player_set_maximum_rate (self->player_skeleton, 2.0); - gst_clapper_mpris_media_player2_player_set_can_seek (self->player_skeleton, TRUE); - gst_clapper_mpris_media_player2_player_set_can_control (self->player_skeleton, TRUE); - - g_object_bind_property (self->player_skeleton, "can-play", - self->player_skeleton, "can-pause", G_BINDING_DEFAULT); - - g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); - g_variant_builder_add (&builder, "{sv}", "mpris:trackid", g_variant_new_string (_get_mpris_trackid (self))); - g_variant_builder_add (&builder, "{sv}", "mpris:length", g_variant_new_uint64 (0)); - if (self->default_art_url) - g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", g_variant_new_string (self->default_art_url)); - gst_clapper_mpris_media_player2_player_set_metadata (self->player_skeleton, g_variant_builder_end (&builder)); - - GST_DEBUG_OBJECT (self, "Ready"); -} - -static void -name_lost_cb (GDBusConnection * connection, - const gchar * name, gpointer user_data) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); - - unregister (self); -} - -static gboolean -mpris_update_props_dispatch (gpointer user_data) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data); - - GST_DEBUG_OBJECT (self, "Updating MPRIS props"); - g_mutex_lock (&self->lock); - - if (self->parse_media_info) { - GVariantBuilder builder; - guint64 duration; - const gchar *track_id, *uri, *title; - - GST_DEBUG_OBJECT (self, "Parsing media info"); - g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); - - track_id = _get_mpris_trackid (self); - uri = gst_clapper_media_info_get_uri (self->media_info); - title = gst_clapper_media_info_get_title (self->media_info); - - if (track_id) { - g_variant_builder_add (&builder, "{sv}", "mpris:trackid", - g_variant_new_string (track_id)); - GST_DEBUG_OBJECT (self, "mpris:trackid: %s", track_id); - } - if (uri) { - g_variant_builder_add (&builder, "{sv}", "xesam:url", - g_variant_new_string (uri)); - GST_DEBUG_OBJECT (self, "xesam:url: %s", uri); - } - if (title) { - g_variant_builder_add (&builder, "{sv}", "xesam:title", - g_variant_new_string (title)); - GST_DEBUG_OBJECT (self, "xesam:title: %s", title); - } - - duration = gst_clapper_media_info_get_duration (self->media_info); - duration = (duration != GST_CLOCK_TIME_NONE) ? duration / GST_USECOND : 0; - g_variant_builder_add (&builder, "{sv}", "mpris:length", g_variant_new_uint64 (duration)); - GST_DEBUG_OBJECT (self, "mpris:length: %ld", duration); - - /* TODO: Check for image sample */ - if (self->default_art_url) { - g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", g_variant_new_string (self->default_art_url)); - GST_DEBUG_OBJECT (self, "mpris:artUrl: %s", self->default_art_url); - } - - GST_DEBUG_OBJECT (self, "Media info parsed"); - self->parse_media_info = FALSE; - - gst_clapper_mpris_media_player2_player_set_metadata ( - self->player_skeleton, g_variant_builder_end (&builder)); - } - if (gst_clapper_mpris_media_player2_player_get_can_play ( - self->player_skeleton) != self->can_play) { - /* "can-play" is bound with "can-pause" */ - gst_clapper_mpris_media_player2_player_set_can_play ( - self->player_skeleton, self->can_play); - GST_DEBUG_OBJECT (self, "CanPlay/CanPause: %s", self->can_play ? "yes" : "no"); - } - if (strcmp (gst_clapper_mpris_media_player2_player_get_playback_status ( - self->player_skeleton), self->playback_status) != 0) { - gst_clapper_mpris_media_player2_player_set_playback_status ( - self->player_skeleton, self->playback_status); - GST_DEBUG_OBJECT (self, "PlaybackStatus: %s", self->playback_status); - } - if (gst_clapper_mpris_media_player2_player_get_position ( - self->player_skeleton) != self->position) { - gst_clapper_mpris_media_player2_player_set_position ( - self->player_skeleton, self->position); - GST_DEBUG_OBJECT (self, "Position: %ld", self->position); - } - - g_mutex_unlock (&self->lock); - GST_DEBUG_OBJECT (self, "MPRIS props updated"); - - return G_SOURCE_REMOVE; -} - -static void -mpris_dispatcher_update_dispatch (GstClapperMpris * self) -{ - if (!self->name_id) - return; - - GST_DEBUG_OBJECT (self, "Queued update props dispatch"); - - g_main_context_invoke_full (self->context, - G_PRIORITY_DEFAULT, mpris_update_props_dispatch, - g_object_ref (self), g_object_unref); -} - -static gpointer -gst_clapper_mpris_main (gpointer data) -{ - GstClapperMpris *self = GST_CLAPPER_MPRIS (data); - - GDBusConnectionFlags flags; - GDBusConnection *connection; - GSource *source; - gchar *address; - - GST_TRACE_OBJECT (self, "Starting main thread"); - - g_main_context_push_thread_default (self->context); - - source = g_idle_source_new (); - g_source_set_callback (source, (GSourceFunc) main_loop_running_cb, self, - NULL); - g_source_attach (source, self->context); - g_source_unref (source); - - address = g_dbus_address_get_for_bus_sync (G_BUS_TYPE_SESSION, NULL, NULL); - if (!address) { - GST_WARNING_OBJECT (self, "No MPRIS bus address"); - goto no_mpris; - } - - GST_DEBUG_OBJECT (self, "Obtained MPRIS DBus address"); - - flags = G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | - G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION; - connection = g_dbus_connection_new_for_address_sync (address, - flags, NULL, NULL, NULL); - g_free (address); - - if (!connection) { - GST_WARNING_OBJECT (self, "No MPRIS bus connection"); - goto no_mpris; - } - - GST_DEBUG_OBJECT (self, "Obtained MPRIS DBus connection"); - - self->name_id = g_bus_own_name_on_connection (connection, self->own_name, - G_BUS_NAME_OWNER_FLAGS_NONE, - (GBusNameAcquiredCallback) name_acquired_cb, - (GBusNameLostCallback) name_lost_cb, - self, NULL); - g_object_unref (connection); - goto done; - -no_mpris: - g_warning ("GstClapperMpris: failed to create DBus connection"); - -done: - GST_TRACE_OBJECT (self, "Starting main loop"); - g_main_loop_run (self->loop); - GST_TRACE_OBJECT (self, "Stopped main loop"); - - unregister (self); - g_main_context_pop_thread_default (self->context); - - GST_TRACE_OBJECT (self, "Stopped main thread"); - - return NULL; -} - -void -gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper, - GstClapperSignalDispatcher * signal_dispatcher) -{ - if (signal_dispatcher) - self->signal_dispatcher = g_object_ref (signal_dispatcher); - - g_signal_connect (self->player_skeleton, "handle-play", - G_CALLBACK (handle_play_cb), clapper); - g_signal_connect (self->player_skeleton, "handle-pause", - G_CALLBACK (handle_pause_cb), clapper); - g_signal_connect (self->player_skeleton, "handle-play-pause", - G_CALLBACK (handle_play_pause_cb), clapper); - g_signal_connect (self->player_skeleton, "handle-seek", - G_CALLBACK (handle_seek_cb), clapper); - g_signal_connect (self->player_skeleton, "handle-set-position", - G_CALLBACK (handle_set_position_cb), clapper); - g_signal_connect (self->player_skeleton, "handle-open-uri", - G_CALLBACK (handle_open_uri_cb), clapper); - - g_object_bind_property (clapper, "volume", self, "volume", G_BINDING_BIDIRECTIONAL); - g_signal_connect (self->player_skeleton, "notify::volume", - G_CALLBACK (handle_volume_notify_cb), self); -} - -void -gst_clapper_mpris_set_playback_status (GstClapperMpris * self, const gchar * status) -{ - g_mutex_lock (&self->lock); - if (strcmp (self->playback_status, status) == 0) { - g_mutex_unlock (&self->lock); - return; - } - g_free (self->playback_status); - self->playback_status = g_strdup (status); - self->can_play = strcmp (status, "Stopped") != 0; - g_mutex_unlock (&self->lock); - - mpris_dispatcher_update_dispatch (self); -} - -void -gst_clapper_mpris_set_position (GstClapperMpris * self, gint64 position) -{ - position /= GST_USECOND; - - g_mutex_lock (&self->lock); - if (self->position == position) { - g_mutex_unlock (&self->lock); - return; - } - self->position = position; - g_mutex_unlock (&self->lock); - - mpris_dispatcher_update_dispatch (self); -} - -void -gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info) -{ - g_mutex_lock (&self->lock); - if (self->media_info) - g_object_unref (self->media_info); - self->media_info = info; - self->parse_media_info = TRUE; - g_mutex_unlock (&self->lock); - - mpris_dispatcher_update_dispatch (self); -} - -/** - * gst_clapper_mpris_new: - * @own_name: DBus own name - * @id_path: DBus id path used for prefix - * @identity: (allow-none): friendly name - * @desktop_entry: (allow-none): Desktop entry filename - * @default_art_url: (allow-none): filepath to default art - * - * Creates a new #GstClapperMpris instance. - * - * Returns: (transfer full): a new #GstClapperMpris instance - */ -GstClapperMpris * -gst_clapper_mpris_new (const gchar * own_name, const gchar * id_path, - const gchar * identity, const gchar * desktop_entry, - const gchar * default_art_url) -{ - return g_object_new (GST_TYPE_CLAPPER_MPRIS, - "own-name", own_name, "id_path", id_path, - "identity", identity, "desktop-entry", desktop_entry, - "default-art-url", default_art_url, NULL); -} diff --git a/lib/gst/clapper/gstclapper-mpris.h b/lib/gst/clapper/gstclapper-mpris.h deleted file mode 100644 index cf0e94b2..00000000 --- a/lib/gst/clapper/gstclapper-mpris.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_MPRIS_H__ -#define __GST_CLAPPER_MPRIS_H__ - -#include -#include - -#include - -G_BEGIN_DECLS - -typedef struct _GstClapperMpris GstClapperMpris; -typedef struct _GstClapperMprisClass GstClapperMprisClass; - -#define GST_TYPE_CLAPPER_MPRIS (gst_clapper_mpris_get_type ()) -#define GST_IS_CLAPPER_MPRIS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_MPRIS)) -#define GST_IS_CLAPPER_MPRIS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_MPRIS)) -#define GST_CLAPPER_MPRIS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_MPRIS, GstClapperMprisClass)) -#define GST_CLAPPER_MPRIS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_MPRIS, GstClapperMpris)) -#define GST_CLAPPER_MPRIS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_MPRIS, GstClapperMprisClass)) -#define GST_CLAPPER_MPRIS_CAST(obj) ((GstClapperMpris*)(obj)) - -#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC -G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstClapperMpris, g_object_unref) -#endif - -GST_CLAPPER_API -GType gst_clapper_mpris_get_type (void); - -GST_CLAPPER_API -GstClapperMpris * gst_clapper_mpris_new (const gchar *own_name, const gchar *id_path, const gchar *identity, - const gchar *desktop_entry, const gchar *default_art_url); - -G_END_DECLS - -#endif /* __GST_CLAPPER_MPRIS_H__ */ diff --git a/lib/gst/clapper/gstclapper-signal-dispatcher-private.h b/lib/gst/clapper/gstclapper-signal-dispatcher-private.h deleted file mode 100644 index 91dc88e4..00000000 --- a/lib/gst/clapper/gstclapper-signal-dispatcher-private.h +++ /dev/null @@ -1,35 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_SIGNAL_DISPATCHER_PRIVATE_H__ -#define __GST_CLAPPER_SIGNAL_DISPATCHER_PRIVATE_H__ - -#include - -G_BEGIN_DECLS - -G_GNUC_INTERNAL void gst_clapper_signal_dispatcher_dispatch (GstClapperSignalDispatcher * self, - GstClapper * clapper, GstClapperSignalDispatcherFunc emitter, gpointer data, - GDestroyNotify destroy); - -G_END_DECLS - -#endif /* __GST_CLAPPER_SIGNAL_DISPATCHER_PRIVATE_H__ */ diff --git a/lib/gst/clapper/gstclapper-signal-dispatcher.c b/lib/gst/clapper/gstclapper-signal-dispatcher.c deleted file mode 100644 index 47bbdd04..00000000 --- a/lib/gst/clapper/gstclapper-signal-dispatcher.c +++ /dev/null @@ -1,58 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-signal-dispatcher.h" -#include "gstclapper-signal-dispatcher-private.h" - -G_DEFINE_INTERFACE (GstClapperSignalDispatcher, gst_clapper_signal_dispatcher, - G_TYPE_OBJECT); - -static void -gst_clapper_signal_dispatcher_default_init (G_GNUC_UNUSED - GstClapperSignalDispatcherInterface * iface) -{ - -} - -void -gst_clapper_signal_dispatcher_dispatch (GstClapperSignalDispatcher * self, - GstClapper * clapper, GstClapperSignalDispatcherFunc emitter, gpointer data, - GDestroyNotify destroy) -{ - GstClapperSignalDispatcherInterface *iface; - - if (!self) { - emitter (data); - if (destroy) - destroy (data); - return; - } - - g_return_if_fail (GST_IS_CLAPPER_SIGNAL_DISPATCHER (self)); - iface = GST_CLAPPER_SIGNAL_DISPATCHER_GET_INTERFACE (self); - g_return_if_fail (iface->dispatch != NULL); - - iface->dispatch (self, clapper, emitter, data, destroy); -} diff --git a/lib/gst/clapper/gstclapper-signal-dispatcher.h b/lib/gst/clapper/gstclapper-signal-dispatcher.h deleted file mode 100644 index ebab4a6c..00000000 --- a/lib/gst/clapper/gstclapper-signal-dispatcher.h +++ /dev/null @@ -1,53 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_SIGNAL_DISPATCHER_H__ -#define __GST_CLAPPER_SIGNAL_DISPATCHER_H__ - -#include -#include - -G_BEGIN_DECLS - -typedef struct _GstClapperSignalDispatcher GstClapperSignalDispatcher; -typedef struct _GstClapperSignalDispatcherInterface GstClapperSignalDispatcherInterface; - -#define GST_TYPE_CLAPPER_SIGNAL_DISPATCHER (gst_clapper_signal_dispatcher_get_type ()) -#define GST_CLAPPER_SIGNAL_DISPATCHER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_SIGNAL_DISPATCHER, GstClapperSignalDispatcher)) -#define GST_IS_CLAPPER_SIGNAL_DISPATCHER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_SIGNAL_DISPATCHER)) -#define GST_CLAPPER_SIGNAL_DISPATCHER_GET_INTERFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), GST_TYPE_CLAPPER_SIGNAL_DISPATCHER, GstClapperSignalDispatcherInterface)) - -typedef void (*GstClapperSignalDispatcherFunc) (gpointer data); - -struct _GstClapperSignalDispatcherInterface { - GTypeInterface parent_iface; - - void (*dispatch) (GstClapperSignalDispatcher * self, GstClapper * clapper, - GstClapperSignalDispatcherFunc emitter, gpointer data, - GDestroyNotify destroy); -}; - -GST_CLAPPER_API -GType gst_clapper_signal_dispatcher_get_type (void); - -G_END_DECLS - -#endif /* __GST_CLAPPER_SIGNAL_DISPATCHER_H__ */ diff --git a/lib/gst/clapper/gstclapper-types.h b/lib/gst/clapper/gstclapper-types.h deleted file mode 100644 index a84429a7..00000000 --- a/lib/gst/clapper/gstclapper-types.h +++ /dev/null @@ -1,37 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_TYPES_H__ -#define __GST_CLAPPER_TYPES_H__ - -#include -#include - -G_BEGIN_DECLS - -typedef struct _GstClapper GstClapper; -typedef struct _GstClapperClass GstClapperClass; - -G_END_DECLS - -#endif /* __GST_CLAPPER_TYPES_H__ */ - - diff --git a/lib/gst/clapper/gstclapper-video-overlay-video-renderer.c b/lib/gst/clapper/gstclapper-video-overlay-video-renderer.c deleted file mode 100644 index 476184d1..00000000 --- a/lib/gst/clapper/gstclapper-video-overlay-video-renderer.c +++ /dev/null @@ -1,345 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapper-videooverlayvideorenderer - * @title: GstClapperVideoOverlayVideoRenderer - * @short_description: Clapper Video Overlay Video Renderer - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-video-overlay-video-renderer.h" -#include "gstclapper.h" - -#include - -struct _GstClapperVideoOverlayVideoRenderer -{ - GObject parent; - - GstVideoOverlay *video_overlay; - gpointer window_handle; - gint x, y, width, height; - - GstElement *video_sink; /* configured video sink, or NULL */ -}; - -struct _GstClapperVideoOverlayVideoRendererClass -{ - GObjectClass parent_class; -}; - -static void - gst_clapper_video_overlay_video_renderer_interface_init - (GstClapperVideoRendererInterface * iface); - -enum -{ - VIDEO_OVERLAY_VIDEO_RENDERER_PROP_0, - VIDEO_OVERLAY_VIDEO_RENDERER_PROP_WINDOW_HANDLE, - VIDEO_OVERLAY_VIDEO_RENDERER_PROP_VIDEO_SINK, - VIDEO_OVERLAY_VIDEO_RENDERER_PROP_LAST -}; - -G_DEFINE_TYPE_WITH_CODE (GstClapperVideoOverlayVideoRenderer, - gst_clapper_video_overlay_video_renderer, G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE (GST_TYPE_CLAPPER_VIDEO_RENDERER, - gst_clapper_video_overlay_video_renderer_interface_init)); - -static GParamSpec - * video_overlay_video_renderer_param_specs - [VIDEO_OVERLAY_VIDEO_RENDERER_PROP_LAST] = { NULL, }; - -static void -gst_clapper_video_overlay_video_renderer_set_property (GObject * object, - guint prop_id, const GValue * value, GParamSpec * pspec) -{ - GstClapperVideoOverlayVideoRenderer *self = - GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (object); - - switch (prop_id) { - case VIDEO_OVERLAY_VIDEO_RENDERER_PROP_WINDOW_HANDLE: - self->window_handle = g_value_get_pointer (value); - if (self->video_overlay) - gst_video_overlay_set_window_handle (self->video_overlay, - (guintptr) self->window_handle); - break; - case VIDEO_OVERLAY_VIDEO_RENDERER_PROP_VIDEO_SINK: - self->video_sink = gst_object_ref_sink (g_value_get_object (value)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_video_overlay_video_renderer_get_property (GObject * object, - guint prop_id, GValue * value, GParamSpec * pspec) -{ - GstClapperVideoOverlayVideoRenderer *self = - GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (object); - - switch (prop_id) { - case VIDEO_OVERLAY_VIDEO_RENDERER_PROP_WINDOW_HANDLE: - g_value_set_pointer (value, self->window_handle); - break; - case VIDEO_OVERLAY_VIDEO_RENDERER_PROP_VIDEO_SINK: - g_value_set_object (value, self->video_sink); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_video_overlay_video_renderer_finalize (GObject * object) -{ - GstClapperVideoOverlayVideoRenderer *self = - GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (object); - - if (self->video_overlay) - gst_object_unref (self->video_overlay); - - if (self->video_sink) - gst_object_unref (self->video_sink); - - G_OBJECT_CLASS - (gst_clapper_video_overlay_video_renderer_parent_class)->finalize (object); -} - -static void - gst_clapper_video_overlay_video_renderer_class_init - (GstClapperVideoOverlayVideoRendererClass * klass) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - - gobject_class->set_property = - gst_clapper_video_overlay_video_renderer_set_property; - gobject_class->get_property = - gst_clapper_video_overlay_video_renderer_get_property; - gobject_class->finalize = gst_clapper_video_overlay_video_renderer_finalize; - - video_overlay_video_renderer_param_specs - [VIDEO_OVERLAY_VIDEO_RENDERER_PROP_WINDOW_HANDLE] = - g_param_spec_pointer ("window-handle", "Window Handle", - "Window handle to embed the video into", - G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); - - video_overlay_video_renderer_param_specs - [VIDEO_OVERLAY_VIDEO_RENDERER_PROP_VIDEO_SINK] = - g_param_spec_object ("video-sink", "Video Sink", - "the video output element to use (NULL = default sink)", - GST_TYPE_ELEMENT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); - - g_object_class_install_properties (gobject_class, - VIDEO_OVERLAY_VIDEO_RENDERER_PROP_LAST, - video_overlay_video_renderer_param_specs); -} - -static void - gst_clapper_video_overlay_video_renderer_init - (GstClapperVideoOverlayVideoRenderer * self) -{ - self->x = self->y = self->width = self->height = -1; - self->video_sink = NULL; -} - -static GstElement *gst_clapper_video_overlay_video_renderer_create_video_sink - (GstClapperVideoRenderer * iface, GstClapper * clapper) -{ - GstElement *video_overlay; - GstClapperVideoOverlayVideoRenderer *self = - GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (iface); - - if (self->video_overlay) - gst_object_unref (self->video_overlay); - - video_overlay = gst_clapper_get_pipeline (clapper); - g_return_val_if_fail (GST_IS_VIDEO_OVERLAY (video_overlay), NULL); - - self->video_overlay = GST_VIDEO_OVERLAY (video_overlay); - - gst_video_overlay_set_window_handle (self->video_overlay, - (guintptr) self->window_handle); - if (self->width != -1 || self->height != -1) - gst_video_overlay_set_render_rectangle (self->video_overlay, self->x, - self->y, self->width, self->height); - - return self->video_sink; -} - -static void - gst_clapper_video_overlay_video_renderer_interface_init - (GstClapperVideoRendererInterface * iface) -{ - iface->create_video_sink = - gst_clapper_video_overlay_video_renderer_create_video_sink; -} - -/** - * gst_clapper_video_overlay_video_renderer_new: - * @window_handle: (allow-none): Window handle to use or %NULL - * - * Returns: (transfer full): - */ -GstClapperVideoRenderer * -gst_clapper_video_overlay_video_renderer_new (gpointer window_handle) -{ - return g_object_new (GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER, - "window-handle", window_handle, NULL); -} - -/** - * gst_clapper_video_overlay_video_renderer_new_with_sink: - * @window_handle: (allow-none): Window handle to use or %NULL - * @video_sink: (transfer floating): the custom video_sink element to be set for the video renderer - * - * Returns: (transfer full): clapper video renderer - */ -GstClapperVideoRenderer * -gst_clapper_video_overlay_video_renderer_new_with_sink (gpointer window_handle, - GstElement * video_sink) -{ - return g_object_new (GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER, - "window-handle", window_handle, "video-sink", video_sink, NULL); -} - -/** - * gst_clapper_video_overlay_video_renderer_set_window_handle: - * @self: #GstClapperVideoRenderer instance - * @window_handle: handle referencing to the platform specific window - * - * Sets the platform specific window handle into which the video - * should be rendered - **/ -void gst_clapper_video_overlay_video_renderer_set_window_handle - (GstClapperVideoOverlayVideoRenderer * self, gpointer window_handle) -{ - g_return_if_fail (GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (self)); - - g_object_set (self, "window-handle", window_handle, NULL); -} - -/** - * gst_clapper_video_overlay_video_renderer_get_window_handle: - * @self: #GstClapperVideoRenderer instance - * - * Returns: (transfer none): The currently set, platform specific window - * handle - */ -gpointer - gst_clapper_video_overlay_video_renderer_get_window_handle - (GstClapperVideoOverlayVideoRenderer * self) { - gpointer window_handle; - - g_return_val_if_fail (GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (self), - NULL); - - g_object_get (self, "window-handle", &window_handle, NULL); - - return window_handle; -} - -/** - * gst_clapper_video_overlay_video_renderer_expose: - * @self: a #GstClapperVideoOverlayVideoRenderer instance. - * - * Tell an overlay that it has been exposed. This will redraw the current frame - * in the drawable even if the pipeline is PAUSED. - */ -void gst_clapper_video_overlay_video_renderer_expose - (GstClapperVideoOverlayVideoRenderer * self) -{ - g_return_if_fail (GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (self)); - - if (self->video_overlay) - gst_video_overlay_expose (self->video_overlay); -} - -/** - * gst_clapper_video_overlay_video_renderer_set_render_rectangle: - * @self: a #GstClapperVideoOverlayVideoRenderer instance - * @x: the horizontal offset of the render area inside the window - * @y: the vertical offset of the render area inside the window - * @width: the width of the render area inside the window - * @height: the height of the render area inside the window - * - * Configure a subregion as a video target within the window set by - * gst_clapper_video_overlay_video_renderer_set_window_handle(). If this is not - * used or not supported the video will fill the area of the window set as the - * overlay to 100%. By specifying the rectangle, the video can be overlaid to - * a specific region of that window only. After setting the new rectangle one - * should call gst_clapper_video_overlay_video_renderer_expose() to force a - * redraw. To unset the region pass -1 for the @width and @height parameters. - * - * This method is needed for non fullscreen video overlay in UI toolkits that - * do not support subwindows. - * - */ -void gst_clapper_video_overlay_video_renderer_set_render_rectangle - (GstClapperVideoOverlayVideoRenderer * self, gint x, gint y, gint width, - gint height) -{ - g_return_if_fail (GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (self)); - - self->x = x; - self->y = y; - self->width = width; - self->height = height; - - if (self->video_overlay) - gst_video_overlay_set_render_rectangle (self->video_overlay, - x, y, width, height); -} - -/** - * gst_clapper_video_overlay_video_renderer_get_render_rectangle: - * @self: a #GstClapperVideoOverlayVideoRenderer instance - * @x: (out) (allow-none): the horizontal offset of the render area inside the window - * @y: (out) (allow-none): the vertical offset of the render area inside the window - * @width: (out) (allow-none): the width of the render area inside the window - * @height: (out) (allow-none): the height of the render area inside the window - * - * Return the currently configured render rectangle. See gst_clapper_video_overlay_video_renderer_set_render_rectangle() - * for details. - * - */ -void gst_clapper_video_overlay_video_renderer_get_render_rectangle - (GstClapperVideoOverlayVideoRenderer * self, gint * x, gint * y, - gint * width, gint * height) -{ - g_return_if_fail (GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (self)); - - if (x) - *x = self->x; - if (y) - *y = self->y; - if (width) - *width = self->width; - if (height) - *height = self->height; -} diff --git a/lib/gst/clapper/gstclapper-video-overlay-video-renderer.h b/lib/gst/clapper/gstclapper-video-overlay-video-renderer.h deleted file mode 100644 index 4c250716..00000000 --- a/lib/gst/clapper/gstclapper-video-overlay-video-renderer.h +++ /dev/null @@ -1,71 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_H__ -#define __GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_H__ - -#include -#include - -G_BEGIN_DECLS - -typedef struct _GstClapperVideoOverlayVideoRenderer - GstClapperVideoOverlayVideoRenderer; -typedef struct _GstClapperVideoOverlayVideoRendererClass - GstClapperVideoOverlayVideoRendererClass; - -#define GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER (gst_clapper_video_overlay_video_renderer_get_type ()) -#define GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER)) -#define GST_IS_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER)) -#define GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER, GstClapperVideoOverlayVideoRendererClass)) -#define GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER, GstClapperVideoOverlayVideoRenderer)) -#define GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER, GstClapperVideoOverlayVideoRendererClass)) -#define GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_CAST(obj) ((GstClapperVideoOverlayVideoRenderer*)(obj)) - -GST_CLAPPER_API -GType gst_clapper_video_overlay_video_renderer_get_type (void); - -GST_CLAPPER_API -GstClapperVideoRenderer * - gst_clapper_video_overlay_video_renderer_new (gpointer window_handle); - -GST_CLAPPER_API -GstClapperVideoRenderer * - gst_clapper_video_overlay_video_renderer_new_with_sink (gpointer window_handle, GstElement *video_sink); - -GST_CLAPPER_API -void gst_clapper_video_overlay_video_renderer_set_window_handle (GstClapperVideoOverlayVideoRenderer *self, gpointer window_handle); - -GST_CLAPPER_API -gpointer gst_clapper_video_overlay_video_renderer_get_window_handle (GstClapperVideoOverlayVideoRenderer *self); - -GST_CLAPPER_API -void gst_clapper_video_overlay_video_renderer_expose (GstClapperVideoOverlayVideoRenderer *self); - -GST_CLAPPER_API -void gst_clapper_video_overlay_video_renderer_set_render_rectangle (GstClapperVideoOverlayVideoRenderer *self, gint x, gint y, gint width, gint height); - -GST_CLAPPER_API -void gst_clapper_video_overlay_video_renderer_get_render_rectangle (GstClapperVideoOverlayVideoRenderer *self, gint *x, gint *y, gint *width, gint *height); - -G_END_DECLS - -#endif /* __GST_CLAPPER_VIDEO_OVERLAY_VIDEO_RENDERER_H__ */ diff --git a/lib/gst/clapper/gstclapper-video-renderer-private.h b/lib/gst/clapper/gstclapper-video-renderer-private.h deleted file mode 100644 index 96a07401..00000000 --- a/lib/gst/clapper/gstclapper-video-renderer-private.h +++ /dev/null @@ -1,33 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_VIDEO_RENDERER_PRIVATE_H__ -#define __GST_CLAPPER_VIDEO_RENDERER_PRIVATE_H__ - -#include - -G_BEGIN_DECLS - -G_GNUC_INTERNAL GstElement * gst_clapper_video_renderer_create_video_sink (GstClapperVideoRenderer *self, GstClapper *clapper); - -G_END_DECLS - -#endif /* __GST_CLAPPER_VIDEO_RENDERER_PRIVATE_H__ */ diff --git a/lib/gst/clapper/gstclapper-video-renderer.c b/lib/gst/clapper/gstclapper-video-renderer.c deleted file mode 100644 index ff44be73..00000000 --- a/lib/gst/clapper/gstclapper-video-renderer.c +++ /dev/null @@ -1,50 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-video-renderer.h" -#include "gstclapper-video-renderer-private.h" - -G_DEFINE_INTERFACE (GstClapperVideoRenderer, gst_clapper_video_renderer, - G_TYPE_OBJECT); - -static void -gst_clapper_video_renderer_default_init (G_GNUC_UNUSED - GstClapperVideoRendererInterface * iface) -{ - -} - -GstElement * -gst_clapper_video_renderer_create_video_sink (GstClapperVideoRenderer * self, - GstClapper * clapper) -{ - GstClapperVideoRendererInterface *iface; - - g_return_val_if_fail (GST_IS_CLAPPER_VIDEO_RENDERER (self), NULL); - iface = GST_CLAPPER_VIDEO_RENDERER_GET_INTERFACE (self); - g_return_val_if_fail (iface->create_video_sink != NULL, NULL); - - return iface->create_video_sink (self, clapper); -} diff --git a/lib/gst/clapper/gstclapper-video-renderer.h b/lib/gst/clapper/gstclapper-video-renderer.h deleted file mode 100644 index 9ef43fa6..00000000 --- a/lib/gst/clapper/gstclapper-video-renderer.h +++ /dev/null @@ -1,49 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_VIDEO_RENDERER_H__ -#define __GST_CLAPPER_VIDEO_RENDERER_H__ - -#include -#include - -G_BEGIN_DECLS - -typedef struct _GstClapperVideoRenderer GstClapperVideoRenderer; -typedef struct _GstClapperVideoRendererInterface GstClapperVideoRendererInterface; - -#define GST_TYPE_CLAPPER_VIDEO_RENDERER (gst_clapper_video_renderer_get_type ()) -#define GST_CLAPPER_VIDEO_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_VIDEO_RENDERER, GstClapperVideoRenderer)) -#define GST_IS_CLAPPER_VIDEO_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_VIDEO_RENDERER)) -#define GST_CLAPPER_VIDEO_RENDERER_GET_INTERFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), GST_TYPE_CLAPPER_VIDEO_RENDERER, GstClapperVideoRendererInterface)) - -struct _GstClapperVideoRendererInterface { - GTypeInterface parent_iface; - - GstElement * (*create_video_sink) (GstClapperVideoRenderer * self, GstClapper * clapper); -}; - -GST_CLAPPER_API -GType gst_clapper_video_renderer_get_type (void); - -G_END_DECLS - -#endif /* __GST_CLAPPER_VIDEO_RENDERER_H__ */ diff --git a/lib/gst/clapper/gstclapper-visualization.c b/lib/gst/clapper/gstclapper-visualization.c deleted file mode 100644 index 05e3c3c2..00000000 --- a/lib/gst/clapper/gstclapper-visualization.c +++ /dev/null @@ -1,180 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2015 Brijesh Singh - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapper-visualization - * @title: GstClapperVisualization - * @short_description: Clapper Visualization - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "gstclapper-visualization.h" - -#include - -static GMutex vis_lock; -static GQueue vis_list = G_QUEUE_INIT; -static guint32 vis_cookie; - -G_DEFINE_BOXED_TYPE (GstClapperVisualization, gst_clapper_visualization, - (GBoxedCopyFunc) gst_clapper_visualization_copy, - (GBoxedFreeFunc) gst_clapper_visualization_free); - -/** - * gst_clapper_visualization_free: - * @vis: #GstClapperVisualization instance - * - * Frees a #GstClapperVisualization. - */ -void -gst_clapper_visualization_free (GstClapperVisualization * vis) -{ - g_return_if_fail (vis != NULL); - - g_free (vis->name); - g_free (vis->description); - g_free (vis); -} - -/** - * gst_clapper_visualization_copy: - * @vis: #GstClapperVisualization instance - * - * Makes a copy of the #GstClapperVisualization. The result must be - * freed using gst_clapper_visualization_free(). - * - * Returns: (transfer full): an allocated copy of @vis. - */ -GstClapperVisualization * -gst_clapper_visualization_copy (const GstClapperVisualization * vis) -{ - GstClapperVisualization *ret; - - g_return_val_if_fail (vis != NULL, NULL); - - ret = g_new0 (GstClapperVisualization, 1); - ret->name = vis->name ? g_strdup (vis->name) : NULL; - ret->description = vis->description ? g_strdup (vis->description) : NULL; - - return ret; -} - -/** - * gst_clapper_visualizations_free: - * @viss: a %NULL terminated array of #GstClapperVisualization to free - * - * Frees a %NULL terminated array of #GstClapperVisualization. - */ -void -gst_clapper_visualizations_free (GstClapperVisualization ** viss) -{ - GstClapperVisualization **p; - - g_return_if_fail (viss != NULL); - - p = viss; - while (*p) { - g_free ((*p)->name); - g_free ((*p)->description); - g_free (*p); - p++; - } - g_free (viss); -} - -static void -gst_clapper_update_visualization_list (void) -{ - GList *features; - GList *l; - guint32 cookie; - GstClapperVisualization *vis; - - g_mutex_lock (&vis_lock); - - /* check if we need to update the list */ - cookie = gst_registry_get_feature_list_cookie (gst_registry_get ()); - if (vis_cookie == cookie) { - g_mutex_unlock (&vis_lock); - return; - } - - /* if update is needed then first free the existing list */ - while ((vis = g_queue_pop_head (&vis_list))) - gst_clapper_visualization_free (vis); - - features = gst_registry_get_feature_list (gst_registry_get (), - GST_TYPE_ELEMENT_FACTORY); - - for (l = features; l; l = l->next) { - GstPluginFeature *feature = l->data; - const gchar *klass; - - klass = gst_element_factory_get_metadata (GST_ELEMENT_FACTORY (feature), - GST_ELEMENT_METADATA_KLASS); - - if (strstr (klass, "Visualization")) { - vis = g_new0 (GstClapperVisualization, 1); - - vis->name = g_strdup (gst_plugin_feature_get_name (feature)); - vis->description = - g_strdup (gst_element_factory_get_metadata (GST_ELEMENT_FACTORY - (feature), GST_ELEMENT_METADATA_DESCRIPTION)); - g_queue_push_tail (&vis_list, vis); - } - } - gst_plugin_feature_list_free (features); - - vis_cookie = cookie; - - g_mutex_unlock (&vis_lock); -} - -/** - * gst_clapper_visualizations_get: - * - * Returns: (transfer full) (array zero-terminated=1) (element-type GstClapperVisualization): - * a %NULL terminated array containing all available - * visualizations. Use gst_clapper_visualizations_free() after - * usage. - */ -GstClapperVisualization ** -gst_clapper_visualizations_get (void) -{ - gint i = 0; - GList *l; - GstClapperVisualization **ret; - - gst_clapper_update_visualization_list (); - - g_mutex_lock (&vis_lock); - ret = g_new0 (GstClapperVisualization *, g_queue_get_length (&vis_list) + 1); - for (l = vis_list.head; l; l = l->next) - ret[i++] = gst_clapper_visualization_copy (l->data); - g_mutex_unlock (&vis_lock); - - return ret; -} diff --git a/lib/gst/clapper/gstclapper-visualization.h b/lib/gst/clapper/gstclapper-visualization.h deleted file mode 100644 index f88f8b53..00000000 --- a/lib/gst/clapper/gstclapper-visualization.h +++ /dev/null @@ -1,61 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2015 Brijesh Singh - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_VISUALIZATION_H__ -#define __GST_CLAPPER_VISUALIZATION_H__ - -#include -#include - -G_BEGIN_DECLS - -typedef struct _GstClapperVisualization GstClapperVisualization; -/** - * GstClapperVisualization: - * @name: name of the visualization. - * @description: description of the visualization. - * - * A #GstClapperVisualization descriptor. - */ -struct _GstClapperVisualization { - gchar *name; - gchar *description; -}; - -GST_CLAPPER_API -GType gst_clapper_visualization_get_type (void); - -GST_CLAPPER_API -GstClapperVisualization * gst_clapper_visualization_copy (const GstClapperVisualization *vis); - -GST_CLAPPER_API -void gst_clapper_visualization_free (GstClapperVisualization *vis); - -GST_CLAPPER_API -GstClapperVisualization ** gst_clapper_visualizations_get (void); - -GST_CLAPPER_API -void gst_clapper_visualizations_free (GstClapperVisualization **viss); - -G_END_DECLS - -#endif /* __GST_CLAPPER_VISUALIZATION_H__ */ diff --git a/lib/gst/clapper/gstclapper.c b/lib/gst/clapper/gstclapper.c deleted file mode 100644 index ae7f65ca..00000000 --- a/lib/gst/clapper/gstclapper.c +++ /dev/null @@ -1,5288 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2015 Brijesh Singh - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapper - * @title: GstClapper - * @short_description: Clapper - * @symbols: - * - GstClapper - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include -#include -#include - -#include - -#include "gstclapper.h" -#include "gstclapper-signal-dispatcher-private.h" -#include "gstclapper-video-renderer-private.h" -#include "gstclapper-media-info-private.h" -#include "gstclapper-mpris-private.h" - -GST_DEBUG_CATEGORY_STATIC (gst_clapper_debug); -#define GST_CAT_DEFAULT gst_clapper_debug - -#define DEFAULT_USE_PLAYBIN3 FALSE -#define DEFAULT_USE_PIPEWIRE FALSE -#define DEFAULT_STATE GST_CLAPPER_STATE_STOPPED -#define DEFAULT_URI NULL -#define DEFAULT_POSITION GST_CLOCK_TIME_NONE -#define DEFAULT_DURATION GST_CLOCK_TIME_NONE -#define DEFAULT_VOLUME 1.0 -#define DEFAULT_MUTE FALSE -#define DEFAULT_RATE 1.0 -#define DEFAULT_POSITION_UPDATE_INTERVAL_MS 1000 -#define DEFAULT_AUDIO_VIDEO_OFFSET 0 -#define DEFAULT_SUBTITLE_VIDEO_OFFSET 0 -#define DEFAULT_SEEK_MODE GST_CLAPPER_SEEK_MODE_DEFAULT - -static gboolean gst_clapper_gstreamer_prepared = FALSE; - -/** - * gst_clapper_error_quark: - */ -GQuark -gst_clapper_error_quark (void) -{ - return g_quark_from_static_string ("gst-clapper-error-quark"); -} - -enum -{ - PROP_0, - PROP_VIDEO_RENDERER, - PROP_SIGNAL_DISPATCHER, - PROP_MPRIS, - PROP_USE_PLAYBIN3, - PROP_USE_PIPEWIRE, - PROP_STATE, - PROP_URI, - PROP_SUBURI, - PROP_POSITION, - PROP_DURATION, - PROP_MEDIA_INFO, - PROP_CURRENT_AUDIO_TRACK, - PROP_CURRENT_VIDEO_TRACK, - PROP_CURRENT_SUBTITLE_TRACK, - PROP_VOLUME, - PROP_MUTE, - PROP_RATE, - PROP_PIPELINE, - PROP_VIDEO_MULTIVIEW_MODE, - PROP_VIDEO_MULTIVIEW_FLAGS, - PROP_AUDIO_VIDEO_OFFSET, - PROP_SUBTITLE_VIDEO_OFFSET, - PROP_SEEK_MODE, - PROP_LAST -}; - -enum -{ - SIGNAL_URI_LOADED, - SIGNAL_POSITION_UPDATED, - SIGNAL_DURATION_CHANGED, - SIGNAL_STATE_CHANGED, - SIGNAL_BUFFERING, - SIGNAL_END_OF_STREAM, - SIGNAL_ERROR, - SIGNAL_WARNING, - SIGNAL_VIDEO_DIMENSIONS_CHANGED, - SIGNAL_MEDIA_INFO_UPDATED, - SIGNAL_VIDEO_DECODER_CHANGED, - SIGNAL_AUDIO_DECODER_CHANGED, - SIGNAL_LAST -}; - -enum -{ - GST_PLAY_FLAG_VIDEO = (1 << 0), - GST_PLAY_FLAG_AUDIO = (1 << 1), - GST_PLAY_FLAG_SUBTITLE = (1 << 2), - GST_PLAY_FLAG_VIS = (1 << 3) -}; - -struct _GstClapper -{ - GstObject parent; - - GstClapperVideoRenderer *video_renderer; - GstClapperSignalDispatcher *signal_dispatcher; - GstClapperMpris *mpris; - - gchar *uri; - gchar *redirect_uri; - gchar *suburi; - - GThread *thread; - GMutex lock; - GCond cond; - GMainContext *context; - GMainLoop *loop; - - GstElement *playbin; - GstBus *bus; - GstState target_state, current_state; - gboolean is_live; - GSource *tick_source; - GstClockTime cached_duration; - - gdouble rate; - - /* Prevent unnecessary signals emissions */ - gdouble last_volume; - gboolean last_mute; - - GstClapperState app_state; - gint buffering; - - GstTagList *global_tags; - GstToc *global_toc; - GstClapperMediaInfo *media_info; - - GstElement *current_vis_element; - - GstClapperSeekMode seek_mode; - - /* Protected by lock */ - gboolean seek_pending; /* Only set from main context */ - GstClockTime last_seek_time; /* Only set from main context */ - GSource *seek_source; - GstClockTime seek_position; - /* If TRUE, all signals are inhibited except the - * state-changed:GST_CLAPPER_STATE_STOPPED/PAUSED. This ensures that no signal - * is emitted after gst_clapper_stop/pause() has been called by the user. */ - gboolean inhibit_sigs; - - /* If TRUE, player is in initial ready state after - * new media was loaded and it can be played */ - gboolean can_start; - - /* If should emit media info updated signal */ - gboolean needs_info_update; - - /* Prevent notify with the same decoders */ - gchar *last_vdecoder; - gchar *last_adecoder; - - /* For playbin3 */ - gboolean use_playbin3; - GstStreamCollection *collection; - gchar *video_sid; - gchar *audio_sid; - gchar *subtitle_sid; - gulong stream_notify_id; - - gboolean use_pipewire; -}; - -struct _GstClapperClass -{ - GstObjectClass parent_class; -}; - -#define parent_class gst_clapper_parent_class -G_DEFINE_TYPE (GstClapper, gst_clapper, GST_TYPE_OBJECT); - -static guint signals[SIGNAL_LAST] = { 0, }; -static GParamSpec *param_specs[PROP_LAST] = { NULL, }; - -static void gst_clapper_dispose (GObject * object); -static void gst_clapper_finalize (GObject * object); -static void gst_clapper_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec); -static void gst_clapper_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec); -static void gst_clapper_constructed (GObject * object); - -static gpointer gst_clapper_main (gpointer data); - -static void gst_clapper_seek_internal_locked (GstClapper * self); -static void gst_clapper_stop_internal (GstClapper * self, gboolean transient); -static gboolean gst_clapper_pause_internal (gpointer user_data); -static gboolean gst_clapper_play_internal (gpointer user_data); -static gboolean gst_clapper_seek_internal (gpointer user_data); -static void gst_clapper_set_rate_internal (GstClapper * self); -static void change_state (GstClapper * self, GstClapperState state); - -static GstClapperMediaInfo *gst_clapper_media_info_create (GstClapper * self); - -static void gst_clapper_streams_info_create (GstClapper * self, - GstClapperMediaInfo * media_info, const gchar * prop, GType type); -static void gst_clapper_stream_info_update (GstClapper * self, - GstClapperStreamInfo * s); -static void gst_clapper_stream_info_update_tags_and_caps (GstClapper * self, - GstClapperStreamInfo * s); -static GstClapperStreamInfo *gst_clapper_stream_info_find (GstClapperMediaInfo * - media_info, GType type, gint stream_index); -static GstClapperStreamInfo *gst_clapper_stream_info_get_current (GstClapper * - self, const gchar * prop, GType type); - -static void gst_clapper_video_info_update (GstClapper * self, - GstClapperStreamInfo * stream_info); -static void gst_clapper_audio_info_update (GstClapper * self, - GstClapperStreamInfo * stream_info); -static void gst_clapper_subtitle_info_update (GstClapper * self, - GstClapperStreamInfo * stream_info); - -static gboolean find_active_decoder_with_stream_id (GstClapper * self, - GstElementFactoryListType type, const gchar * stream_id); - -/* For playbin3 */ -static void gst_clapper_streams_info_create_from_collection (GstClapper * self, - GstClapperMediaInfo * media_info, GstStreamCollection * collection); -static void gst_clapper_stream_info_update_from_stream (GstClapper * self, - GstClapperStreamInfo * s, GstStream * stream); -static GstClapperStreamInfo *gst_clapper_stream_info_find_from_stream_id - (GstClapperMediaInfo * media_info, const gchar * stream_id); -static GstClapperStreamInfo *gst_clapper_stream_info_get_current_from_stream_id - (GstClapper * self, const gchar * stream_id, GType type); -static void stream_notify_cb (GstStreamCollection * collection, - GstStream * stream, GParamSpec * pspec, GstClapper * self); - -static void *get_title (GstTagList * tags); -static void *get_container_format (GstTagList * tags); -static void *get_from_tags (GstClapper * self, GstClapperMediaInfo * media_info, - void *(*func) (GstTagList *)); -static void *get_cover_sample (GstTagList * tags); - -static void remove_seek_source (GstClapper * self); - -static void -gst_clapper_init (GstClapper * self) -{ - GST_TRACE_OBJECT (self, "Initializing"); - - self = gst_clapper_get_instance_private (self); - - g_mutex_init (&self->lock); - g_cond_init (&self->cond); - - self->context = g_main_context_new (); - self->loop = g_main_loop_new (self->context, FALSE); - self->seek_pending = FALSE; - self->seek_position = GST_CLOCK_TIME_NONE; - self->last_seek_time = GST_CLOCK_TIME_NONE; - self->inhibit_sigs = FALSE; - self->needs_info_update = FALSE; - self->can_start = FALSE; - self->app_state = GST_CLAPPER_STATE_STOPPED; - - GST_TRACE_OBJECT (self, "Initialized"); -} - -static void -gst_clapper_class_init (GstClapperClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->set_property = gst_clapper_set_property; - gobject_class->get_property = gst_clapper_get_property; - gobject_class->dispose = gst_clapper_dispose; - gobject_class->finalize = gst_clapper_finalize; - gobject_class->constructed = gst_clapper_constructed; - - GST_DEBUG_CATEGORY_INIT (gst_clapper_debug, "Clapper", 0, "GstClapper"); - - param_specs[PROP_VIDEO_RENDERER] = - g_param_spec_object ("video-renderer", - "Video Renderer", "Video renderer to use for rendering videos", - GST_TYPE_CLAPPER_VIDEO_RENDERER, - G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_SIGNAL_DISPATCHER] = - g_param_spec_object ("signal-dispatcher", - "Signal Dispatcher", "Dispatcher for the signals to e.g. event loops", - GST_TYPE_CLAPPER_SIGNAL_DISPATCHER, - G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_MPRIS] = - g_param_spec_object ("mpris", - "MPRIS", "Clapper MPRIS for playback control over DBus", - GST_TYPE_CLAPPER_MPRIS, - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_USE_PLAYBIN3] = - g_param_spec_boolean ("use-playbin3", "Use playbin3", "Use playbin3", - DEFAULT_USE_PLAYBIN3, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_USE_PIPEWIRE] = - g_param_spec_boolean ("use-pipewire", "Use PipeWire", "PipeWire audio output", - DEFAULT_USE_PIPEWIRE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_STATE] = - g_param_spec_enum ("state", "Clapper State", "Current player state", - GST_TYPE_CLAPPER_STATE, DEFAULT_STATE, G_PARAM_READABLE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_URI] = g_param_spec_string ("uri", "URI", "Current URI", - DEFAULT_URI, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_SUBURI] = g_param_spec_string ("suburi", "Subtitle URI", - "Current Subtitle URI", NULL, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_POSITION] = - g_param_spec_uint64 ("position", "Position", "Current Position", - 0, G_MAXUINT64, DEFAULT_POSITION, - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_MEDIA_INFO] = - g_param_spec_object ("media-info", "Media Info", - "Current media information", GST_TYPE_CLAPPER_MEDIA_INFO, - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_CURRENT_AUDIO_TRACK] = - g_param_spec_object ("current-audio-track", "Current Audio Track", - "Current audio track information", GST_TYPE_CLAPPER_AUDIO_INFO, - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_CURRENT_VIDEO_TRACK] = - g_param_spec_object ("current-video-track", "Current Video Track", - "Current video track information", GST_TYPE_CLAPPER_VIDEO_INFO, - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_CURRENT_SUBTITLE_TRACK] = - g_param_spec_object ("current-subtitle-track", "Current Subtitle Track", - "Current audio subtitle information", GST_TYPE_CLAPPER_SUBTITLE_INFO, - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_DURATION] = - g_param_spec_uint64 ("duration", "Duration", "Duration", - 0, G_MAXUINT64, DEFAULT_DURATION, - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_VOLUME] = - g_param_spec_double ("volume", "Volume", "Volume", - 0, 1.5, DEFAULT_VOLUME, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_MUTE] = - g_param_spec_boolean ("mute", "Mute", "Mute", - DEFAULT_MUTE, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_PIPELINE] = - g_param_spec_object ("pipeline", "Pipeline", - "GStreamer pipeline that is used", - GST_TYPE_ELEMENT, G_PARAM_READABLE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_RATE] = - g_param_spec_double ("rate", "rate", "Playback rate", - -64.0, 64.0, DEFAULT_RATE, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_VIDEO_MULTIVIEW_MODE] = - g_param_spec_enum ("video-multiview-mode", - "Multiview Mode Override", - "Re-interpret a video stream as one of several frame-packed stereoscopic modes.", - GST_TYPE_VIDEO_MULTIVIEW_FRAME_PACKING, - GST_VIDEO_MULTIVIEW_FRAME_PACKING_NONE, - G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_VIDEO_MULTIVIEW_FLAGS] = - g_param_spec_flags ("video-multiview-flags", - "Multiview Flags Override", - "Override details of the multiview frame layout", - GST_TYPE_VIDEO_MULTIVIEW_FLAGS, GST_VIDEO_MULTIVIEW_FLAGS_NONE, - G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_AUDIO_VIDEO_OFFSET] = - g_param_spec_int64 ("audio-video-offset", "Audio Video Offset", - "The synchronisation offset between audio and video in nanoseconds", - G_MININT64, G_MAXINT64, 0, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_SUBTITLE_VIDEO_OFFSET] = - g_param_spec_int64 ("subtitle-video-offset", "Text Video Offset", - "The synchronisation offset between text and video in nanoseconds", - G_MININT64, G_MAXINT64, 0, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - param_specs[PROP_SEEK_MODE] = - g_param_spec_enum ("seek-mode", "Clapper Seek Mode", - "Selected seek mode to use when performing seeks", - GST_TYPE_CLAPPER_SEEK_MODE, DEFAULT_SEEK_MODE, G_PARAM_READWRITE | - G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); - - g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); - - signals[SIGNAL_URI_LOADED] = - g_signal_new ("uri-loaded", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); - - signals[SIGNAL_POSITION_UPDATED] = - g_signal_new ("position-updated", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, GST_TYPE_CLOCK_TIME); - - signals[SIGNAL_DURATION_CHANGED] = - g_signal_new ("duration-changed", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, GST_TYPE_CLOCK_TIME); - - signals[SIGNAL_STATE_CHANGED] = - g_signal_new ("state-changed", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, GST_TYPE_CLAPPER_STATE); - - signals[SIGNAL_BUFFERING] = - g_signal_new ("buffering", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, G_TYPE_INT); - - signals[SIGNAL_END_OF_STREAM] = - g_signal_new ("end-of-stream", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 0, G_TYPE_INVALID); - - signals[SIGNAL_ERROR] = - g_signal_new ("error", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, G_TYPE_ERROR); - - signals[SIGNAL_MEDIA_INFO_UPDATED] = - g_signal_new ("media-info-updated", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, GST_TYPE_CLAPPER_MEDIA_INFO); - - signals[SIGNAL_VIDEO_DIMENSIONS_CHANGED] = - g_signal_new ("video-dimensions-changed", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT); - - signals[SIGNAL_WARNING] = - g_signal_new ("warning", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, G_TYPE_ERROR); - - signals[SIGNAL_VIDEO_DECODER_CHANGED] = - g_signal_new ("video-decoder-changed", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); - - signals[SIGNAL_AUDIO_DECODER_CHANGED] = - g_signal_new ("audio-decoder-changed", G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, - NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); -} - -static void -gst_clapper_dispose (GObject * object) -{ - GstClapper *self = GST_CLAPPER (object); - - GST_TRACE_OBJECT (self, "Stopping main thread"); - - if (self->loop) { - g_main_loop_quit (self->loop); - - if (self->thread != g_thread_self ()) - g_thread_join (self->thread); - else - g_thread_unref (self->thread); - self->thread = NULL; - - g_main_loop_unref (self->loop); - self->loop = NULL; - - g_main_context_unref (self->context); - self->context = NULL; - } - - G_OBJECT_CLASS (parent_class)->dispose (object); -} - -static void -gst_clapper_finalize (GObject * object) -{ - GstClapper *self = GST_CLAPPER (object); - - GST_TRACE_OBJECT (self, "Finalize"); - - g_free (self->uri); - g_free (self->redirect_uri); - g_free (self->suburi); - g_free (self->last_vdecoder); - g_free (self->last_adecoder); - g_free (self->video_sid); - g_free (self->audio_sid); - g_free (self->subtitle_sid); - if (self->global_tags) - gst_tag_list_unref (self->global_tags); - if (self->global_toc) - gst_toc_unref (self->global_toc); - if (self->video_renderer) - g_object_unref (self->video_renderer); - if (self->signal_dispatcher) - g_object_unref (self->signal_dispatcher); - if (self->mpris) - g_object_unref (self->mpris); - if (self->current_vis_element) - gst_object_unref (self->current_vis_element); - if (self->collection) - gst_object_unref (self->collection); - g_mutex_clear (&self->lock); - g_cond_clear (&self->cond); - - G_OBJECT_CLASS (parent_class)->finalize (object); -} - -static void -gst_clapper_constructed (GObject * object) -{ - GstClapper *self = GST_CLAPPER (object); - - GST_TRACE_OBJECT (self, "Constructed"); - - g_mutex_lock (&self->lock); - self->thread = g_thread_new ("GstClapper", gst_clapper_main, self); - while (!self->loop || !g_main_loop_is_running (self->loop)) - g_cond_wait (&self->cond, &self->lock); - g_mutex_unlock (&self->lock); - - G_OBJECT_CLASS (parent_class)->constructed (object); -} - -typedef struct -{ - GstClapper *clapper; - gchar *uri; -} UriLoadedSignalData; - -static void -uri_loaded_dispatch (gpointer user_data) -{ - UriLoadedSignalData *data = user_data; - - g_signal_emit (data->clapper, signals[SIGNAL_URI_LOADED], 0, data->uri); -} - -static void -uri_loaded_signal_data_free (UriLoadedSignalData * data) -{ - g_object_unref (data->clapper); - g_free (data->uri); - g_free (data); -} - -static gboolean -gst_clapper_set_uri_internal (gpointer user_data) -{ - GstClapper *self = user_data; - - gst_clapper_stop_internal (self, FALSE); - - g_mutex_lock (&self->lock); - GST_DEBUG_OBJECT (self, "Changing URI to '%s'", GST_STR_NULL (self->uri)); - g_object_set (self->playbin, "uri", self->uri, NULL); - g_object_set (self->playbin, "suburi", NULL, NULL); - self->can_start = TRUE; - - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_URI_LOADED], 0, NULL, NULL, NULL) != 0) { - UriLoadedSignalData *data = g_new (UriLoadedSignalData, 1); - - data->clapper = g_object_ref (self); - data->uri = g_strdup (self->uri); - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - uri_loaded_dispatch, data, - (GDestroyNotify) uri_loaded_signal_data_free); - } - - g_mutex_unlock (&self->lock); - - return G_SOURCE_REMOVE; -} - -static gboolean -gst_clapper_set_suburi_internal (gpointer user_data) -{ - GstClapper *self = user_data; - GstClockTime position; - GstState target_state; - - /* save the state and position */ - target_state = self->target_state; - position = gst_clapper_get_position (self); - - gst_clapper_stop_internal (self, TRUE); - g_mutex_lock (&self->lock); - - GST_DEBUG_OBJECT (self, "Changing SUBURI to '%s'", - GST_STR_NULL (self->suburi)); - - g_object_set (self->playbin, "suburi", self->suburi, NULL); - - g_mutex_unlock (&self->lock); - - /* restore state and position */ - if (position != GST_CLOCK_TIME_NONE) - gst_clapper_seek (self, position); - if (target_state == GST_STATE_PAUSED) - gst_clapper_pause_internal (self); - else if (target_state == GST_STATE_PLAYING) - gst_clapper_play_internal (self); - - return G_SOURCE_REMOVE; -} - -static void -gst_clapper_set_rate_internal (GstClapper * self) -{ - self->seek_position = gst_clapper_get_position (self); - - /* If there is no seek being dispatch to the main context currently do that, - * otherwise we just updated the rate so that it will be taken by - * the seek handler from the main context instead of the old one. - */ - if (!self->seek_source) { - /* If no seek is pending then create new seek source */ - if (!self->seek_pending) { - self->seek_source = g_idle_source_new (); - g_source_set_callback (self->seek_source, - (GSourceFunc) gst_clapper_seek_internal, self, NULL); - g_source_attach (self->seek_source, self->context); - } - } -} - -static void -gst_clapper_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec) -{ - GstClapper *self = GST_CLAPPER (object); - - switch (prop_id) { - case PROP_VIDEO_RENDERER: - self->video_renderer = g_value_dup_object (value); - break; - case PROP_SIGNAL_DISPATCHER: - self->signal_dispatcher = g_value_dup_object (value); - break; - case PROP_MPRIS: - self->mpris = g_value_dup_object (value); - break; - case PROP_USE_PLAYBIN3: - self->use_playbin3 = g_value_get_boolean (value); - break; - case PROP_USE_PIPEWIRE: - self->use_pipewire = g_value_get_boolean (value); - break; - case PROP_URI:{ - g_mutex_lock (&self->lock); - g_free (self->uri); - g_free (self->redirect_uri); - self->redirect_uri = NULL; - - g_free (self->suburi); - self->suburi = NULL; - - self->uri = g_value_dup_string (value); - GST_DEBUG_OBJECT (self, "Set uri=%s", self->uri); - g_mutex_unlock (&self->lock); - - g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT, - gst_clapper_set_uri_internal, self, NULL); - break; - } - case PROP_SUBURI:{ - g_mutex_lock (&self->lock); - g_free (self->suburi); - - self->suburi = g_value_dup_string (value); - GST_DEBUG_OBJECT (self, "Set suburi=%s", self->suburi); - g_mutex_unlock (&self->lock); - - g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT, - gst_clapper_set_suburi_internal, self, NULL); - break; - } - case PROP_VOLUME: { - GValue volume_linear = G_VALUE_INIT; - gdouble volume = g_value_get_double (value); - - GST_DEBUG_OBJECT (self, "Set volume=%lf", volume); - volume = gst_stream_volume_convert_volume ( - GST_STREAM_VOLUME_FORMAT_CUBIC, GST_STREAM_VOLUME_FORMAT_LINEAR, volume); - GST_DEBUG_OBJECT (self, "Converted linear volume=%lf", volume); - - g_value_init (&volume_linear, G_TYPE_DOUBLE); - g_value_set_double (&volume_linear, volume); - g_object_set_property (G_OBJECT (self->playbin), "volume", &volume_linear); - - g_value_unset (&volume_linear); - break; - } - case PROP_RATE: - g_mutex_lock (&self->lock); - self->rate = g_value_get_double (value); - GST_DEBUG_OBJECT (self, "Set rate=%lf", g_value_get_double (value)); - gst_clapper_set_rate_internal (self); - g_mutex_unlock (&self->lock); - break; - case PROP_MUTE: - GST_DEBUG_OBJECT (self, "Set mute=%d", g_value_get_boolean (value)); - g_object_set_property (G_OBJECT (self->playbin), "mute", value); - break; - case PROP_VIDEO_MULTIVIEW_MODE: - GST_DEBUG_OBJECT (self, "Set multiview mode=%u", - g_value_get_enum (value)); - g_object_set_property (G_OBJECT (self->playbin), "video-multiview-mode", - value); - break; - case PROP_VIDEO_MULTIVIEW_FLAGS: - GST_DEBUG_OBJECT (self, "Set multiview flags=%x", - g_value_get_flags (value)); - g_object_set_property (G_OBJECT (self->playbin), "video-multiview-flags", - value); - break; - case PROP_AUDIO_VIDEO_OFFSET: - g_object_set_property (G_OBJECT (self->playbin), "av-offset", value); - break; - case PROP_SUBTITLE_VIDEO_OFFSET: - g_object_set_property (G_OBJECT (self->playbin), "text-offset", value); - break; - case PROP_SEEK_MODE: - g_mutex_lock (&self->lock); - self->seek_mode = g_value_get_enum (value); - g_mutex_unlock (&self->lock); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec) -{ - GstClapper *self = GST_CLAPPER (object); - - switch (prop_id) { - case PROP_MPRIS: - g_mutex_lock (&self->lock); - g_value_set_object (value, self->mpris); - g_mutex_unlock (&self->lock); - break; - case PROP_STATE: - g_mutex_lock (&self->lock); - g_value_set_enum (value, self->app_state); - g_mutex_unlock (&self->lock); - break; - case PROP_URI: - g_mutex_lock (&self->lock); - g_value_set_string (value, self->uri); - g_mutex_unlock (&self->lock); - break; - case PROP_SUBURI: - g_mutex_lock (&self->lock); - g_value_set_string (value, self->suburi); - g_mutex_unlock (&self->lock); - GST_DEBUG_OBJECT (self, "Returning suburi=%s", - g_value_get_string (value)); - break; - case PROP_POSITION:{ - gint64 position = GST_CLOCK_TIME_NONE; - - gst_element_query_position (self->playbin, GST_FORMAT_TIME, &position); - g_value_set_uint64 (value, position); - GST_TRACE_OBJECT (self, "Returning position=%" GST_TIME_FORMAT, - GST_TIME_ARGS (g_value_get_uint64 (value))); - break; - } - case PROP_DURATION:{ - g_value_set_uint64 (value, self->cached_duration); - GST_TRACE_OBJECT (self, "Returning duration=%" GST_TIME_FORMAT, - GST_TIME_ARGS (g_value_get_uint64 (value))); - break; - } - case PROP_MEDIA_INFO:{ - GstClapperMediaInfo *media_info = gst_clapper_get_media_info (self); - g_value_take_object (value, media_info); - break; - } - case PROP_CURRENT_AUDIO_TRACK:{ - GstClapperAudioInfo *audio_info = - gst_clapper_get_current_audio_track (self); - g_value_take_object (value, audio_info); - break; - } - case PROP_CURRENT_VIDEO_TRACK:{ - GstClapperVideoInfo *video_info = - gst_clapper_get_current_video_track (self); - g_value_take_object (value, video_info); - break; - } - case PROP_CURRENT_SUBTITLE_TRACK:{ - GstClapperSubtitleInfo *subtitle_info = - gst_clapper_get_current_subtitle_track (self); - g_value_take_object (value, subtitle_info); - break; - } - case PROP_VOLUME: { - gdouble volume; - - g_object_get_property (G_OBJECT (self->playbin), "volume", value); - volume = g_value_get_double (value); - volume = gst_stream_volume_convert_volume ( - GST_STREAM_VOLUME_FORMAT_LINEAR, GST_STREAM_VOLUME_FORMAT_CUBIC, volume); - g_value_set_double (value, volume); - GST_TRACE_OBJECT (self, "Returning volume=%lf", volume); - break; - } - case PROP_RATE: - g_mutex_lock (&self->lock); - g_value_set_double (value, self->rate); - g_mutex_unlock (&self->lock); - break; - case PROP_MUTE: - g_object_get_property (G_OBJECT (self->playbin), "mute", value); - GST_TRACE_OBJECT (self, "Returning mute=%d", g_value_get_boolean (value)); - break; - case PROP_PIPELINE: - g_value_set_object (value, self->playbin); - break; - case PROP_VIDEO_MULTIVIEW_MODE:{ - g_object_get_property (G_OBJECT (self->playbin), "video-multiview-mode", - value); - GST_TRACE_OBJECT (self, "Return multiview mode=%d", - g_value_get_enum (value)); - break; - } - case PROP_VIDEO_MULTIVIEW_FLAGS:{ - g_object_get_property (G_OBJECT (self->playbin), "video-multiview-flags", - value); - GST_TRACE_OBJECT (self, "Return multiview flags=%x", - g_value_get_flags (value)); - break; - } - case PROP_AUDIO_VIDEO_OFFSET: - g_object_get_property (G_OBJECT (self->playbin), "av-offset", value); - break; - case PROP_SUBTITLE_VIDEO_OFFSET: - g_object_get_property (G_OBJECT (self->playbin), "text-offset", value); - break; - case PROP_SEEK_MODE: - g_mutex_lock (&self->lock); - g_value_set_enum (value, self->seek_mode); - g_mutex_unlock (&self->lock); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static gboolean -main_loop_running_cb (gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - GST_TRACE_OBJECT (self, "Main loop running now"); - - g_mutex_lock (&self->lock); - g_cond_signal (&self->cond); - g_mutex_unlock (&self->lock); - - return G_SOURCE_REMOVE; -} - -typedef struct -{ - GstClapper *clapper; - GstClapperMediaInfo *info; -} MediaInfoUpdatedSignalData; - -static void -media_info_updated_dispatch (gpointer user_data) -{ - MediaInfoUpdatedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - if (data->clapper->target_state >= GST_STATE_PAUSED) { - g_signal_emit (data->clapper, signals[SIGNAL_MEDIA_INFO_UPDATED], 0, - data->info); - } -} - -static void -free_media_info_updated_signal_data (MediaInfoUpdatedSignalData * data) -{ - g_object_unref (data->clapper); - g_object_unref (data->info); - g_free (data); -} - -static void -emit_media_info_updated (GstClapper * self) -{ - MediaInfoUpdatedSignalData *data = g_new (MediaInfoUpdatedSignalData, 1); - self->needs_info_update = FALSE; - data->clapper = g_object_ref (self); - g_mutex_lock (&self->lock); - data->info = gst_clapper_media_info_copy (self->media_info); - g_mutex_unlock (&self->lock); - - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - media_info_updated_dispatch, data, - (GDestroyNotify) free_media_info_updated_signal_data); -} - -typedef struct -{ - GstClapper *clapper; - GstClapperState state; -} StateChangedSignalData; - -static void -state_changed_dispatch (gpointer user_data) -{ - StateChangedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs && data->state != GST_CLAPPER_STATE_STOPPED - && data->state != GST_CLAPPER_STATE_PAUSED) - return; - - g_signal_emit (data->clapper, signals[SIGNAL_STATE_CHANGED], 0, data->state); -} - -static void -state_changed_signal_data_free (StateChangedSignalData * data) -{ - g_object_unref (data->clapper); - g_free (data); -} - -static void -rate_notify_dispatch (gpointer user_data) -{ - GstClapper *clapper = user_data; - - if (clapper->inhibit_sigs) - return; - - g_object_notify_by_pspec (G_OBJECT (clapper), param_specs[PROP_RATE]); -} - -static void -emit_rate_notify (GstClapper * self) -{ - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - rate_notify_dispatch, g_object_ref (self), - (GDestroyNotify) g_object_unref); -} - -static void -change_state (GstClapper * self, GstClapperState state) -{ - if (state == self->app_state) - return; - - GST_DEBUG_OBJECT (self, "Changing app state from %s to %s", - gst_clapper_state_get_name (self->app_state), - gst_clapper_state_get_name (state)); - self->app_state = state; - - if (state == GST_CLAPPER_STATE_STOPPED) { - self->needs_info_update = FALSE; - if (self->rate != 1.0) { - self->rate = 1.0; - emit_rate_notify (self); - } - } - - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_STATE_CHANGED], 0, NULL, NULL, NULL) != 0) { - StateChangedSignalData *data = g_new (StateChangedSignalData, 1); - - data->clapper = g_object_ref (self); - data->state = state; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - state_changed_dispatch, data, - (GDestroyNotify) state_changed_signal_data_free); - } - - if (!self->mpris) - return; - - switch (state) { - case GST_CLAPPER_STATE_STOPPED: - gst_clapper_mpris_set_playback_status (self->mpris, "Stopped"); - break; - case GST_CLAPPER_STATE_PAUSED: - gst_clapper_mpris_set_playback_status (self->mpris, "Paused"); - break; - case GST_CLAPPER_STATE_PLAYING: - gst_clapper_mpris_set_playback_status (self->mpris, "Playing"); - break; - default: - break; - } -} - -typedef struct -{ - GstClapper *clapper; - GstClockTime position; -} PositionUpdatedSignalData; - -static void -position_updated_dispatch (gpointer user_data) -{ - PositionUpdatedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - if (data->clapper->target_state >= GST_STATE_PAUSED) { - g_signal_emit (data->clapper, signals[SIGNAL_POSITION_UPDATED], 0, - data->position); - } -} - -static void -position_updated_signal_data_free (PositionUpdatedSignalData * data) -{ - g_object_unref (data->clapper); - g_free (data); -} - -static gboolean -tick_cb (gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - gint64 position; - - if (self->target_state >= GST_STATE_PAUSED - && gst_element_query_position (self->playbin, GST_FORMAT_TIME, - &position)) { - GST_LOG_OBJECT (self, "Position %" GST_TIME_FORMAT, - GST_TIME_ARGS (position)); - - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_POSITION_UPDATED], 0, NULL, NULL, NULL) != 0) { - PositionUpdatedSignalData *data = g_new (PositionUpdatedSignalData, 1); - - data->clapper = g_object_ref (self); - data->position = position; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - position_updated_dispatch, data, - (GDestroyNotify) position_updated_signal_data_free); - } - if (self->mpris) - gst_clapper_mpris_set_position (self->mpris, position); - } - - return G_SOURCE_CONTINUE; -} - -static void -add_tick_source (GstClapper * self) -{ - if (self->tick_source) - return; - - self->tick_source = g_timeout_source_new (DEFAULT_POSITION_UPDATE_INTERVAL_MS); - g_source_set_callback (self->tick_source, (GSourceFunc) tick_cb, self, NULL); - g_source_attach (self->tick_source, self->context); -} - -static void -remove_tick_source (GstClapper * self) -{ - if (!self->tick_source) - return; - - g_source_destroy (self->tick_source); - g_source_unref (self->tick_source); - self->tick_source = NULL; -} - -typedef struct -{ - GstClapper *clapper; - GError *err; -} ErrorSignalData; - -static void -error_dispatch (gpointer user_data) -{ - ErrorSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - g_signal_emit (data->clapper, signals[SIGNAL_ERROR], 0, data->err); -} - -static void -free_error_signal_data (ErrorSignalData * data) -{ - g_object_unref (data->clapper); - g_clear_error (&data->err); - g_free (data); -} - -static void -emit_error (GstClapper * self, GError * err) -{ - GST_ERROR_OBJECT (self, "Error: %s (%s, %d)", err->message, - g_quark_to_string (err->domain), err->code); - - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_ERROR], 0, NULL, NULL, NULL) != 0) { - ErrorSignalData *data = g_new (ErrorSignalData, 1); - - data->clapper = g_object_ref (self); - data->err = g_error_copy (err); - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - error_dispatch, data, (GDestroyNotify) free_error_signal_data); - } - - g_error_free (err); - - remove_tick_source (self); - - self->target_state = GST_STATE_NULL; - self->current_state = GST_STATE_NULL; - self->is_live = FALSE; - gst_element_set_state (self->playbin, GST_STATE_NULL); - change_state (self, GST_CLAPPER_STATE_STOPPED); - self->buffering = 100; - - g_mutex_lock (&self->lock); - if (self->media_info) { - g_object_unref (self->media_info); - self->media_info = NULL; - } - if (self->global_tags) { - gst_tag_list_unref (self->global_tags); - self->global_tags = NULL; - } - if (self->global_toc) { - gst_toc_unref (self->global_toc); - self->global_toc = NULL; - } - self->seek_pending = FALSE; - remove_seek_source (self); - self->seek_position = GST_CLOCK_TIME_NONE; - self->last_seek_time = GST_CLOCK_TIME_NONE; - g_mutex_unlock (&self->lock); -} - -static void -dump_dot_file (GstClapper * self, const gchar * name) -{ - gchar *full_name; - - full_name = g_strdup_printf ("gst-clapper.%p.%s", self, name); - - GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (self->playbin), - GST_DEBUG_GRAPH_SHOW_ALL, full_name); - - g_free (full_name); -} - -typedef struct -{ - GstClapper *clapper; - GError *err; -} WarningSignalData; - -static void -warning_dispatch (gpointer user_data) -{ - WarningSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - g_signal_emit (data->clapper, signals[SIGNAL_WARNING], 0, data->err); -} - -static void -free_warning_signal_data (WarningSignalData * data) -{ - g_object_unref (data->clapper); - g_clear_error (&data->err); - g_free (data); -} - -static void -emit_warning (GstClapper * self, GError * err) -{ - GST_ERROR_OBJECT (self, "Warning: %s (%s, %d)", err->message, - g_quark_to_string (err->domain), err->code); - - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_WARNING], 0, NULL, NULL, NULL) != 0) { - WarningSignalData *data = g_new (WarningSignalData, 1); - - data->clapper = g_object_ref (self); - data->err = g_error_copy (err); - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - warning_dispatch, data, (GDestroyNotify) free_warning_signal_data); - } - - g_error_free (err); -} - -static void -error_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GError *err, *clapper_err; - gchar *name, *debug, *message, *full_message; - - dump_dot_file (self, "error"); - - gst_message_parse_error (msg, &err, &debug); - - name = gst_object_get_path_string (msg->src); - message = gst_error_get_message (err->domain, err->code); - - if (debug) - full_message = - g_strdup_printf ("Error from element %s: %s\n%s\n%s", name, message, - err->message, debug); - else - full_message = - g_strdup_printf ("Error from element %s: %s\n%s", name, message, - err->message); - - GST_ERROR_OBJECT (self, "ERROR: from element %s: %s", name, err->message); - if (debug != NULL) - GST_ERROR_OBJECT (self, "Additional debug info: %s", debug); - - clapper_err = - g_error_new_literal (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - full_message); - emit_error (self, clapper_err); - - g_clear_error (&err); - g_free (debug); - g_free (name); - g_free (full_message); - g_free (message); -} - -static void -warning_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GError *err, *clapper_err; - gchar *name, *debug, *message, *full_message; - - dump_dot_file (self, "warning"); - - gst_message_parse_warning (msg, &err, &debug); - - name = gst_object_get_path_string (msg->src); - message = gst_error_get_message (err->domain, err->code); - - if (debug) - full_message = - g_strdup_printf ("Warning from element %s: %s\n%s\n%s", name, message, - err->message, debug); - else - full_message = - g_strdup_printf ("Warning from element %s: %s\n%s", name, message, - err->message); - - GST_WARNING_OBJECT (self, "WARNING: from element %s: %s", name, err->message); - if (debug != NULL) - GST_WARNING_OBJECT (self, "Additional debug info: %s", debug); - - clapper_err = - g_error_new_literal (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - full_message); - emit_warning (self, clapper_err); - - g_clear_error (&err); - g_free (debug); - g_free (name); - g_free (full_message); - g_free (message); -} - -static void -eos_dispatch (gpointer user_data) -{ - GstClapper *clapper = user_data; - - if (clapper->inhibit_sigs) - return; - - g_signal_emit (clapper, signals[SIGNAL_END_OF_STREAM], 0); -} - -static void -eos_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - GST_DEBUG_OBJECT (self, "End of stream"); - - tick_cb (self); - remove_tick_source (self); - - /* When connected client should handle what to do (stop/repeat) */ - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_END_OF_STREAM], 0, NULL, NULL, NULL) != 0) { - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - eos_dispatch, g_object_ref (self), (GDestroyNotify) g_object_unref); - } else - gst_clapper_stop_internal (self, FALSE); -} - -typedef struct -{ - GstClapper *clapper; - gint percent; -} BufferingSignalData; - -static void -buffering_dispatch (gpointer user_data) -{ - BufferingSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - if (data->clapper->target_state >= GST_STATE_PAUSED) { - g_signal_emit (data->clapper, signals[SIGNAL_BUFFERING], 0, data->percent); - } -} - -static void -buffering_signal_data_free (BufferingSignalData * data) -{ - g_object_unref (data->clapper); - g_free (data); -} - -static void -buffering_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - gint percent; - - if (self->target_state < GST_STATE_PAUSED) - return; - if (self->is_live) - return; - - gst_message_parse_buffering (msg, &percent); - GST_LOG_OBJECT (self, "Buffering %d%%", percent); - - if (percent < 100 && self->target_state >= GST_STATE_PAUSED) { - GstStateChangeReturn state_ret; - - GST_DEBUG_OBJECT (self, "Waiting for buffering to finish"); - state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED); - - if (state_ret == GST_STATE_CHANGE_FAILURE) { - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to handle buffering")); - return; - } - - change_state (self, GST_CLAPPER_STATE_BUFFERING); - } - - if (self->buffering != percent) { - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_BUFFERING], 0, NULL, NULL, NULL) != 0) { - BufferingSignalData *data = g_new (BufferingSignalData, 1); - - data->clapper = g_object_ref (self); - data->percent = percent; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - buffering_dispatch, data, - (GDestroyNotify) buffering_signal_data_free); - } - - self->buffering = percent; - } - - - g_mutex_lock (&self->lock); - if (percent == 100 && (self->seek_position != GST_CLOCK_TIME_NONE || - self->seek_pending)) { - g_mutex_unlock (&self->lock); - - GST_DEBUG_OBJECT (self, "Buffering finished - seek pending"); - } else if (percent == 100 && self->target_state >= GST_STATE_PLAYING - && self->current_state >= GST_STATE_PAUSED) { - GstStateChangeReturn state_ret; - - g_mutex_unlock (&self->lock); - - GST_DEBUG_OBJECT (self, "Buffering finished - going to PLAYING"); - state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING); - /* Application state change is happening when the state change happened */ - if (state_ret == GST_STATE_CHANGE_FAILURE) - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to handle buffering")); - } else if (percent == 100 && self->target_state >= GST_STATE_PAUSED) { - g_mutex_unlock (&self->lock); - - GST_DEBUG_OBJECT (self, "Buffering finished - staying PAUSED"); - change_state (self, GST_CLAPPER_STATE_PAUSED); - } else { - g_mutex_unlock (&self->lock); - } -} - -static void -clock_lost_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstStateChangeReturn state_ret; - - GST_DEBUG_OBJECT (self, "Clock lost"); - if (self->target_state >= GST_STATE_PLAYING) { - state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED); - if (state_ret != GST_STATE_CHANGE_FAILURE) - state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING); - - if (state_ret == GST_STATE_CHANGE_FAILURE) - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to handle clock loss")); - } -} - -typedef struct -{ - GstClapper *clapper; - gint width, height; -} VideoDimensionsChangedSignalData; - -static void -video_dimensions_changed_dispatch (gpointer user_data) -{ - VideoDimensionsChangedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - if (data->clapper->target_state >= GST_STATE_PAUSED) { - g_signal_emit (data->clapper, signals[SIGNAL_VIDEO_DIMENSIONS_CHANGED], 0, - data->width, data->height); - } -} - -static void -video_dimensions_changed_signal_data_free (VideoDimensionsChangedSignalData * - data) -{ - g_object_unref (data->clapper); - g_free (data); -} - -static void -check_video_dimensions_changed (GstClapper * self) -{ - GstElement *video_sink; - GstPad *video_sink_pad; - GstCaps *caps; - GstVideoInfo info; - gint width = 0, height = 0; - - g_object_get (self->playbin, "video-sink", &video_sink, NULL); - if (!video_sink) - goto out; - - video_sink_pad = gst_element_get_static_pad (video_sink, "sink"); - if (!video_sink_pad) { - gst_object_unref (video_sink); - goto out; - } - - caps = gst_pad_get_current_caps (video_sink_pad); - - if (caps) { - if (gst_video_info_from_caps (&info, caps)) { - info.width = info.width * info.par_n / info.par_d; - - GST_DEBUG_OBJECT (self, "Video dimensions changed: %dx%d", info.width, - info.height); - width = info.width; - height = info.height; - } - - gst_caps_unref (caps); - } - gst_object_unref (video_sink_pad); - gst_object_unref (video_sink); - -out: - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_VIDEO_DIMENSIONS_CHANGED], 0, NULL, NULL, NULL) != 0) { - VideoDimensionsChangedSignalData *data = - g_new (VideoDimensionsChangedSignalData, 1); - - data->clapper = g_object_ref (self); - data->width = width; - data->height = height; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - video_dimensions_changed_dispatch, data, - (GDestroyNotify) video_dimensions_changed_signal_data_free); - } -} - -static void -notify_caps_cb (G_GNUC_UNUSED GObject * object, - G_GNUC_UNUSED GParamSpec * pspec, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - if (self->target_state >= GST_STATE_PAUSED) { - check_video_dimensions_changed (self); - - g_mutex_lock (&self->lock); - if (self->media_info != NULL) - self->needs_info_update = TRUE; - g_mutex_unlock (&self->lock); - } -} - -typedef struct -{ - GstClapper *clapper; - GstClockTime duration; -} DurationChangedSignalData; - -static void -duration_changed_dispatch (gpointer user_data) -{ - DurationChangedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - if (data->clapper->target_state >= GST_STATE_PAUSED) { - g_signal_emit (data->clapper, signals[SIGNAL_DURATION_CHANGED], 0, - data->duration); - } -} - -static void -duration_changed_signal_data_free (DurationChangedSignalData * data) -{ - g_object_unref (data->clapper); - g_free (data); -} - -static void -emit_duration_changed (GstClapper * self, GstClockTime duration) -{ - if (self->cached_duration == duration - || self->cached_duration / (250 * GST_MSECOND) == duration / (250 * GST_MSECOND)) - return; - - GST_DEBUG_OBJECT (self, "Duration changed %" GST_TIME_FORMAT, - GST_TIME_ARGS (duration)); - - self->cached_duration = duration; - g_mutex_lock (&self->lock); - if (self->media_info) - self->media_info->duration = duration; - g_mutex_unlock (&self->lock); - - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_DURATION_CHANGED], 0, NULL, NULL, NULL) != 0) { - DurationChangedSignalData *data = g_new (DurationChangedSignalData, 1); - - data->clapper = g_object_ref (self); - data->duration = duration; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - duration_changed_dispatch, data, - (GDestroyNotify) duration_changed_signal_data_free); - } -} - -static void -state_changed_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstState old_state, new_state, pending_state; - - gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state); - - if (GST_MESSAGE_SRC (msg) == GST_OBJECT (self->playbin)) { - gchar *transition_name; - - GST_DEBUG_OBJECT (self, "Changed state old: %s new: %s pending: %s", - gst_element_state_get_name (old_state), - gst_element_state_get_name (new_state), - gst_element_state_get_name (pending_state)); - - transition_name = g_strdup_printf ("%s_%s", - gst_element_state_get_name (old_state), - gst_element_state_get_name (new_state)); - dump_dot_file (self, transition_name); - g_free (transition_name); - - self->current_state = new_state; - - if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED - && pending_state == GST_STATE_VOID_PENDING) { - gint64 duration = -1; - - GST_DEBUG_OBJECT (self, "Initial PAUSED - pre-rolled"); - - g_mutex_lock (&self->lock); - if (self->media_info) - g_object_unref (self->media_info); - self->media_info = gst_clapper_media_info_create (self); - g_mutex_unlock (&self->lock); - check_video_dimensions_changed (self); - if (gst_element_query_duration (self->playbin, GST_FORMAT_TIME, - &duration)) { - emit_duration_changed (self, duration); - } else { - self->cached_duration = GST_CLOCK_TIME_NONE; - } - emit_media_info_updated (self); - if (self->mpris) { - GstClapperMediaInfo *info; - - g_mutex_lock (&self->lock); - info = gst_clapper_media_info_copy (self->media_info); - g_mutex_unlock (&self->lock); - - gst_clapper_mpris_set_media_info (self->mpris, info); - } - } - - if (new_state == GST_STATE_PAUSED - && pending_state == GST_STATE_VOID_PENDING) { - remove_tick_source (self); - - g_mutex_lock (&self->lock); - if (self->seek_pending) { - self->seek_pending = FALSE; - - if (!self->media_info->seekable) { - GST_DEBUG_OBJECT (self, "Media is not seekable"); - remove_seek_source (self); - self->seek_position = GST_CLOCK_TIME_NONE; - self->last_seek_time = GST_CLOCK_TIME_NONE; - } else if (self->seek_source) { - GST_DEBUG_OBJECT (self, "Seek finished but new seek is pending"); - gst_clapper_seek_internal_locked (self); - } else { - GST_DEBUG_OBJECT (self, "Seek finished"); - } - } - - if (self->seek_position != GST_CLOCK_TIME_NONE) { - GST_DEBUG_OBJECT (self, "Seeking now that we reached PAUSED state"); - gst_clapper_seek_internal_locked (self); - g_mutex_unlock (&self->lock); - } else if (!self->seek_pending) { - g_mutex_unlock (&self->lock); - - tick_cb (self); - - if (self->target_state >= GST_STATE_PLAYING && self->buffering == 100) { - GstStateChangeReturn state_ret; - - state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING); - if (state_ret == GST_STATE_CHANGE_FAILURE) - emit_error (self, g_error_new (GST_CLAPPER_ERROR, - GST_CLAPPER_ERROR_FAILED, "Failed to play")); - } else if (self->buffering == 100) { - change_state (self, GST_CLAPPER_STATE_PAUSED); - } - } else { - g_mutex_unlock (&self->lock); - } - } else if (new_state == GST_STATE_PLAYING - && pending_state == GST_STATE_VOID_PENDING) { - - /* If no seek is currently pending, add the tick source. This can happen - * if we seeked already but the state-change message was still queued up */ - if (!self->seek_pending) { - add_tick_source (self); - change_state (self, GST_CLAPPER_STATE_PLAYING); - } - } else if (new_state == GST_STATE_READY && old_state > GST_STATE_READY) { - change_state (self, GST_CLAPPER_STATE_STOPPED); - } else { - /* Otherwise we neither reached PLAYING nor PAUSED, so must - * wait for something to happen... i.e. are BUFFERING now */ - change_state (self, GST_CLAPPER_STATE_BUFFERING); - } - } -} - -static void -duration_changed_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - gint64 duration = GST_CLOCK_TIME_NONE; - - if (gst_element_query_duration (self->playbin, GST_FORMAT_TIME, &duration)) { - emit_duration_changed (self, duration); - } -} - -static void -latency_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - GST_DEBUG_OBJECT (self, "Latency changed"); - - gst_bin_recalculate_latency (GST_BIN (self->playbin)); -} - -static void -request_state_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstState state; - GstStateChangeReturn state_ret; - - gst_message_parse_request_state (msg, &state); - - GST_DEBUG_OBJECT (self, "State %s requested", - gst_element_state_get_name (state)); - - self->target_state = state; - state_ret = gst_element_set_state (self->playbin, state); - if (state_ret == GST_STATE_CHANGE_FAILURE) - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to change to requested state %s", - gst_element_state_get_name (state))); -} - -static void -media_info_update (GstClapper * self, GstClapperMediaInfo * info) -{ - /* Update title from new tags or leave the title from URI */ - gchar *tags_title = get_from_tags (self, info, get_title); - if (tags_title) { - g_free (info->title); - info->title = tags_title; - } - - g_free (info->container); - info->container = get_from_tags (self, info, get_container_format); - - if (info->image_sample) - gst_sample_unref (info->image_sample); - info->image_sample = get_from_tags (self, info, get_cover_sample); - - GST_DEBUG_OBJECT (self, "title: %s, container: %s " - "image_sample: %p", info->title, info->container, info->image_sample); -} - -static void -merge_tags (GstTagList **my_tags, GstTagList *tags) -{ - if (*my_tags) { - *my_tags = gst_tag_list_make_writable (*my_tags); - gst_tag_list_insert (*my_tags, tags, GST_TAG_MERGE_REPLACE); - } else { - *my_tags = gst_tag_list_ref (tags); - } -} - -static void -tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstTagList *tags = NULL; - - gst_message_parse_tag (msg, &tags); - - GST_DEBUG_OBJECT (self, "received %s tags", - gst_tag_list_get_scope (tags) == - GST_TAG_SCOPE_GLOBAL ? "global" : "stream"); - - if (gst_tag_list_get_scope (tags) == GST_TAG_SCOPE_GLOBAL) { - g_mutex_lock (&self->lock); - if (self->media_info) { - merge_tags (&self->media_info->tags, tags); - media_info_update (self, self->media_info); - } else { - merge_tags (&self->global_tags, tags); - } - g_mutex_unlock (&self->lock); - } - - gst_tag_list_unref (tags); -} - -static void -toc_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstToc *toc = NULL; - - gst_message_parse_toc (msg, &toc, NULL); - - GST_DEBUG_OBJECT (self, "received %s toc", - gst_toc_get_scope (toc) == GST_TOC_SCOPE_GLOBAL ? "global" : "stream"); - - if (gst_toc_get_scope (toc) == GST_TOC_SCOPE_GLOBAL) { - g_mutex_lock (&self->lock); - if (self->media_info) { - if (self->media_info->toc) - gst_toc_unref (self->media_info->toc); - self->media_info->toc = gst_toc_ref (toc); - media_info_update (self, self->media_info); - g_mutex_unlock (&self->lock); - } else { - if (self->global_toc) - gst_toc_unref (self->global_toc); - self->global_toc = gst_toc_ref (toc); - g_mutex_unlock (&self->lock); - } - } - - gst_toc_unref (toc); -} - -static void -element_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - const GstStructure *s; - - s = gst_message_get_structure (msg); - if (gst_structure_has_name (s, "redirect")) { - const gchar *new_location; - - new_location = gst_structure_get_string (s, "new-location"); - if (!new_location) { - const GValue *locations_list, *location_val; - guint i, size; - - locations_list = gst_structure_get_value (s, "locations"); - size = gst_value_list_get_size (locations_list); - for (i = 0; i < size; ++i) { - const GstStructure *location_s; - - location_val = gst_value_list_get_value (locations_list, i); - if (!GST_VALUE_HOLDS_STRUCTURE (location_val)) - continue; - - location_s = (const GstStructure *) g_value_get_boxed (location_val); - if (!gst_structure_has_name (location_s, "redirect")) - continue; - - new_location = gst_structure_get_string (location_s, "new-location"); - if (new_location) - break; - } - } - - if (new_location) { - GstState target_state; - - GST_DEBUG_OBJECT (self, "Redirect to '%s'", new_location); - - /* Remember target state and restore after setting the URI */ - target_state = self->target_state; - - gst_clapper_stop_internal (self, TRUE); - - g_mutex_lock (&self->lock); - g_free (self->redirect_uri); - self->redirect_uri = g_strdup (new_location); - g_object_set (self->playbin, "uri", self->redirect_uri, NULL); - g_mutex_unlock (&self->lock); - - if (target_state == GST_STATE_PAUSED) - gst_clapper_pause_internal (self); - else if (target_state == GST_STATE_PLAYING) - gst_clapper_play_internal (self); - } - } -} - -static void -qos_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - gboolean live; - guint64 running_time, stream_time, timestamp, duration; - - gst_message_parse_qos (msg, &live, &running_time, &stream_time, - ×tamp, &duration); - - GST_DEBUG_OBJECT (self, "QOS dropped buffer" - ", element live: %s" - ", running time: %" GST_TIME_FORMAT - ", stream time: %" GST_TIME_FORMAT - ", timestamp: %" GST_TIME_FORMAT - ", duration: %" GST_TIME_FORMAT, - live ? "yes" : "no", GST_TIME_ARGS (running_time), - GST_TIME_ARGS (stream_time), GST_TIME_ARGS (timestamp), - GST_TIME_ARGS (duration)); -} - -/* Must be called with lock */ -static gboolean -update_stream_collection (GstClapper * self, GstStreamCollection * collection) -{ - if (self->collection && self->collection == collection) - return FALSE; - - if (self->collection && self->stream_notify_id) - g_signal_handler_disconnect (self->collection, self->stream_notify_id); - - gst_object_replace ((GstObject **) & self->collection, - (GstObject *) collection); - if (self->media_info) { - gst_object_unref (self->media_info); - self->media_info = gst_clapper_media_info_create (self); - } - - self->stream_notify_id = - g_signal_connect (self->collection, "stream-notify", - G_CALLBACK (stream_notify_cb), self); - - return TRUE; -} - -static void -stream_collection_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstStreamCollection *collection = NULL; - - gst_message_parse_stream_collection (msg, &collection); - - if (!collection) - return; - - g_mutex_lock (&self->lock); - update_stream_collection (self, collection); - gst_object_unref (collection); - g_mutex_unlock (&self->lock); -} - -static void -streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstStreamCollection *collection = NULL; - gchar *video_sid, *audio_sid; - guint i, len; - - gst_message_parse_streams_selected (msg, &collection); - - if (!collection) - return; - - g_mutex_lock (&self->lock); - update_stream_collection (self, collection); - gst_object_unref (collection); - - g_free (self->video_sid); - g_free (self->audio_sid); - g_free (self->subtitle_sid); - self->video_sid = NULL; - self->audio_sid = NULL; - self->subtitle_sid = NULL; - - len = gst_message_streams_selected_get_size (msg); - for (i = 0; i < len; i++) { - GstStream *stream; - GstStreamType stream_type; - const gchar *stream_id; - gchar **current_sid; - stream = gst_message_streams_selected_get_stream (msg, i); - stream_type = gst_stream_get_stream_type (stream); - stream_id = gst_stream_get_stream_id (stream); - if (stream_type & GST_STREAM_TYPE_AUDIO) - current_sid = &self->audio_sid; - else if (stream_type & GST_STREAM_TYPE_VIDEO) - current_sid = &self->video_sid; - else if (stream_type & GST_STREAM_TYPE_TEXT) - current_sid = &self->subtitle_sid; - else { - GST_WARNING_OBJECT (self, - "Unknown stream-id %s with type 0x%x", stream_id, stream_type); - continue; - } - - if (G_UNLIKELY (*current_sid)) { - GST_FIXME_OBJECT (self, - "Multiple streams are selected for type %s, choose the first one", - gst_stream_type_get_name (stream_type)); - continue; - } - - *current_sid = g_strdup (stream_id); - } - - video_sid = g_strdup (self->video_sid); - audio_sid = g_strdup (self->audio_sid); - - g_mutex_unlock (&self->lock); - - if (video_sid) { - find_active_decoder_with_stream_id (self, GST_ELEMENT_FACTORY_TYPE_DECODER - | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO, video_sid); - g_free (video_sid); - } - if (audio_sid) { - find_active_decoder_with_stream_id (self, GST_ELEMENT_FACTORY_TYPE_DECODER - | GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, audio_sid); - g_free (audio_sid); - } -} - -static gboolean -clapper_get_has_flag (GstClapper * self, gint pos) -{ - gint flags; - - g_object_get (self->playbin, "flags", &flags, NULL); - - return (flags & pos) == pos; -} - -static void -clapper_set_flag (GstClapper * self, gint pos) -{ - gint flags; - - g_object_get (self->playbin, "flags", &flags, NULL); - flags |= pos; - g_object_set (self->playbin, "flags", flags, NULL); - - GST_DEBUG_OBJECT (self, "setting flags=%#x", flags); -} - -static void -clapper_clear_flag (GstClapper * self, gint pos) -{ - gint flags; - - g_object_get (self->playbin, "flags", &flags, NULL); - flags &= ~pos; - g_object_set (self->playbin, "flags", flags, NULL); - - GST_DEBUG_OBJECT (self, "setting flags=%#x", flags); -} - -static GstCaps * -get_caps (GstClapper * self, gint stream_index, GType type) -{ - GstPad *pad = NULL; - GstCaps *caps = NULL; - - if (type == GST_TYPE_CLAPPER_VIDEO_INFO) - g_signal_emit_by_name (G_OBJECT (self->playbin), - "get-video-pad", stream_index, &pad); - else if (type == GST_TYPE_CLAPPER_AUDIO_INFO) - g_signal_emit_by_name (G_OBJECT (self->playbin), - "get-audio-pad", stream_index, &pad); - else - g_signal_emit_by_name (G_OBJECT (self->playbin), - "get-text-pad", stream_index, &pad); - - if (pad) { - caps = gst_pad_get_current_caps (pad); - gst_object_unref (pad); - } - - return caps; -} - -static void -gst_clapper_subtitle_info_update (GstClapper * self, - GstClapperStreamInfo * stream_info) -{ - GstClapperSubtitleInfo *info = (GstClapperSubtitleInfo *) stream_info; - - /* Free the old info */ - g_free (info->title); - info->title = NULL; - g_free (info->language); - info->language = NULL; - - if (stream_info->tags) { - gst_tag_list_get_string (stream_info->tags, GST_TAG_TITLE, &info->title); - - /* First try to get the language full name from tag, if name is not - * available then try language code. If we find the language code - * then use gstreamer api to translate code to full name. - */ - gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_NAME, - &info->language); - if (!info->language) { - gchar *lang_code = NULL; - - gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_CODE, - &lang_code); - if (lang_code) { - info->language = g_strdup (gst_tag_get_language_name (lang_code)); - g_free (lang_code); - } - } - - /* If we are still failed to find language name then check if external - * subtitle is loaded and compare the stream index between current sub - * stream index with our stream index and if matches then declare it as - * external subtitle and use the filename. - */ - if (!info->language) { - gint text_index = -1; - gchar *suburi = NULL; - - g_object_get (G_OBJECT (self->playbin), "current-suburi", &suburi, NULL); - if (suburi) { - if (self->use_playbin3) { - if (!g_strcmp0 (self->subtitle_sid, stream_info->stream_id)) - info->language = g_path_get_basename (suburi); - } else { - g_object_get (G_OBJECT (self->playbin), "current-text", &text_index, - NULL); - if (text_index == gst_clapper_stream_info_get_index (stream_info)) - info->language = g_path_get_basename (suburi); - } - g_free (suburi); - } - } - } - - GST_DEBUG_OBJECT (self, "Subtitle title: %s", info->title); - GST_DEBUG_OBJECT (self, "Subtitle language: %s", info->language); -} - -static void -gst_clapper_video_info_update (GstClapper * self, - GstClapperStreamInfo * stream_info) -{ - GstClapperVideoInfo *info = (GstClapperVideoInfo *) stream_info; - - if (stream_info->caps) { - GstStructure *s; - - s = gst_caps_get_structure (stream_info->caps, 0); - if (s) { - gint width, height; - gint fps_n, fps_d; - gint par_n, par_d; - - if (gst_structure_get_int (s, "width", &width)) - info->width = width; - else - info->width = -1; - - if (gst_structure_get_int (s, "height", &height)) - info->height = height; - else - info->height = -1; - - if (gst_structure_get_fraction (s, "framerate", &fps_n, &fps_d)) { - info->framerate_num = fps_n; - info->framerate_denom = fps_d; - } else { - info->framerate_num = 0; - info->framerate_denom = 1; - } - - - if (gst_structure_get_fraction (s, "pixel-aspect-ratio", &par_n, &par_d)) { - info->par_num = par_n; - info->par_denom = par_d; - } else { - info->par_num = 1; - info->par_denom = 1; - } - } - } else { - info->width = info->height = -1; - info->par_num = info->par_denom = 1; - info->framerate_num = 0; - info->framerate_denom = 1; - } - - if (stream_info->tags) { - guint bitrate, max_bitrate; - - if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_BITRATE, &bitrate)) - info->bitrate = bitrate; - else - info->bitrate = -1; - - if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_MAXIMUM_BITRATE, - &max_bitrate) || gst_tag_list_get_uint (stream_info->tags, - GST_TAG_NOMINAL_BITRATE, &max_bitrate)) - info->max_bitrate = max_bitrate; - else - info->max_bitrate = -1; - } else { - info->bitrate = info->max_bitrate = -1; - } - - GST_DEBUG_OBJECT (self, "width=%d height=%d fps=%.2f par=%d:%d " - "bitrate=%d max_bitrate=%d", info->width, info->height, - (gdouble) info->framerate_num / info->framerate_denom, - info->par_num, info->par_denom, info->bitrate, info->max_bitrate); -} - -static void -gst_clapper_audio_info_update (GstClapper * self, - GstClapperStreamInfo * stream_info) -{ - GstClapperAudioInfo *info = (GstClapperAudioInfo *) stream_info; - - if (stream_info->caps) { - GstStructure *s; - - s = gst_caps_get_structure (stream_info->caps, 0); - if (s) { - gint rate, channels; - - if (gst_structure_get_int (s, "rate", &rate)) - info->sample_rate = rate; - else - info->sample_rate = -1; - - if (gst_structure_get_int (s, "channels", &channels)) - info->channels = channels; - else - info->channels = 0; - } - } else { - info->sample_rate = -1; - info->channels = 0; - } - - if (stream_info->tags) { - guint bitrate, max_bitrate; - - if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_BITRATE, &bitrate)) - info->bitrate = bitrate; - else - info->bitrate = -1; - - if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_MAXIMUM_BITRATE, - &max_bitrate) || gst_tag_list_get_uint (stream_info->tags, - GST_TAG_NOMINAL_BITRATE, &max_bitrate)) - info->max_bitrate = max_bitrate; - else - info->max_bitrate = -1; - - /* if we have old language the free it */ - g_free (info->language); - info->language = NULL; - - /* First try to get the language full name from tag, if name is not - * available then try language code. If we find the language code - * then use gstreamer api to translate code to full name. - */ - gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_NAME, - &info->language); - if (!info->language) { - gchar *lang_code = NULL; - - gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_CODE, - &lang_code); - if (lang_code) { - info->language = g_strdup (gst_tag_get_language_name (lang_code)); - g_free (lang_code); - } - } - } else { - g_free (info->language); - info->language = NULL; - info->max_bitrate = info->bitrate = -1; - } - - GST_DEBUG_OBJECT (self, "language=%s rate=%d channels=%d bitrate=%d " - "max_bitrate=%d", info->language, info->sample_rate, info->channels, - info->bitrate, info->max_bitrate); -} - -static GstClapperStreamInfo * -gst_clapper_stream_info_find (GstClapperMediaInfo * media_info, - GType type, gint stream_index) -{ - GList *list, *l; - GstClapperStreamInfo *info = NULL; - - if (!media_info) - return NULL; - - list = gst_clapper_media_info_get_stream_list (media_info); - for (l = list; l != NULL; l = l->next) { - info = (GstClapperStreamInfo *) l->data; - if ((G_OBJECT_TYPE (info) == type) && (info->stream_index == stream_index)) { - return info; - } - } - - return NULL; -} - -static GstClapperStreamInfo * -gst_clapper_stream_info_find_from_stream_id (GstClapperMediaInfo * media_info, - const gchar * stream_id) -{ - GList *list, *l; - GstClapperStreamInfo *info = NULL; - - if (!media_info) - return NULL; - - list = gst_clapper_media_info_get_stream_list (media_info); - for (l = list; l != NULL; l = l->next) { - info = (GstClapperStreamInfo *) l->data; - if (!g_strcmp0 (info->stream_id, stream_id)) { - return info; - } - } - - return NULL; -} - -static GstClapperStreamInfo * -gst_clapper_stream_info_get_current (GstClapper * self, const gchar * prop, - GType type) -{ - gint current; - GstClapperStreamInfo *info; - - if (!self->media_info) - return NULL; - - g_object_get (G_OBJECT (self->playbin), prop, ¤t, NULL); - g_mutex_lock (&self->lock); - info = gst_clapper_stream_info_find (self->media_info, type, current); - if (info) - info = gst_clapper_stream_info_copy (info); - g_mutex_unlock (&self->lock); - - return info; -} - -static GstClapperStreamInfo * -gst_clapper_stream_info_get_current_from_stream_id (GstClapper * self, - const gchar * stream_id, GType type) -{ - GstClapperStreamInfo *info; - - if (!self->media_info || !stream_id) - return NULL; - - g_mutex_lock (&self->lock); - info = - gst_clapper_stream_info_find_from_stream_id (self->media_info, stream_id); - if (info && G_OBJECT_TYPE (info) == type) - info = gst_clapper_stream_info_copy (info); - else - info = NULL; - g_mutex_unlock (&self->lock); - - return info; -} - -static void -stream_notify_cb (GstStreamCollection * collection, GstStream * stream, - GParamSpec * pspec, GstClapper * self) -{ - GstClapperStreamInfo *info; - const gchar *stream_id; - gboolean emit_update = FALSE; - - if (!self->media_info) - return; - - if (G_PARAM_SPEC_VALUE_TYPE (pspec) != GST_TYPE_CAPS && - G_PARAM_SPEC_VALUE_TYPE (pspec) != GST_TYPE_TAG_LIST) - return; - - stream_id = gst_stream_get_stream_id (stream); - g_mutex_lock (&self->lock); - info = - gst_clapper_stream_info_find_from_stream_id (self->media_info, stream_id); - if (info) { - gst_clapper_stream_info_update_from_stream (self, info, stream); - emit_update = (self->needs_info_update && GST_IS_CLAPPER_VIDEO_INFO (info)); - } - g_mutex_unlock (&self->lock); - - if (emit_update) - emit_media_info_updated (self); -} - -static void -gst_clapper_stream_info_update (GstClapper * self, GstClapperStreamInfo * s) -{ - if (GST_IS_CLAPPER_VIDEO_INFO (s)) - gst_clapper_video_info_update (self, s); - else if (GST_IS_CLAPPER_AUDIO_INFO (s)) - gst_clapper_audio_info_update (self, s); - else - gst_clapper_subtitle_info_update (self, s); -} - -static gchar * -stream_info_get_codec (GstClapperStreamInfo * s) -{ - const gchar *type; - GstTagList *tags; - gchar *codec = NULL; - - if (GST_IS_CLAPPER_VIDEO_INFO (s)) - type = GST_TAG_VIDEO_CODEC; - else if (GST_IS_CLAPPER_AUDIO_INFO (s)) - type = GST_TAG_AUDIO_CODEC; - else - type = GST_TAG_SUBTITLE_CODEC; - - tags = gst_clapper_stream_info_get_tags (s); - if (tags) { - gst_tag_list_get_string (tags, type, &codec); - if (!codec) - gst_tag_list_get_string (tags, GST_TAG_CODEC, &codec); - } - - if (!codec) { - GstCaps *caps; - caps = gst_clapper_stream_info_get_caps (s); - if (caps) { - codec = gst_pb_utils_get_codec_description (caps); - } - } - - return codec; -} - -static void -gst_clapper_stream_info_update_tags_and_caps (GstClapper * self, - GstClapperStreamInfo * s) -{ - GstTagList *tags; - gint stream_index; - - stream_index = gst_clapper_stream_info_get_index (s); - - if (GST_IS_CLAPPER_VIDEO_INFO (s)) - g_signal_emit_by_name (self->playbin, "get-video-tags", - stream_index, &tags); - else if (GST_IS_CLAPPER_AUDIO_INFO (s)) - g_signal_emit_by_name (self->playbin, "get-audio-tags", - stream_index, &tags); - else - g_signal_emit_by_name (self->playbin, "get-text-tags", stream_index, &tags); - - if (s->tags) - gst_tag_list_unref (s->tags); - s->tags = tags; - - if (s->caps) - gst_caps_unref (s->caps); - s->caps = get_caps (self, stream_index, G_OBJECT_TYPE (s)); - - g_free (s->codec); - s->codec = stream_info_get_codec (s); - - GST_DEBUG_OBJECT (self, "%s index: %d tags: %p caps: %p", - gst_clapper_stream_info_get_stream_type (s), stream_index, - s->tags, s->caps); - - gst_clapper_stream_info_update (self, s); -} - -static void -gst_clapper_streams_info_create (GstClapper * self, - GstClapperMediaInfo * media_info, const gchar * prop, GType type) -{ - gint i; - gint total = -1; - GstClapperStreamInfo *s; - - if (!media_info) - return; - - g_object_get (G_OBJECT (self->playbin), prop, &total, NULL); - - GST_DEBUG_OBJECT (self, "%s: %d", prop, total); - - for (i = 0; i < total; i++) { - /* check if stream already exist in the list */ - s = gst_clapper_stream_info_find (media_info, type, i); - - if (!s) { - /* create a new stream info instance */ - s = gst_clapper_stream_info_new (i, type); - - /* add the object in stream list */ - media_info->stream_list = g_list_append (media_info->stream_list, s); - - /* based on type, add the object in its corresponding stream_ list */ - if (GST_IS_CLAPPER_AUDIO_INFO (s)) - media_info->audio_stream_list = g_list_append - (media_info->audio_stream_list, s); - else if (GST_IS_CLAPPER_VIDEO_INFO (s)) - media_info->video_stream_list = g_list_append - (media_info->video_stream_list, s); - else - media_info->subtitle_stream_list = g_list_append - (media_info->subtitle_stream_list, s); - - GST_DEBUG_OBJECT (self, "create %s stream stream_index: %d", - gst_clapper_stream_info_get_stream_type (s), i); - } - - gst_clapper_stream_info_update_tags_and_caps (self, s); - } -} - -static void -gst_clapper_stream_info_update_from_stream (GstClapper * self, - GstClapperStreamInfo * s, GstStream * stream) -{ - if (s->tags) - gst_tag_list_unref (s->tags); - s->tags = gst_stream_get_tags (stream); - - if (s->caps) - gst_caps_unref (s->caps); - s->caps = gst_stream_get_caps (stream); - - g_free (s->codec); - s->codec = stream_info_get_codec (s); - - GST_DEBUG_OBJECT (self, "%s index: %d tags: %p caps: %p", - gst_clapper_stream_info_get_stream_type (s), s->stream_index, - s->tags, s->caps); - - gst_clapper_stream_info_update (self, s); -} - -static void -gst_clapper_streams_info_create_from_collection (GstClapper * self, - GstClapperMediaInfo * media_info, GstStreamCollection * collection) -{ - guint i; - guint total; - GstClapperStreamInfo *s; - guint n_audio = 0; - guint n_video = 0; - guint n_text = 0; - - if (!media_info || !collection) - return; - - total = gst_stream_collection_get_size (collection); - - for (i = 0; i < total; i++) { - GstStream *stream = gst_stream_collection_get_stream (collection, i); - GstStreamType stream_type = gst_stream_get_stream_type (stream); - const gchar *stream_id = gst_stream_get_stream_id (stream); - - if (stream_type & GST_STREAM_TYPE_AUDIO) { - s = gst_clapper_stream_info_new (n_audio, GST_TYPE_CLAPPER_AUDIO_INFO); - n_audio++; - } else if (stream_type & GST_STREAM_TYPE_VIDEO) { - s = gst_clapper_stream_info_new (n_video, GST_TYPE_CLAPPER_VIDEO_INFO); - n_video++; - } else if (stream_type & GST_STREAM_TYPE_TEXT) { - s = gst_clapper_stream_info_new (n_text, GST_TYPE_CLAPPER_SUBTITLE_INFO); - n_text++; - } else { - GST_DEBUG_OBJECT (self, "Unknown type stream %d", i); - continue; - } - - s->stream_id = g_strdup (stream_id); - - /* add the object in stream list */ - media_info->stream_list = g_list_append (media_info->stream_list, s); - - /* based on type, add the object in its corresponding stream_ list */ - if (GST_IS_CLAPPER_AUDIO_INFO (s)) - media_info->audio_stream_list = g_list_append - (media_info->audio_stream_list, s); - else if (GST_IS_CLAPPER_VIDEO_INFO (s)) - media_info->video_stream_list = g_list_append - (media_info->video_stream_list, s); - else - media_info->subtitle_stream_list = g_list_append - (media_info->subtitle_stream_list, s); - - GST_DEBUG_OBJECT (self, "create %s stream stream_index: %d", - gst_clapper_stream_info_get_stream_type (s), s->stream_index); - - gst_clapper_stream_info_update_from_stream (self, s, stream); - } -} - -static void -video_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - g_mutex_lock (&self->lock); - gst_clapper_streams_info_create (self, self->media_info, - "n-video", GST_TYPE_CLAPPER_VIDEO_INFO); - g_mutex_unlock (&self->lock); -} - -static void -audio_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - g_mutex_lock (&self->lock); - gst_clapper_streams_info_create (self, self->media_info, - "n-audio", GST_TYPE_CLAPPER_AUDIO_INFO); - g_mutex_unlock (&self->lock); -} - -static void -subtitle_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - g_mutex_lock (&self->lock); - gst_clapper_streams_info_create (self, self->media_info, - "n-text", GST_TYPE_CLAPPER_SUBTITLE_INFO); - g_mutex_unlock (&self->lock); -} - -static gchar * -get_title_from_uri (const gchar * uri) -{ - gchar *proto = gst_uri_get_protocol (uri); - gchar *title = NULL; - - if (strcmp (proto, "file") == 0) { - const gchar *ext = strrchr (uri, '.'); - if (ext && strlen (ext) < 8) { - gchar *filename = g_filename_from_uri (uri, NULL, NULL); - if (filename) { - gchar *base = g_path_get_basename (filename); - g_free (filename); - title = g_strndup (base, strlen (base) - strlen (ext)); - g_free (base); - } - } - } else if (strcmp (proto, "dvb") == 0) { - const gchar *channel = strrchr (uri, '/') + 1; - title = g_strdup (channel); - } - g_free (proto); - - return title; -} - -static void * -get_title (GstTagList * tags) -{ - gchar *title = NULL; - - gst_tag_list_get_string (tags, GST_TAG_TITLE, &title); - if (!title) - gst_tag_list_get_string (tags, GST_TAG_TITLE_SORTNAME, &title); - - return title; -} - -static void * -get_container_format (GstTagList * tags) -{ - gchar *container = NULL; - - gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container); - - /* TODO: If container is not available then maybe consider - * parsing caps or file extension to guess the container format. - */ - - return container; -} - -static void * -get_from_tags (GstClapper * self, GstClapperMediaInfo * media_info, - void *(*func) (GstTagList *)) -{ - GList *l; - void *ret = NULL; - - if (media_info->tags) { - ret = func (media_info->tags); - if (ret) - return ret; - } - - /* if global tag does not exit then try video and audio streams */ - GST_DEBUG_OBJECT (self, "trying video tags"); - for (l = gst_clapper_media_info_get_video_streams (media_info); l != NULL; - l = l->next) { - GstTagList *tags; - - tags = gst_clapper_stream_info_get_tags ((GstClapperStreamInfo *) l->data); - if (tags) - ret = func (tags); - - if (ret) - return ret; - } - - GST_DEBUG_OBJECT (self, "trying audio tags"); - for (l = gst_clapper_media_info_get_audio_streams (media_info); l != NULL; - l = l->next) { - GstTagList *tags; - - tags = gst_clapper_stream_info_get_tags ((GstClapperStreamInfo *) l->data); - if (tags) - ret = func (tags); - - if (ret) - return ret; - } - - GST_DEBUG_OBJECT (self, "failed to get the information from tags"); - return NULL; -} - -static void * -get_cover_sample (GstTagList * tags) -{ - GstSample *cover_sample = NULL; - - gst_tag_list_get_sample (tags, GST_TAG_IMAGE, &cover_sample); - if (!cover_sample) - gst_tag_list_get_sample (tags, GST_TAG_PREVIEW_IMAGE, &cover_sample); - - return cover_sample; -} - -static GstClapperMediaInfo * -gst_clapper_media_info_create (GstClapper * self) -{ - GstClapperMediaInfo *media_info; - GstQuery *query; - - GST_DEBUG_OBJECT (self, "begin"); - media_info = gst_clapper_media_info_new (self->uri); - media_info->duration = gst_clapper_get_duration (self); - media_info->tags = self->global_tags; - media_info->toc = self->global_toc; - media_info->is_live = self->is_live; - self->global_tags = NULL; - self->global_toc = NULL; - - query = gst_query_new_seeking (GST_FORMAT_TIME); - if (gst_element_query (self->playbin, query)) - gst_query_parse_seeking (query, NULL, &media_info->seekable, NULL, NULL); - gst_query_unref (query); - - if (self->use_playbin3 && self->collection) { - gst_clapper_streams_info_create_from_collection (self, media_info, - self->collection); - } else { - /* create audio/video/sub streams */ - gst_clapper_streams_info_create (self, media_info, "n-video", - GST_TYPE_CLAPPER_VIDEO_INFO); - gst_clapper_streams_info_create (self, media_info, "n-audio", - GST_TYPE_CLAPPER_AUDIO_INFO); - gst_clapper_streams_info_create (self, media_info, "n-text", - GST_TYPE_CLAPPER_SUBTITLE_INFO); - } - - media_info->title = get_from_tags (self, media_info, get_title); - if (!media_info->title) - media_info->title = get_title_from_uri (self->uri); - - media_info->container = - get_from_tags (self, media_info, get_container_format); - media_info->image_sample = get_from_tags (self, media_info, get_cover_sample); - - GST_DEBUG_OBJECT (self, "uri: %s, title: %s, duration: %" GST_TIME_FORMAT - ", seekable: %s, live: %s, container: %s, image_sample %p", - media_info->uri, media_info->title, GST_TIME_ARGS (media_info->duration), - media_info->seekable ? "yes" : "no", media_info->is_live ? "yes" : "no", - media_info->container, media_info->image_sample); - - GST_DEBUG_OBJECT (self, "end"); - return media_info; -} - -static void -tags_changed_cb (GstClapper * self, gint stream_index, GType type) -{ - GstClapperStreamInfo *s; - - if (!self->media_info) - return; - - /* update the stream information */ - g_mutex_lock (&self->lock); - s = gst_clapper_stream_info_find (self->media_info, type, stream_index); - gst_clapper_stream_info_update_tags_and_caps (self, s); - g_mutex_unlock (&self->lock); -} - -static void -video_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index, - gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - tags_changed_cb (self, stream_index, - GST_TYPE_CLAPPER_VIDEO_INFO); - - if (self->needs_info_update) - emit_media_info_updated (self); -} - -static void -audio_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index, - gpointer user_data) -{ - tags_changed_cb (GST_CLAPPER (user_data), stream_index, - GST_TYPE_CLAPPER_AUDIO_INFO); -} - -static void -subtitle_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index, - gpointer user_data) -{ - tags_changed_cb (GST_CLAPPER (user_data), stream_index, - GST_TYPE_CLAPPER_SUBTITLE_INFO); -} - -static void -volume_notify_dispatch (gpointer user_data) -{ - GstClapper *clapper = user_data; - - if (clapper->inhibit_sigs) - return; - - g_object_notify_by_pspec (G_OBJECT (clapper), param_specs[PROP_VOLUME]); -} - -static void -volume_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec, - GstClapper * self) -{ - gdouble volume = gst_clapper_get_volume (self); - - if (self->last_volume != volume) { - self->last_volume = volume; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - volume_notify_dispatch, g_object_ref (self), - (GDestroyNotify) g_object_unref); - } -} - -static void -mute_notify_dispatch (gpointer user_data) -{ - GstClapper *clapper = user_data; - - if (clapper->inhibit_sigs) - return; - - g_object_notify_by_pspec (G_OBJECT (clapper), param_specs[PROP_MUTE]); -} - -static void -mute_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec, - GstClapper * self) -{ - gboolean mute = gst_clapper_get_mute (self); - - if (self->last_mute != mute) { - self->last_mute = mute; - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - mute_notify_dispatch, g_object_ref (self), - (GDestroyNotify) g_object_unref); - } -} - -typedef struct -{ - GstClapper *clapper; - gchar *decoder_name; -} DecoderChangedSignalData; - -static void -video_decoder_changed_dispatch (gpointer user_data) -{ - DecoderChangedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - g_signal_emit (data->clapper, signals[SIGNAL_VIDEO_DECODER_CHANGED], - 0, data->decoder_name); -} - -static void -audio_decoder_changed_dispatch (gpointer user_data) -{ - DecoderChangedSignalData *data = user_data; - - if (data->clapper->inhibit_sigs) - return; - - g_signal_emit (data->clapper, signals[SIGNAL_AUDIO_DECODER_CHANGED], - 0, data->decoder_name); -} - -static void -decoder_changed_signal_data_free (DecoderChangedSignalData * data) -{ - g_object_unref (data->clapper); - g_free (data->decoder_name); - g_free (data); -} - -static void -emit_decoder_changed (GstClapper * self, gchar * decoder_name, - GstElementFactoryListType type) -{ - GstClapperSignalDispatcherFunc func = NULL; - - if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) == - GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) { - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_VIDEO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 && - g_strcmp0 (self->last_vdecoder, decoder_name) != 0) { - func = video_decoder_changed_dispatch; - g_free (self->last_vdecoder); - self->last_vdecoder = g_strdup (decoder_name); - } - } else if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) == - GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) { - if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID, - signals[SIGNAL_AUDIO_DECODER_CHANGED], 0, NULL, NULL, NULL) != 0 && - g_strcmp0 (self->last_adecoder, decoder_name) != 0) { - func = audio_decoder_changed_dispatch; - g_free (self->last_adecoder); - self->last_adecoder = g_strdup (decoder_name); - } - } - - if (func) { - DecoderChangedSignalData *data = g_new (DecoderChangedSignalData, 1); - - data->clapper = g_object_ref (self); - data->decoder_name = g_strdup (decoder_name); - - gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self, - func, data, (GDestroyNotify) decoder_changed_signal_data_free); - } -} - -static gboolean -iterate_decoder_pads (GstClapper * self, GstElement * element, - const gchar * stream_id, GstElementFactoryListType type) -{ - GstIterator *iter; - GValue value = { 0, }; - gboolean found = FALSE; - - iter = gst_element_iterate_src_pads (element); - - while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) { - GstPad *decoder_pad = g_value_get_object (&value); - gchar *decoder_stream_id = gst_pad_get_stream_id (decoder_pad); - - GST_DEBUG_OBJECT (self, "Decoder stream: %s", decoder_stream_id); - - /* In case of playbin3, pad may not be active yet */ - if ((found = (g_strcmp0 (decoder_stream_id, stream_id) == 0 - || (!decoder_stream_id && self->use_playbin3)))) { - GstElementFactory *factory; - gchar *plugin_name; - - factory = gst_element_get_factory (element); - plugin_name = gst_object_get_name (GST_OBJECT_CAST (factory)); - - if (plugin_name) { - GST_DEBUG_OBJECT (self, "Found decoder: %s", plugin_name); - emit_decoder_changed (self, plugin_name, type); - - g_free (plugin_name); - } - } - - g_free (decoder_stream_id); - g_value_unset (&value); - - if (found) - break; - } - - gst_iterator_free (iter); - - return found; -} - -static gboolean -find_active_decoder_with_stream_id (GstClapper * self, GstElementFactoryListType type, - const gchar * stream_id) -{ - GstIterator *iter; - GValue value = { 0, }; - gboolean found = FALSE; - - GST_DEBUG_OBJECT (self, "Searching for decoder with stream: %s", stream_id); - - iter = gst_bin_iterate_recurse (GST_BIN (self->playbin)); - - while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) { - GstElement *element = g_value_get_object (&value); - GstElementFactory *factory = gst_element_get_factory (element); - - if (factory && gst_element_factory_list_is_type (factory, type)) - found = iterate_decoder_pads (self, element, stream_id, type); - - g_value_unset (&value); - - if (found) - break; - } - - gst_iterator_free (iter); - - return found; -} - -static void -update_current_decoder (GstClapper *self, GstElementFactoryListType type) -{ - GstIterator *iter; - GValue value = { 0, }; - - iter = gst_bin_iterate_all_by_element_factory_name ( - GST_BIN (self->playbin), "input-selector"); - - while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) { - GstElement *element = g_value_get_object (&value); - GstPad *active_pad; - gboolean found = FALSE; - - g_object_get (G_OBJECT (element), "active-pad", &active_pad, NULL); - - if (active_pad) { - gchar *stream_id; - - stream_id = gst_pad_get_stream_id (active_pad); - gst_object_unref (active_pad); - - if (stream_id) { - found = find_active_decoder_with_stream_id (self, type, stream_id); - g_free (stream_id); - } - } - - g_value_unset (&value); - - if (found) - break; - } - - gst_iterator_free (iter); -} - -static void -current_video_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec, - GstClapper * self) -{ - GstElementFactoryListType type = GST_ELEMENT_FACTORY_TYPE_DECODER - | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO; - - update_current_decoder (self, type); -} - -static void -current_audio_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec, - GstClapper * self) -{ - GstElementFactoryListType type = GST_ELEMENT_FACTORY_TYPE_DECODER - | GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO; - - update_current_decoder (self, type); -} - -static void -element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self) -{ - GstElementFactory *factory; - - if ((factory = gst_element_get_factory (element))) { - gchar *plugin_name = gst_object_get_name (GST_OBJECT_CAST (factory)); - if (plugin_name) { - GST_INFO_OBJECT (self, "Plugin setup: %s", plugin_name); - - /* TODO: Set plugin props */ - - g_free (plugin_name); - } - } -} - -static void -_update_from_env (gboolean * enabled, const gchar * env_name) -{ - const gchar *env = g_getenv (env_name); - - if (env) { - if (g_str_has_prefix (env, "1")) - *enabled = TRUE; - else if (g_str_has_prefix (env, "0")) - *enabled = FALSE; - } -} - -static gpointer -gst_clapper_main (gpointer data) -{ - GstClapper *self = GST_CLAPPER (data); - GstBus *bus; - GSource *source; - GstElement *scaletempo, *pipewiresink; - - GST_TRACE_OBJECT (self, "Starting main thread"); - - g_main_context_push_thread_default (self->context); - - source = g_idle_source_new (); - g_source_set_callback (source, (GSourceFunc) main_loop_running_cb, self, - NULL); - g_source_attach (source, self->context); - g_source_unref (source); - - _update_from_env (&self->use_playbin3, "GST_CLAPPER_USE_PLAYBIN3"); - - /* Takes precedence over `GST_CLAPPER_USE_PLAYBIN3` as it - * influences element factory behavior */ - _update_from_env (&self->use_playbin3, "USE_PLAYBIN3"); - - if (self->use_playbin3) { - GST_DEBUG_OBJECT (self, "playbin3 enabled"); - self->playbin = gst_element_factory_make ("playbin3", "playbin3"); - } else { - self->playbin = gst_element_factory_make ("playbin", "playbin"); - } - - if (!self->playbin) { - g_error ("GstClapper: 'playbin' element not found, please check your setup"); - g_assert_not_reached (); - } - - gst_object_ref_sink (self->playbin); - - /* Do not start muted and make it sync with last_mute value */ - g_object_set (self->playbin, "mute", FALSE, NULL); - - if (self->video_renderer) { - GstElement *video_sink = - gst_clapper_video_renderer_create_video_sink (self->video_renderer, self); - if (video_sink) { - const gchar *fps_env; - GstPad *video_sink_pad = gst_element_get_static_pad (video_sink, "sink"); - if (video_sink_pad) { - g_signal_connect (video_sink_pad, "notify::caps", - (GCallback) notify_caps_cb, self); - gst_object_unref (video_sink_pad); - } - fps_env = g_getenv ("GST_CLAPPER_DISPLAY_FPS"); - if (fps_env && g_str_has_prefix (fps_env, "1")) { - GstElement *fpsdisplaysink = - gst_element_factory_make ("fpsdisplaysink", "fpsdisplaysink"); - if (fpsdisplaysink) { - GST_DEBUG_OBJECT (self, "FPS display enabled"); - g_object_set (fpsdisplaysink, "video-sink", video_sink, NULL); - video_sink = fpsdisplaysink; - } - } - g_object_set (self->playbin, "video-sink", video_sink, NULL); - } - } - - _update_from_env (&self->use_pipewire, "GST_CLAPPER_USE_PIPEWIRE"); - - if (self->use_pipewire) { - pipewiresink = gst_element_factory_make ("pipewiresink", NULL); - if (pipewiresink) { - g_object_set (self->playbin, "audio-sink", pipewiresink, NULL); - } else { - GstElement *fakesink; - - g_warning ("GstClapper: pipewiresink element not available"); - fakesink = gst_element_factory_make ("fakesink", "fakeaudiosink"); - if (fakesink) { - g_object_set (fakesink, "sync", TRUE, NULL); - g_object_set (self->playbin, "audio-sink", fakesink, NULL); - } else { - g_warning ("GstClapper: default audio sink will be used instead"); - } - } - } - - scaletempo = gst_element_factory_make ("scaletempo", NULL); - if (scaletempo) { - g_object_set (self->playbin, "audio-filter", scaletempo, NULL); - } else { - g_warning ("GstClapper: scaletempo element not available. " - "Audio pitch will not be preserved during trick modes."); - } - - self->bus = bus = gst_element_get_bus (self->playbin); - gst_bus_add_signal_watch (bus); - - if (self->mpris) - gst_clapper_mpris_set_clapper (self->mpris, self, self->signal_dispatcher); - - g_signal_connect (G_OBJECT (bus), "message::error", G_CALLBACK (error_cb), - self); - g_signal_connect (G_OBJECT (bus), "message::warning", G_CALLBACK (warning_cb), - self); - g_signal_connect (G_OBJECT (bus), "message::eos", G_CALLBACK (eos_cb), self); - g_signal_connect (G_OBJECT (bus), "message::state-changed", - G_CALLBACK (state_changed_cb), self); - g_signal_connect (G_OBJECT (bus), "message::buffering", - G_CALLBACK (buffering_cb), self); - g_signal_connect (G_OBJECT (bus), "message::clock-lost", - G_CALLBACK (clock_lost_cb), self); - g_signal_connect (G_OBJECT (bus), "message::duration-changed", - G_CALLBACK (duration_changed_cb), self); - g_signal_connect (G_OBJECT (bus), "message::latency", - G_CALLBACK (latency_cb), self); - g_signal_connect (G_OBJECT (bus), "message::request-state", - G_CALLBACK (request_state_cb), self); - g_signal_connect (G_OBJECT (bus), "message::element", - G_CALLBACK (element_cb), self); - g_signal_connect (G_OBJECT (bus), "message::tag", G_CALLBACK (tags_cb), self); - g_signal_connect (G_OBJECT (bus), "message::toc", G_CALLBACK (toc_cb), self); - - if (gst_debug_category_get_threshold (gst_clapper_debug) >= GST_LEVEL_DEBUG) - g_signal_connect (G_OBJECT (bus), "message::qos", G_CALLBACK (qos_cb), self); - - if (self->use_playbin3) { - g_signal_connect (G_OBJECT (bus), "message::stream-collection", - G_CALLBACK (stream_collection_cb), self); - g_signal_connect (G_OBJECT (bus), "message::streams-selected", - G_CALLBACK (streams_selected_cb), self); - } else { - g_signal_connect (self->playbin, "video-changed", - G_CALLBACK (video_changed_cb), self); - g_signal_connect (self->playbin, "audio-changed", - G_CALLBACK (audio_changed_cb), self); - g_signal_connect (self->playbin, "text-changed", - G_CALLBACK (subtitle_changed_cb), self); - - g_signal_connect (self->playbin, "video-tags-changed", - G_CALLBACK (video_tags_changed_cb), self); - g_signal_connect (self->playbin, "audio-tags-changed", - G_CALLBACK (audio_tags_changed_cb), self); - g_signal_connect (self->playbin, "text-tags-changed", - G_CALLBACK (subtitle_tags_changed_cb), self); - - g_signal_connect (self->playbin, "notify::current-video", - G_CALLBACK (current_video_notify_cb), self); - g_signal_connect (self->playbin, "notify::current-audio", - G_CALLBACK (current_audio_notify_cb), self); - } - - g_signal_connect (self->playbin, "notify::volume", - G_CALLBACK (volume_notify_cb), self); - g_signal_connect (self->playbin, "notify::mute", - G_CALLBACK (mute_notify_cb), self); - g_signal_connect (self->playbin, "element-setup", - G_CALLBACK (element_setup_cb), self); - - self->target_state = GST_STATE_NULL; - self->current_state = GST_STATE_NULL; - change_state (self, GST_CLAPPER_STATE_STOPPED); - self->buffering = 100; - self->is_live = FALSE; - self->rate = 1.0; - self->seek_mode = DEFAULT_SEEK_MODE; - self->cached_duration = GST_CLOCK_TIME_NONE; - - GST_TRACE_OBJECT (self, "Starting main loop"); - g_main_loop_run (self->loop); - GST_TRACE_OBJECT (self, "Stopped main loop"); - - gst_bus_remove_signal_watch (bus); - gst_object_unref (bus); - - remove_tick_source (self); - - g_mutex_lock (&self->lock); - if (self->media_info) { - g_object_unref (self->media_info); - self->media_info = NULL; - } - - remove_seek_source (self); - g_mutex_unlock (&self->lock); - - g_main_context_pop_thread_default (self->context); - - self->target_state = GST_STATE_NULL; - self->current_state = GST_STATE_NULL; - if (self->playbin) { - gst_element_set_state (self->playbin, GST_STATE_NULL); - gst_object_unref (self->playbin); - self->playbin = NULL; - } - - GST_TRACE_OBJECT (self, "Stopped main thread"); - - return NULL; -} - -static gboolean -gst_clapper_set_feature_rank_versioned (const gchar * name, guint rank, - guint min_major, guint min_minor, guint min_micro) -{ - GstRegistry *registry = gst_registry_get (); - GstPluginFeature *feature = gst_registry_lookup_feature (registry, name); - gboolean res = FALSE; - - if (!feature) { - GST_DEBUG ("Cannot change rank of unavailable feature: %s", name); - return res; - } - if (gst_plugin_feature_check_version (feature, min_major, min_minor, min_micro)) { - guint old_rank = gst_plugin_feature_get_rank (feature); - gst_plugin_feature_set_rank (feature, rank); - res = TRUE; - GST_DEBUG ("Changed rank: %i -> %i for %s", old_rank, rank, name); - } else { - GST_DEBUG ("Feature %s is at older version then required", name); - } - - gst_object_unref (feature); - return res; -} - -static gboolean -gst_clapper_set_feature_rank (const gchar * name, guint rank) -{ - return gst_clapper_set_feature_rank_versioned (name, rank, 0, 0, 0); -} - -static gboolean -gst_clapper_has_plugin_with_features (const gchar * name) -{ - GstRegistry *registry = gst_registry_get (); - GList *features = gst_registry_get_feature_list_by_plugin (registry, name); - - gboolean ret = g_list_length (features) > 0; - - gst_plugin_feature_list_free (features); - return ret; -} - -static gboolean -parse_feature_name (gchar * str, const gchar ** feature) -{ - if (!str) - return FALSE; - - g_strstrip (str); - - if (str[0] != '\0') { - *feature = str; - return TRUE; - } - - return FALSE; -} - -static gboolean -parse_feature_rank (gchar * str, GstRank * rank) -{ - if (!str) - return FALSE; - - g_strstrip (str); - - if (g_ascii_isdigit (str[0])) { - unsigned long l; - char *endptr; - l = strtoul (str, &endptr, 10); - if (endptr > str && endptr[0] == 0) { - *rank = (GstRank) l; - } else { - return FALSE; - } - } else if (g_ascii_strcasecmp (str, "NONE") == 0) { - *rank = GST_RANK_NONE; - } else if (g_ascii_strcasecmp (str, "MARGINAL") == 0) { - *rank = GST_RANK_MARGINAL; - } else if (g_ascii_strcasecmp (str, "SECONDARY") == 0) { - *rank = GST_RANK_SECONDARY; - } else if (g_ascii_strcasecmp (str, "PRIMARY") == 0) { - *rank = GST_RANK_PRIMARY; - } else if (g_ascii_strcasecmp (str, "MAX") == 0) { - *rank = (GstRank) G_MAXINT; - } else { - return FALSE; - } - - return TRUE; -} - -static void -_env_feature_rank_update (void) -{ - const gchar *env; - gchar **split, **walk; - - env = g_getenv ("GST_PLUGIN_FEATURE_RANK"); - - if (!env) - return; - - split = g_strsplit (env, ",", 0); - - for (walk = split; *walk; walk++) { - if (strchr (*walk, ':')) { - gchar **values; - - values = g_strsplit (*walk, ":", 2); - if (values[0] && values[1]) { - GstRank rank; - const gchar *name; - - if (parse_feature_name (values[0], &name) - && parse_feature_rank (values[1], &rank)) { - GstPluginFeature *feature; - - feature = gst_registry_find_feature (gst_registry_get (), name, - GST_TYPE_ELEMENT_FACTORY); - if (feature) { - GstRank old_rank; - - old_rank = gst_plugin_feature_get_rank (feature); - if (old_rank != rank) { - gst_plugin_feature_set_rank (feature, rank); - GST_DEBUG ("Updated rank from env: %i -> %i for %s", old_rank, rank, name); - } - gst_object_unref (feature); - } - } - } - - g_strfreev (values); - } - } - - g_strfreev (split); -} - -static void -gst_clapper_prepare_gstreamer (void) -{ - const guint rank = GST_RANK_PRIMARY + 24; - - GST_DEBUG ("Preparing GStreamer plugins"); - - /* Too many problems with VAAPI decodebin, meanwhile VA works - * fine and there is already a pending MR on GStreamer to enable - * it by default, so we do that a little early */ - if (gst_clapper_has_plugin_with_features ("va")) { - gst_clapper_set_feature_rank ("vampeg2dec", rank); - gst_clapper_set_feature_rank ("vah264dec", rank); - gst_clapper_set_feature_rank ("vah265dec", rank); - gst_clapper_set_feature_rank ("vavp8dec", rank); - gst_clapper_set_feature_rank ("vavp9dec", rank); - gst_clapper_set_feature_rank ("vaav1dec", rank); - } - /* We do promise working HW accel out of box, so enable NVDEC too */ - if (gst_clapper_has_plugin_with_features ("nvcodec")) { - gst_clapper_set_feature_rank ("nvh264dec", rank + 4); - gst_clapper_set_feature_rank ("nvh265dec", rank + 4); - gst_clapper_set_feature_rank ("nvvp8dec", rank + 4); - gst_clapper_set_feature_rank ("nvvp9dec", rank + 4); - } - - /* After setting defaults, update them from ENV */ - _env_feature_rank_update (); - - gst_clapper_gstreamer_prepared = TRUE; - GST_DEBUG ("GStreamer plugins prepared"); -} - -/** - * gst_clapper_gst_init: - * @argc: (inout) (allow-none): pointer to application's argc - * @argv: (inout) (array length=argc) (allow-none): pointer to application's argv - * - * Automatically initializes GStreamer library if this was not done by the user yet - * and tweaks some of its defaults to our liking. It is recommended to use this - * function instead of plain gst_init(). - * - * This also allows usage of GstClapper API alone without importing GStreamer - * on the implementation side. - */ -void -gst_clapper_gst_init (int * argc, char ** argv[]) -{ - if (!gst_is_initialized ()) - gst_init (argc, argv); - if (!gst_clapper_gstreamer_prepared) - gst_clapper_prepare_gstreamer (); -} - -/** - * gst_clapper_new: - * @video_renderer: (transfer full) (allow-none): GstClapperVideoRenderer to use - * @signal_dispatcher: (transfer full) (allow-none): GstClapperSignalDispatcher to use - * @mpris: (transfer full) (allow-none): GstClapperMpris to use - * - * Creates a new #GstClapper instance that uses @signal_dispatcher to dispatch - * signals to some event loop system, or emits signals directly if NULL is - * passed. See gst_clapper_g_main_context_signal_dispatcher_new(). - * - * Video is going to be rendered by @video_renderer, or if %NULL is provided - * no special video set up will be done and some default handling will be - * performed. - * - * Returns: (transfer full): a new #GstClapper instance - */ -GstClapper * -gst_clapper_new (GstClapperVideoRenderer * video_renderer, - GstClapperSignalDispatcher * signal_dispatcher, - GstClapperMpris * mpris) -{ - GstClapper *self; - - self = g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer, - "signal-dispatcher", signal_dispatcher, "mpris", mpris, NULL); - - if (video_renderer) - g_object_unref (video_renderer); - if (signal_dispatcher) - g_object_unref (signal_dispatcher); - if (mpris) - g_object_unref (mpris); - - return self; -} - -static gboolean -gst_clapper_play_internal (gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstStateChangeReturn state_ret; - - GST_DEBUG_OBJECT (self, "Play"); - - g_mutex_lock (&self->lock); - if (!self->uri) { - g_mutex_unlock (&self->lock); - return G_SOURCE_REMOVE; - } - g_mutex_unlock (&self->lock); - - self->target_state = GST_STATE_PLAYING; - - if (self->current_state < GST_STATE_PAUSED) - change_state (self, GST_CLAPPER_STATE_BUFFERING); - - if (self->current_state >= GST_STATE_PAUSED - && self->buffering >= 100 && !(self->seek_position != GST_CLOCK_TIME_NONE - || self->seek_pending)) { - state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING); - } else { - state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED); - } - - if (state_ret == GST_STATE_CHANGE_FAILURE) { - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to play")); - return G_SOURCE_REMOVE; - } else if (state_ret == GST_STATE_CHANGE_NO_PREROLL) { - self->is_live = TRUE; - GST_DEBUG_OBJECT (self, "Pipeline is live"); - } - - return G_SOURCE_REMOVE; -} - -/** - * gst_clapper_play: - * @clapper: #GstClapper instance - * - * Request to play the loaded stream. - */ -void -gst_clapper_play (GstClapper * self) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (!self->can_start && self->app_state == GST_CLAPPER_STATE_STOPPED) { - GST_DEBUG_OBJECT (self, "Player stopped, play request ignored"); - return; - } - - g_mutex_lock (&self->lock); - self->inhibit_sigs = FALSE; - self->can_start = FALSE; - g_mutex_unlock (&self->lock); - - g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT, - gst_clapper_play_internal, self, NULL); -} - -static gboolean -gst_clapper_pause_internal (gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - GstStateChangeReturn state_ret; - - GST_DEBUG_OBJECT (self, "Pause"); - - g_mutex_lock (&self->lock); - if (!self->uri) { - g_mutex_unlock (&self->lock); - return G_SOURCE_REMOVE; - } - g_mutex_unlock (&self->lock); - - tick_cb (self); - remove_tick_source (self); - - self->target_state = GST_STATE_PAUSED; - - if (self->current_state < GST_STATE_PAUSED) - change_state (self, GST_CLAPPER_STATE_BUFFERING); - - state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED); - if (state_ret == GST_STATE_CHANGE_FAILURE) { - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to pause")); - return G_SOURCE_REMOVE; - } else if (state_ret == GST_STATE_CHANGE_NO_PREROLL) { - self->is_live = TRUE; - GST_DEBUG_OBJECT (self, "Pipeline is live"); - } - - return G_SOURCE_REMOVE; -} - -/** - * gst_clapper_pause: - * @clapper: #GstClapper instance - * - * Pauses the current stream. - */ -void -gst_clapper_pause (GstClapper * self) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (self->app_state == GST_CLAPPER_STATE_STOPPED) { - GST_DEBUG_OBJECT (self, "Player stopped, pause request ignored"); - return; - } - - if (G_UNLIKELY (self->cached_duration <= GST_SECOND)) { - GST_DEBUG_OBJECT (self, "Cannot pause on this stream"); - return; - } - - g_mutex_lock (&self->lock); - self->inhibit_sigs = FALSE; - g_mutex_unlock (&self->lock); - - g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT, - gst_clapper_pause_internal, self, NULL); -} - -/** - * gst_clapper_toggle_play: - * @clapper: #GstClapper instance - * - * Toggle between play and pause on the loaded stream. - * This function does nothing if player is stopped. - */ -void -gst_clapper_toggle_play (GstClapper * self) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (self->app_state == GST_CLAPPER_STATE_PLAYING) - gst_clapper_pause (self); - else - gst_clapper_play (self); -} - -static void -gst_clapper_stop_internal (GstClapper * self, gboolean transient) -{ - /* directly return if we're already stopped */ - if (self->current_state <= GST_STATE_READY && - self->target_state <= GST_STATE_READY) - return; - - GST_DEBUG_OBJECT (self, "Stop (transient %d)", transient); - - tick_cb (self); - remove_tick_source (self); - - self->target_state = GST_STATE_NULL; - self->current_state = GST_STATE_NULL; - self->is_live = FALSE; - gst_bus_set_flushing (self->bus, TRUE); - gst_element_set_state (self->playbin, GST_STATE_NULL); - gst_bus_set_flushing (self->bus, FALSE); - change_state (self, transient && self->app_state != GST_CLAPPER_STATE_STOPPED - ? GST_CLAPPER_STATE_BUFFERING : GST_CLAPPER_STATE_STOPPED); - self->buffering = 100; - self->cached_duration = GST_CLOCK_TIME_NONE; - g_mutex_lock (&self->lock); - if (self->media_info) { - g_object_unref (self->media_info); - self->media_info = NULL; - } - if (self->global_tags) { - gst_tag_list_unref (self->global_tags); - self->global_tags = NULL; - } - if (self->global_toc) { - gst_toc_unref (self->global_toc); - self->global_toc = NULL; - } - self->seek_pending = FALSE; - remove_seek_source (self); - self->seek_position = GST_CLOCK_TIME_NONE; - self->last_seek_time = GST_CLOCK_TIME_NONE; - if (self->collection) { - if (self->stream_notify_id) - g_signal_handler_disconnect (self->collection, self->stream_notify_id); - self->stream_notify_id = 0; - gst_object_unref (self->collection); - self->collection = NULL; - } - g_free (self->video_sid); - g_free (self->audio_sid); - g_free (self->subtitle_sid); - self->video_sid = NULL; - self->audio_sid = NULL; - self->subtitle_sid = NULL; - g_mutex_unlock (&self->lock); -} - -static gboolean -gst_clapper_stop_internal_dispatch (gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - gst_clapper_stop_internal (self, FALSE); - - return G_SOURCE_REMOVE; -} - - -/** - * gst_clapper_stop: - * @clapper: #GstClapper instance - * - * Stops playing the current stream and resets to the first position - * in the stream. - */ -void -gst_clapper_stop (GstClapper * self) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_mutex_lock (&self->lock); - self->inhibit_sigs = TRUE; - g_mutex_unlock (&self->lock); - - g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT, - gst_clapper_stop_internal_dispatch, self, NULL); -} - -/* Must be called with lock from main context, releases lock! */ -static void -gst_clapper_seek_internal_locked (GstClapper * self) -{ - gboolean ret; - GstClockTime position; - gdouble rate; - GstStateChangeReturn state_ret; - GstEvent *s_event; - GstClapperSeekMode seek_mode; - GstSeekFlags flags = 0; - - remove_seek_source (self); - - /* Only seek in PAUSED */ - if (self->current_state < GST_STATE_PAUSED) { - return; - } else if (self->current_state != GST_STATE_PAUSED) { - g_mutex_unlock (&self->lock); - state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED); - if (state_ret == GST_STATE_CHANGE_FAILURE) { - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to seek")); - } - g_mutex_lock (&self->lock); - return; - } - - self->last_seek_time = gst_util_get_timestamp (); - position = self->seek_position; - self->seek_position = GST_CLOCK_TIME_NONE; - self->seek_pending = TRUE; - rate = self->rate; - seek_mode = self->seek_mode; - g_mutex_unlock (&self->lock); - - remove_tick_source (self); - - flags |= GST_SEEK_FLAG_FLUSH; - - switch (seek_mode) { - case GST_CLAPPER_SEEK_MODE_ACCURATE: - flags |= GST_SEEK_FLAG_ACCURATE; - break; - case GST_CLAPPER_SEEK_MODE_FAST: - flags |= GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_SNAP_NEAREST; - break; - default: - break; - } - - if (rate != 1.0) - flags |= GST_SEEK_FLAG_TRICKMODE; - - if (rate >= 0.0) { - s_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, - GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE); - } else { - s_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, - GST_SEEK_TYPE_SET, G_GINT64_CONSTANT (0), GST_SEEK_TYPE_SET, position); - } - - GST_DEBUG_OBJECT (self, "Seek with rate %.2lf to %" GST_TIME_FORMAT, - rate, GST_TIME_ARGS (position)); - - ret = gst_element_send_event (self->playbin, s_event); - if (!ret) - emit_error (self, g_error_new (GST_CLAPPER_ERROR, GST_CLAPPER_ERROR_FAILED, - "Failed to seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (position))); - - g_mutex_lock (&self->lock); -} - -static gboolean -gst_clapper_seek_internal (gpointer user_data) -{ - GstClapper *self = GST_CLAPPER (user_data); - - g_mutex_lock (&self->lock); - gst_clapper_seek_internal_locked (self); - g_mutex_unlock (&self->lock); - - return G_SOURCE_REMOVE; -} - -/** - * gst_clapper_set_rate: - * @clapper: #GstClapper instance - * @rate: playback rate - * - * Playback at specified rate - */ -void -gst_clapper_set_rate (GstClapper * self, gdouble rate) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - g_return_if_fail (rate != 0.0); - - g_object_set (self, "rate", rate, NULL); -} - -/** - * gst_clapper_get_rate: - * @clapper: #GstClapper instance - * - * Returns: current playback rate - */ -gdouble -gst_clapper_get_rate (GstClapper * self) -{ - gdouble val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_RATE); - - g_object_get (self, "rate", &val, NULL); - - return val; -} - -/** - * gst_clapper_seek: - * @clapper: #GstClapper instance - * @position: position to seek in nanoseconds - * - * Seeks the currently-playing stream to the absolute @position time - * in nanoseconds. - */ -void -gst_clapper_seek (GstClapper * self, GstClockTime position) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - g_return_if_fail (GST_CLOCK_TIME_IS_VALID (position)); - - g_mutex_lock (&self->lock); - if (self->media_info && !self->media_info->seekable) { - GST_DEBUG_OBJECT (self, "Media is not seekable"); - g_mutex_unlock (&self->lock); - return; - } - - self->seek_position = position; - - /* If there is no seek being dispatch to the main context currently do that, - * otherwise we just updated the seek position so that it will be taken by - * the seek handler from the main context instead of the old one. - */ - if (!self->seek_source) { - GstClockTime now = gst_util_get_timestamp (); - - /* If no seek is pending or it was started more than 250 mseconds ago seek - * immediately, otherwise wait until the 250 mseconds have passed */ - if (!self->seek_pending || (now - self->last_seek_time > 250 * GST_MSECOND)) { - self->seek_source = g_idle_source_new (); - g_source_set_callback (self->seek_source, - (GSourceFunc) gst_clapper_seek_internal, self, NULL); - GST_TRACE_OBJECT (self, "Dispatching seek to position %" GST_TIME_FORMAT, - GST_TIME_ARGS (position)); - g_source_attach (self->seek_source, self->context); - } else { - guint delay = 250000 - (now - self->last_seek_time) / 1000; - - /* Note that last_seek_time must be set to something at this point and - * it must be smaller than 250 mseconds */ - self->seek_source = g_timeout_source_new (delay); - g_source_set_callback (self->seek_source, - (GSourceFunc) gst_clapper_seek_internal, self, NULL); - - GST_TRACE_OBJECT (self, - "Delaying seek to position %" GST_TIME_FORMAT " by %u us", - GST_TIME_ARGS (position), delay); - g_source_attach (self->seek_source, self->context); - } - } - g_mutex_unlock (&self->lock); -} - -/** - * gst_clapper_seek_offset: - * @clapper: #GstClapper instance - * @offset: offset from current position to seek to in nanoseconds - * - * Seeks the currently-playing stream to the @offset time - * in nanoseconds. - */ -void -gst_clapper_seek_offset (GstClapper * self, GstClockTime offset) -{ - GstClockTime position; - - g_return_if_fail (GST_IS_CLAPPER (self)); - g_return_if_fail (GST_CLOCK_TIME_IS_VALID (offset)); - - position = gst_clapper_get_position (self); - - /* TODO: Prevent negative values */ - - gst_clapper_seek (self, position + offset); -} - -static void -remove_seek_source (GstClapper * self) -{ - if (!self->seek_source) - return; - - g_source_destroy (self->seek_source); - g_source_unref (self->seek_source); - self->seek_source = NULL; -} - -/** - * gst_clapper_get_state: - * @clapper: #GstClapper instance - * - * Returns: Current player state - */ -GstClapperState -gst_clapper_get_state (GstClapper * self) -{ - GstClapperState state; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_STATE); - - g_object_get (self, "state", &state, NULL); - - return state; -} - -/** - * gst_clapper_get_uri: - * @clapper: #GstClapper instance - * - * Gets the URI of the currently-playing stream. - * - * Returns: (transfer full): a string containing the URI of the - * currently-playing stream. g_free() after usage. - */ -gchar * -gst_clapper_get_uri (GstClapper * self) -{ - gchar *val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_URI); - - g_object_get (self, "uri", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_uri: - * @clapper: #GstClapper instance - * @uri: next URI to play. - * - * Sets the next URI to play. - */ -void -gst_clapper_set_uri (GstClapper * self, const gchar * val) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "uri", val, NULL); -} - -/** - * gst_clapper_set_subtitle_uri: - * @clapper: #GstClapper instance - * @uri: subtitle URI - * - * Sets the external subtitle URI. This should be combined with a call to - * gst_clapper_set_subtitle_track_enabled(@clapper, TRUE) so the subtitles are actually - * rendered. - */ -void -gst_clapper_set_subtitle_uri (GstClapper * self, const gchar * suburi) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "suburi", suburi, NULL); -} - -/** - * gst_clapper_get_subtitle_uri: - * @clapper: #GstClapper instance - * - * current subtitle URI - * - * Returns: (transfer full): URI of the current external subtitle. - * g_free() after usage. - */ -gchar * -gst_clapper_get_subtitle_uri (GstClapper * self) -{ - gchar *val = NULL; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - g_object_get (self, "suburi", &val, NULL); - - return val; -} - -/** - * gst_clapper_get_position: - * @clapper: #GstClapper instance - * - * Returns: the absolute position time, in nanoseconds, of the - * currently-playing stream. - */ -GstClockTime -gst_clapper_get_position (GstClapper * self) -{ - GstClockTime val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_POSITION); - - g_object_get (self, "position", &val, NULL); - - return val; -} - -/** - * gst_clapper_get_duration: - * @clapper: #GstClapper instance - * - * Retrieves the duration of the media stream that self represents. - * - * Returns: the duration of the currently-playing media stream, in - * nanoseconds. - */ -GstClockTime -gst_clapper_get_duration (GstClapper * self) -{ - GstClockTime val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_DURATION); - - g_object_get (self, "duration", &val, NULL); - - return val; -} - -/** - * gst_clapper_get_volume: - * @clapper: #GstClapper instance - * - * Returns the current volume level, as a percentage between 0 and 1.5 - * - * Returns: the cubic volume as percentage between 0 and 1.5 - */ -gdouble -gst_clapper_get_volume (GstClapper * self) -{ - gdouble val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_VOLUME); - - g_object_get (self, "volume", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_volume: - * @clapper: #GstClapper instance - * @val: the new volume level, as a percentage between 0 and 1.5 - * - * Sets the volume level of the stream as a percentage between 0 and 1.5 - * Volume operates on a cubic scale. - */ -void -gst_clapper_set_volume (GstClapper * self, gdouble val) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "volume", val, NULL); -} - -/** - * gst_clapper_get_mute: - * @clapper: #GstClapper instance - * - * Returns: %TRUE if the currently-playing stream is muted. - */ -gboolean -gst_clapper_get_mute (GstClapper * self) -{ - gboolean val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_MUTE); - - g_object_get (self, "mute", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_mute: - * @clapper: #GstClapper instance - * @val: Mute state the should be set - * - * %TRUE if the currently-playing stream should be muted. - */ -void -gst_clapper_set_mute (GstClapper * self, gboolean val) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "mute", val, NULL); -} - -/** - * gst_clapper_get_pipeline: - * @clapper: #GstClapper instance - * - * Returns: (transfer full): The internal playbin instance. - * - * The caller should free it with g_object_unref() - */ -GstElement * -gst_clapper_get_pipeline (GstClapper * self) -{ - GstElement *val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - g_object_get (self, "pipeline", &val, NULL); - - return val; -} - -/** - * gst_clapper_get_mpris: - * @clapper: #GstClapper instance - * - * A Function to get the #GstClapperMpris instance. - * - * Returns: (transfer full): mpris instance. - * - * The caller should free it with g_object_unref() - */ -GstClapperMpris * -gst_clapper_get_mpris (GstClapper * self) -{ - GstClapperMpris *val; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - g_object_get (self, "mpris", &val, NULL); - - return val; -} - -/** - * gst_clapper_get_media_info: - * @clapper: #GstClapper instance - * - * A Function to get the current media info #GstClapperMediaInfo instance. - * - * Returns: (transfer full): media info instance. - * - * The caller should free it with g_object_unref() - */ -GstClapperMediaInfo * -gst_clapper_get_media_info (GstClapper * self) -{ - GstClapperMediaInfo *info; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - if (!self->media_info) - return NULL; - - g_mutex_lock (&self->lock); - info = gst_clapper_media_info_copy (self->media_info); - g_mutex_unlock (&self->lock); - - return info; -} - -/** - * gst_clapper_get_current_audio_track: - * @clapper: #GstClapper instance - * - * A Function to get current audio #GstClapperAudioInfo instance. - * - * Returns: (transfer full): current audio track. - * - * The caller should free it with g_object_unref() - */ -GstClapperAudioInfo * -gst_clapper_get_current_audio_track (GstClapper * self) -{ - GstClapperAudioInfo *info; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - if (!clapper_get_has_flag (self, GST_PLAY_FLAG_AUDIO)) - return NULL; - - if (self->use_playbin3) { - info = (GstClapperAudioInfo *) - gst_clapper_stream_info_get_current_from_stream_id (self, - self->audio_sid, GST_TYPE_CLAPPER_AUDIO_INFO); - } else { - info = (GstClapperAudioInfo *) gst_clapper_stream_info_get_current (self, - "current-audio", GST_TYPE_CLAPPER_AUDIO_INFO); - } - - return info; -} - -/** - * gst_clapper_get_current_video_track: - * @clapper: #GstClapper instance - * - * A Function to get current video #GstClapperVideoInfo instance. - * - * Returns: (transfer full): current video track. - * - * The caller should free it with g_object_unref() - */ -GstClapperVideoInfo * -gst_clapper_get_current_video_track (GstClapper * self) -{ - GstClapperVideoInfo *info; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - if (!clapper_get_has_flag (self, GST_PLAY_FLAG_VIDEO)) - return NULL; - - if (self->use_playbin3) { - info = (GstClapperVideoInfo *) - gst_clapper_stream_info_get_current_from_stream_id (self, - self->video_sid, GST_TYPE_CLAPPER_VIDEO_INFO); - } else { - info = (GstClapperVideoInfo *) gst_clapper_stream_info_get_current (self, - "current-video", GST_TYPE_CLAPPER_VIDEO_INFO); - } - - return info; -} - -/** - * gst_clapper_get_current_subtitle_track: - * @clapper: #GstClapper instance - * - * A Function to get current subtitle #GstClapperSubtitleInfo instance. - * - * Returns: (transfer full): current subtitle track. - * - * The caller should free it with g_object_unref() - */ -GstClapperSubtitleInfo * -gst_clapper_get_current_subtitle_track (GstClapper * self) -{ - GstClapperSubtitleInfo *info; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - if (!clapper_get_has_flag (self, GST_PLAY_FLAG_SUBTITLE)) - return NULL; - - if (self->use_playbin3) { - info = (GstClapperSubtitleInfo *) - gst_clapper_stream_info_get_current_from_stream_id (self, - self->subtitle_sid, GST_TYPE_CLAPPER_SUBTITLE_INFO); - } else { - info = (GstClapperSubtitleInfo *) gst_clapper_stream_info_get_current (self, - "current-text", GST_TYPE_CLAPPER_SUBTITLE_INFO); - } - - return info; -} - -/* Must be called with lock */ -static gboolean -gst_clapper_select_streams (GstClapper * self) -{ - GList *stream_list = NULL; - gboolean ret = FALSE; - - if (self->audio_sid) - stream_list = g_list_append (stream_list, g_strdup (self->audio_sid)); - if (self->video_sid) - stream_list = g_list_append (stream_list, g_strdup (self->video_sid)); - if (self->subtitle_sid) - stream_list = g_list_append (stream_list, g_strdup (self->subtitle_sid)); - - g_mutex_unlock (&self->lock); - if (stream_list) { - ret = gst_element_send_event (self->playbin, - gst_event_new_select_streams (stream_list)); - g_list_free_full (stream_list, g_free); - } else { - GST_ERROR_OBJECT (self, "No available streams for select-streams"); - } - g_mutex_lock (&self->lock); - - return ret; -} - -/** - * gst_clapper_set_audio_track: - * @clapper: #GstClapper instance - * @stream_index: stream index - * - * Returns: %TRUE or %FALSE - * - * Sets the audio track @stream_idex. - */ -gboolean -gst_clapper_set_audio_track (GstClapper * self, gint stream_index) -{ - GstClapperStreamInfo *info; - gboolean ret = TRUE; - - g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE); - - g_mutex_lock (&self->lock); - info = gst_clapper_stream_info_find (self->media_info, - GST_TYPE_CLAPPER_AUDIO_INFO, stream_index); - g_mutex_unlock (&self->lock); - if (!info) { - GST_ERROR_OBJECT (self, "invalid audio stream index %d", stream_index); - return FALSE; - } - - if (self->use_playbin3) { - g_mutex_lock (&self->lock); - g_free (self->audio_sid); - self->audio_sid = g_strdup (info->stream_id); - ret = gst_clapper_select_streams (self); - g_mutex_unlock (&self->lock); - } else { - g_object_set (G_OBJECT (self->playbin), "current-audio", stream_index, - NULL); - } - - GST_DEBUG_OBJECT (self, "set stream index '%d'", stream_index); - return ret; -} - -/** - * gst_clapper_set_video_track: - * @clapper: #GstClapper instance - * @stream_index: stream index - * - * Returns: %TRUE or %FALSE - * - * Sets the video track @stream_index. - */ -gboolean -gst_clapper_set_video_track (GstClapper * self, gint stream_index) -{ - GstClapperStreamInfo *info; - gboolean ret = TRUE; - - g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE); - - /* check if stream_index exist in our internal media_info list */ - g_mutex_lock (&self->lock); - info = gst_clapper_stream_info_find (self->media_info, - GST_TYPE_CLAPPER_VIDEO_INFO, stream_index); - g_mutex_unlock (&self->lock); - if (!info) { - GST_ERROR_OBJECT (self, "invalid video stream index %d", stream_index); - return FALSE; - } - - if (self->use_playbin3) { - g_mutex_lock (&self->lock); - g_free (self->video_sid); - self->video_sid = g_strdup (info->stream_id); - ret = gst_clapper_select_streams (self); - g_mutex_unlock (&self->lock); - } else { - g_object_set (G_OBJECT (self->playbin), "current-video", stream_index, - NULL); - } - - GST_DEBUG_OBJECT (self, "set stream index '%d'", stream_index); - return ret; -} - -/** - * gst_clapper_set_subtitle_track: - * @clapper: #GstClapper instance - * @stream_index: stream index - * - * Returns: %TRUE or %FALSE - * - * Sets the subtitle stack @stream_index. - */ -gboolean -gst_clapper_set_subtitle_track (GstClapper * self, gint stream_index) -{ - GstClapperStreamInfo *info; - gboolean ret = TRUE; - - g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE); - - g_mutex_lock (&self->lock); - info = gst_clapper_stream_info_find (self->media_info, - GST_TYPE_CLAPPER_SUBTITLE_INFO, stream_index); - g_mutex_unlock (&self->lock); - if (!info) { - GST_ERROR_OBJECT (self, "invalid subtitle stream index %d", stream_index); - return FALSE; - } - - if (self->use_playbin3) { - g_mutex_lock (&self->lock); - g_free (self->subtitle_sid); - self->subtitle_sid = g_strdup (info->stream_id); - ret = gst_clapper_select_streams (self); - g_mutex_unlock (&self->lock); - } else { - g_object_set (G_OBJECT (self->playbin), "current-text", stream_index, NULL); - } - - GST_DEBUG_OBJECT (self, "set stream index '%d'", stream_index); - return ret; -} - -/** - * gst_clapper_set_audio_track_enabled: - * @clapper: #GstClapper instance - * @enabled: TRUE or FALSE - * - * Enable or disable the current audio track. - */ -void -gst_clapper_set_audio_track_enabled (GstClapper * self, gboolean enabled) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (enabled) - clapper_set_flag (self, GST_PLAY_FLAG_AUDIO); - else - clapper_clear_flag (self, GST_PLAY_FLAG_AUDIO); - - GST_DEBUG_OBJECT (self, "track is '%s'", enabled ? "Enabled" : "Disabled"); -} - -/** - * gst_clapper_set_video_track_enabled: - * @clapper: #GstClapper instance - * @enabled: TRUE or FALSE - * - * Enable or disable the current video track. - */ -void -gst_clapper_set_video_track_enabled (GstClapper * self, gboolean enabled) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (enabled) - clapper_set_flag (self, GST_PLAY_FLAG_VIDEO); - else - clapper_clear_flag (self, GST_PLAY_FLAG_VIDEO); - - GST_DEBUG_OBJECT (self, "track is '%s'", enabled ? "Enabled" : "Disabled"); -} - -/** - * gst_clapper_set_subtitle_track_enabled: - * @clapper: #GstClapper instance - * @enabled: TRUE or FALSE - * - * Enable or disable the current subtitle track. - */ -void -gst_clapper_set_subtitle_track_enabled (GstClapper * self, gboolean enabled) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (enabled) - clapper_set_flag (self, GST_PLAY_FLAG_SUBTITLE); - else - clapper_clear_flag (self, GST_PLAY_FLAG_SUBTITLE); - - GST_DEBUG_OBJECT (self, "track is '%s'", enabled ? "Enabled" : "Disabled"); -} - -/** - * gst_clapper_set_visualization: - * @clapper: #GstClapper instance - * @name: visualization element obtained from - * #gst_clapper_visualizations_get() - * - * Returns: %TRUE if the visualizations was set correctly. Otherwise, - * %FALSE. - */ -gboolean -gst_clapper_set_visualization (GstClapper * self, const gchar * name) -{ - g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE); - - g_mutex_lock (&self->lock); - if (self->current_vis_element) { - gst_object_unref (self->current_vis_element); - self->current_vis_element = NULL; - } - - if (name) { - self->current_vis_element = gst_element_factory_make (name, NULL); - if (!self->current_vis_element) - goto error_no_element; - gst_object_ref_sink (self->current_vis_element); - } - g_object_set (self->playbin, "vis-plugin", self->current_vis_element, NULL); - - g_mutex_unlock (&self->lock); - GST_DEBUG_OBJECT (self, "set vis-plugin to '%s'", name); - - return TRUE; - -error_no_element: - g_mutex_unlock (&self->lock); - GST_WARNING_OBJECT (self, "could not find visualization '%s'", name); - return FALSE; -} - -/** - * gst_clapper_get_current_visualization: - * @clapper: #GstClapper instance - * - * Returns: (transfer full): Name of the currently enabled visualization. - * g_free() after usage. - */ -gchar * -gst_clapper_get_current_visualization (GstClapper * self) -{ - gchar *name = NULL; - GstElement *vis_plugin = NULL; - - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - if (!clapper_get_has_flag (self, GST_PLAY_FLAG_VIS)) - return NULL; - - g_object_get (self->playbin, "vis-plugin", &vis_plugin, NULL); - - if (vis_plugin) { - GstElementFactory *factory = gst_element_get_factory (vis_plugin); - if (factory) - name = g_strdup (gst_plugin_feature_get_name (factory)); - gst_object_unref (vis_plugin); - } - - GST_DEBUG_OBJECT (self, "vis-plugin '%s' %p", name, vis_plugin); - - return name; -} - -/** - * gst_clapper_set_visualization_enabled: - * @clapper: #GstClapper instance - * @enabled: TRUE or FALSE - * - * Enable or disable the visualization. - */ -void -gst_clapper_set_visualization_enabled (GstClapper * self, gboolean enabled) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - if (enabled) - clapper_set_flag (self, GST_PLAY_FLAG_VIS); - else - clapper_clear_flag (self, GST_PLAY_FLAG_VIS); - - GST_DEBUG_OBJECT (self, "visualization is '%s'", - enabled ? "Enabled" : "Disabled"); -} - -struct CBChannelMap -{ - const gchar *label; /* channel label name */ - const gchar *name; /* get_name () */ -}; - -static const struct CBChannelMap cb_channel_map[] = { - /* GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS */ {"BRIGHTNESS", "brightness"}, - /* GST_CLAPPER_COLOR_BALANCE_CONTRAST */ {"CONTRAST", "contrast"}, - /* GST_CLAPPER_COLOR_BALANCE_SATURATION */ {"SATURATION", "saturation"}, - /* GST_CLAPPER_COLOR_BALANCE_HUE */ {"HUE", "hue"}, -}; - -static GstColorBalanceChannel * -gst_clapper_color_balance_find_channel (GstClapper * self, - GstClapperColorBalanceType type) -{ - GstColorBalanceChannel *channel; - const GList *l, *channels; - - if (type < GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS || - type > GST_CLAPPER_COLOR_BALANCE_HUE) - return NULL; - - channels = - gst_color_balance_list_channels (GST_COLOR_BALANCE (self->playbin)); - for (l = channels; l; l = l->next) { - channel = l->data; - if (g_strrstr (channel->label, cb_channel_map[type].label)) - return channel; - } - - return NULL; -} - -/** - * gst_clapper_has_color_balance: - * @clapper:#GstClapper instance - * - * Checks whether the @clapper has color balance support available. - * - * Returns: %TRUE if @clapper has color balance support. Otherwise, - * %FALSE. - */ -gboolean -gst_clapper_has_color_balance (GstClapper * self) -{ - const GList *channels; - - g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE); - - if (!GST_IS_COLOR_BALANCE (self->playbin)) - return FALSE; - - channels = - gst_color_balance_list_channels (GST_COLOR_BALANCE (self->playbin)); - return (channels != NULL); -} - -/** - * gst_clapper_set_color_balance: - * @clapper: #GstClapper instance - * @type: #GstClapperColorBalanceType - * @value: The new value for the @type, ranged [0,1] - * - * Sets the current value of the indicated channel @type to the passed - * value. - */ -void -gst_clapper_set_color_balance (GstClapper * self, GstClapperColorBalanceType type, - gdouble value) -{ - GstColorBalanceChannel *channel; - gdouble new_val; - - g_return_if_fail (GST_IS_CLAPPER (self)); - g_return_if_fail (value >= 0.0 && value <= 1.0); - - if (!GST_IS_COLOR_BALANCE (self->playbin)) - return; - - channel = gst_clapper_color_balance_find_channel (self, type); - if (!channel) - return; - - value = CLAMP (value, 0.0, 1.0); - - /* Convert to channel range */ - new_val = channel->min_value + value * ((gdouble) channel->max_value - - (gdouble) channel->min_value); - - gst_color_balance_set_value (GST_COLOR_BALANCE (self->playbin), channel, - new_val); -} - -/** - * gst_clapper_get_color_balance: - * @clapper: #GstClapper instance - * @type: #GstClapperColorBalanceType - * - * Retrieve the current value of the indicated @type. - * - * Returns: The current value of @type, between [0,1]. In case of - * error -1 is returned. - */ -gdouble -gst_clapper_get_color_balance (GstClapper * self, GstClapperColorBalanceType type) -{ - GstColorBalanceChannel *channel; - gint value; - - g_return_val_if_fail (GST_IS_CLAPPER (self), -1); - - if (!GST_IS_COLOR_BALANCE (self->playbin)) - return -1; - - channel = gst_clapper_color_balance_find_channel (self, type); - if (!channel) - return -1; - - value = gst_color_balance_get_value (GST_COLOR_BALANCE (self->playbin), - channel); - - return ((gdouble) value - - (gdouble) channel->min_value) / ((gdouble) channel->max_value - - (gdouble) channel->min_value); -} - -/** - * gst_clapper_get_multiview_mode: - * @clapper: #GstClapper instance - * - * Retrieve the current value of the indicated @type. - * - * Returns: The current value of @type, Default: -1 "none" - */ -GstVideoMultiviewFramePacking -gst_clapper_get_multiview_mode (GstClapper * self) -{ - GstVideoMultiviewFramePacking val = GST_VIDEO_MULTIVIEW_FRAME_PACKING_NONE; - - g_return_val_if_fail (GST_IS_CLAPPER (self), - GST_VIDEO_MULTIVIEW_FRAME_PACKING_NONE); - - g_object_get (self, "video-multiview-mode", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_multiview_mode: - * @clapper: #GstClapper instance - * @mode: The new value for the @type - * - * Sets the current value of the indicated mode @type to the passed - * value. - */ -void -gst_clapper_set_multiview_mode (GstClapper * self, - GstVideoMultiviewFramePacking mode) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "video-multiview-mode", mode, NULL); -} - -/** - * gst_clapper_get_multiview_flags: - * @clapper: #GstClapper instance - * - * Retrieve the current value of the indicated @type. - * - * Returns: The current value of @type, Default: 0x00000000 "none - */ -GstVideoMultiviewFlags -gst_clapper_get_multiview_flags (GstClapper * self) -{ - GstVideoMultiviewFlags val = GST_VIDEO_MULTIVIEW_FLAGS_NONE; - - g_return_val_if_fail (GST_IS_CLAPPER (self), val); - - g_object_get (self, "video-multiview-flags", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_multiview_flags: - * @clapper: #GstClapper instance - * @flags: The new value for the @type - * - * Sets the current value of the indicated mode @type to the passed - * value. - */ -void -gst_clapper_set_multiview_flags (GstClapper * self, GstVideoMultiviewFlags flags) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "video-multiview-flags", flags, NULL); -} - -/** - * gst_clapper_get_audio_video_offset: - * @clapper: #GstClapper instance - * - * Retrieve the current value of audio-video-offset property - * - * Returns: The current value of audio-video-offset in nanoseconds - */ -gint64 -gst_clapper_get_audio_video_offset (GstClapper * self) -{ - gint64 val = 0; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_AUDIO_VIDEO_OFFSET); - - g_object_get (self, "audio-video-offset", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_audio_video_offset: - * @clapper: #GstClapper instance - * @offset: #gint64 in nanoseconds - * - * Sets audio-video-offset property by value of @offset - */ -void -gst_clapper_set_audio_video_offset (GstClapper * self, gint64 offset) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "audio-video-offset", offset, NULL); -} - -/** - * gst_clapper_get_subtitle_video_offset: - * @clapper: #GstClapper instance - * - * Retrieve the current value of subtitle-video-offset property - * - * Returns: The current value of subtitle-video-offset in nanoseconds - */ -gint64 -gst_clapper_get_subtitle_video_offset (GstClapper * self) -{ - gint64 val = 0; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_SUBTITLE_VIDEO_OFFSET); - - g_object_get (self, "subtitle-video-offset", &val, NULL); - - return val; -} - -/** - * gst_clapper_set_subtitle_video_offset: - * @clapper: #GstClapper instance - * @offset: #gint64 in nanoseconds - * - * Sets subtitle-video-offset property by value of @offset - */ -void -gst_clapper_set_subtitle_video_offset (GstClapper * self, gint64 offset) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "subtitle-video-offset", offset, NULL); -} - - -#define C_ENUM(v) ((gint) v) -#define C_FLAGS(v) ((guint) v) - -GType -gst_clapper_color_balance_type_get_type (void) -{ - static gsize id = 0; - static const GEnumValue values[] = { - {C_ENUM (GST_CLAPPER_COLOR_BALANCE_HUE), "GST_CLAPPER_COLOR_BALANCE_HUE", - "hue"}, - {C_ENUM (GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS), - "GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS", "brightness"}, - {C_ENUM (GST_CLAPPER_COLOR_BALANCE_SATURATION), - "GST_CLAPPER_COLOR_BALANCE_SATURATION", "saturation"}, - {C_ENUM (GST_CLAPPER_COLOR_BALANCE_CONTRAST), - "GST_CLAPPER_COLOR_BALANCE_CONTRAST", "contrast"}, - {0, NULL, NULL} - }; - - if (g_once_init_enter (&id)) { - GType tmp = g_enum_register_static ("GstClapperColorBalanceType", values); - g_once_init_leave (&id, tmp); - } - - return (GType) id; -} - -/** - * gst_clapper_color_balance_type_get_name: - * @type: a #GstClapperColorBalanceType - * - * Gets a string representing the given color balance type. - * - * Returns: (transfer none): a string with the name of the color - * balance type. - */ -const gchar * -gst_clapper_color_balance_type_get_name (GstClapperColorBalanceType type) -{ - g_return_val_if_fail (type >= GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS && - type <= GST_CLAPPER_COLOR_BALANCE_HUE, NULL); - - return cb_channel_map[type].name; -} - -GType -gst_clapper_state_get_type (void) -{ - static gsize id = 0; - static const GEnumValue values[] = { - {C_ENUM (GST_CLAPPER_STATE_STOPPED), "GST_CLAPPER_STATE_STOPPED", "stopped"}, - {C_ENUM (GST_CLAPPER_STATE_BUFFERING), "GST_CLAPPER_STATE_BUFFERING", - "buffering"}, - {C_ENUM (GST_CLAPPER_STATE_PAUSED), "GST_CLAPPER_STATE_PAUSED", "paused"}, - {C_ENUM (GST_CLAPPER_STATE_PLAYING), "GST_CLAPPER_STATE_PLAYING", "playing"}, - {0, NULL, NULL} - }; - - if (g_once_init_enter (&id)) { - GType tmp = g_enum_register_static ("GstClapperState", values); - g_once_init_leave (&id, tmp); - } - - return (GType) id; -} - -/** - * gst_clapper_state_get_name: - * @state: a #GstClapperState - * - * Gets a string representing the given state. - * - * Returns: (transfer none): a string with the name of the state. - */ -const gchar * -gst_clapper_state_get_name (GstClapperState state) -{ - switch (state) { - case GST_CLAPPER_STATE_STOPPED: - return "stopped"; - case GST_CLAPPER_STATE_BUFFERING: - return "buffering"; - case GST_CLAPPER_STATE_PAUSED: - return "paused"; - case GST_CLAPPER_STATE_PLAYING: - return "playing"; - } - - g_assert_not_reached (); - return NULL; -} - -GType -gst_clapper_error_get_type (void) -{ - static gsize id = 0; - static const GEnumValue values[] = { - {C_ENUM (GST_CLAPPER_ERROR_FAILED), "GST_CLAPPER_ERROR_FAILED", "failed"}, - {0, NULL, NULL} - }; - - if (g_once_init_enter (&id)) { - GType tmp = g_enum_register_static ("GstClapperError", values); - g_once_init_leave (&id, tmp); - } - - return (GType) id; -} - -/** - * gst_clapper_error_get_name: - * @error: a #GstClapperError - * - * Gets a string representing the given error. - * - * Returns: (transfer none): a string with the given error. - */ -const gchar * -gst_clapper_error_get_name (GstClapperError error) -{ - switch (error) { - case GST_CLAPPER_ERROR_FAILED: - return "failed"; - } - - g_assert_not_reached (); - return NULL; -} - -GType -gst_clapper_seek_mode_get_type (void) -{ - static gsize id = 0; - static const GEnumValue values[] = { - {C_ENUM (GST_CLAPPER_SEEK_MODE_DEFAULT), "GST_CLAPPER_SEEK_MODE_DEFAULT", - "default"}, - {C_ENUM (GST_CLAPPER_SEEK_MODE_ACCURATE), "GST_CLAPPER_SEEK_MODE_ACCURATE", - "accurate"}, - {C_ENUM (GST_CLAPPER_SEEK_MODE_FAST), "GST_CLAPPER_SEEK_MODE_FAST", "fast"}, - {0, NULL, NULL} - }; - - if (g_once_init_enter (&id)) { - GType tmp = g_enum_register_static ("GstClapperSeekMode", values); - g_once_init_leave (&id, tmp); - } - - return (GType) id; -} - -/** - * gst_clapper_get_seek_mode: - * @clapper: #GstClapper instance - * - * Returns: The currently used seek mode, Default: 0 "default" - */ -GstClapperSeekMode -gst_clapper_get_seek_mode (GstClapper * self) -{ - GstClapperSeekMode mode; - - g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_SEEK_MODE); - - g_object_get (self, "seek-mode", &mode, NULL); - - return mode; -} - -/** - * gst_clapper_set_seek_mode: - * @clapper: #GstClapper instance - * @mode: #GstClapperSeekMode - * - * Changes currently used clapper seek mode to the one of @mode - */ -void -gst_clapper_set_seek_mode (GstClapper * self, GstClapperSeekMode mode) -{ - g_return_if_fail (GST_IS_CLAPPER (self)); - - g_object_set (self, "seek-mode", mode, NULL); -} - -/** - * gst_clapper_get_video_snapshot: - * @clapper: #GstClapper instance - * @format: output format of the video snapshot - * @config: (allow-none): Additional configuration - * - * Get a snapshot of the currently selected video stream, if any. The format can be - * selected with @format and optional configuration is possible with @config - * Currently supported settings are: - * - width, height of type G_TYPE_INT - * - pixel-aspect-ratio of type GST_TYPE_FRACTION - * Except for GST_CLAPPER_THUMBNAIL_RAW_NATIVE format, if no config is set, pixel-aspect-ratio would be 1/1 - * - * Returns: (transfer full): Current video snapshot sample or %NULL on failure - */ -GstSample * -gst_clapper_get_video_snapshot (GstClapper * self, - GstClapperSnapshotFormat format, const GstStructure * config) -{ - gint video_tracks = 0; - GstSample *sample = NULL; - GstCaps *caps = NULL; - gint width = -1; - gint height = -1; - gint par_n = 1; - gint par_d = 1; - g_return_val_if_fail (GST_IS_CLAPPER (self), NULL); - - g_object_get (self->playbin, "n-video", &video_tracks, NULL); - if (video_tracks == 0) { - GST_DEBUG_OBJECT (self, "total video track num is 0"); - return NULL; - } - - switch (format) { - case GST_CLAPPER_THUMBNAIL_RAW_xRGB: - caps = gst_caps_new_simple ("video/x-raw", - "format", G_TYPE_STRING, "xRGB", NULL); - break; - case GST_CLAPPER_THUMBNAIL_RAW_BGRx: - caps = gst_caps_new_simple ("video/x-raw", - "format", G_TYPE_STRING, "BGRx", NULL); - break; - case GST_CLAPPER_THUMBNAIL_JPG: - caps = gst_caps_new_empty_simple ("image/jpeg"); - break; - case GST_CLAPPER_THUMBNAIL_PNG: - caps = gst_caps_new_empty_simple ("image/png"); - break; - case GST_CLAPPER_THUMBNAIL_RAW_NATIVE: - default: - caps = gst_caps_new_empty_simple ("video/x-raw"); - break; - } - - if (NULL != config) { - if (!gst_structure_get_int (config, "width", &width)) - width = -1; - if (!gst_structure_get_int (config, "height", &height)) - height = -1; - if (!gst_structure_get_fraction (config, "pixel-aspect-ratio", &par_n, - &par_d)) { - if (format != GST_CLAPPER_THUMBNAIL_RAW_NATIVE) { - par_n = 1; - par_d = 1; - } else { - par_n = 0; - par_d = 0; - } - } - } - - if (width > 0 && height > 0) { - gst_caps_set_simple (caps, "width", G_TYPE_INT, width, - "height", G_TYPE_INT, height, NULL); - } - - if (format != GST_CLAPPER_THUMBNAIL_RAW_NATIVE) { - gst_caps_set_simple (caps, "pixel-aspect-ratio", GST_TYPE_FRACTION, - par_n, par_d, NULL); - } else if (NULL != config && par_n != 0 && par_d != 0) { - gst_caps_set_simple (caps, "pixel-aspect-ratio", GST_TYPE_FRACTION, - par_n, par_d, NULL); - } - - g_signal_emit_by_name (self->playbin, "convert-sample", caps, &sample); - gst_caps_unref (caps); - if (!sample) { - GST_WARNING_OBJECT (self, "Failed to retrieve or convert video frame"); - return NULL; - } - - return sample; -} diff --git a/lib/gst/clapper/gstclapper.h b/lib/gst/clapper/gstclapper.h deleted file mode 100644 index d93cdf31..00000000 --- a/lib/gst/clapper/gstclapper.h +++ /dev/null @@ -1,318 +0,0 @@ -/* GStreamer - * - * Copyright (C) 2014-2015 Sebastian Dröge - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_H__ -#define __GST_CLAPPER_H__ - -#include -#include - -#include -#include -#include -#include -#include -#include - -G_BEGIN_DECLS - -/* ClapperState */ -GST_CLAPPER_API -GType gst_clapper_state_get_type (void); -#define GST_TYPE_CLAPPER_STATE (gst_clapper_state_get_type ()) - -/** - * GstClapperState: - * @GST_CLAPPER_STATE_STOPPED: clapper is stopped. - * @GST_CLAPPER_STATE_BUFFERING: clapper is buffering. - * @GST_CLAPPER_STATE_PAUSED: clapper is paused. - * @GST_CLAPPER_STATE_PLAYING: clapper is currently playing a stream. - */ -typedef enum -{ - GST_CLAPPER_STATE_STOPPED, - GST_CLAPPER_STATE_BUFFERING, - GST_CLAPPER_STATE_PAUSED, - GST_CLAPPER_STATE_PLAYING -} GstClapperState; - -GST_CLAPPER_API -const gchar * gst_clapper_state_get_name (GstClapperState state); - -/* ClapperSeekMode */ -GST_CLAPPER_API -GType gst_clapper_seek_mode_get_type (void); -#define GST_TYPE_CLAPPER_SEEK_MODE (gst_clapper_seek_mode_get_type ()) - -/** - * GstClapperSeekMode: - * @GST_CLAPPER_SEEK_MODE_DEFAULT: default seek method (flush only). - * @GST_CLAPPER_SEEK_MODE_ACCURATE: accurate seek method. - * @GST_CLAPPER_SEEK_MODE_FAST: fast seek method (next keyframe). - */ -typedef enum -{ - GST_CLAPPER_SEEK_MODE_DEFAULT, - GST_CLAPPER_SEEK_MODE_ACCURATE, - GST_CLAPPER_SEEK_MODE_FAST, -} GstClapperSeekMode; - -/* ClapperError */ -GST_CLAPPER_API -GQuark gst_clapper_error_quark (void); - -GST_CLAPPER_API -GType gst_clapper_error_get_type (void); -#define GST_CLAPPER_ERROR (gst_clapper_error_quark ()) -#define GST_TYPE_CLAPPER_ERROR (gst_clapper_error_get_type ()) - -/** - * GstClapperError: - * @GST_CLAPPER_ERROR_FAILED: generic error. - */ -typedef enum { - GST_CLAPPER_ERROR_FAILED = 0 -} GstClapperError; - -GST_CLAPPER_API -const gchar * gst_clapper_error_get_name (GstClapperError error); - -/* ClapperColorBalanceType */ -GST_CLAPPER_API -GType gst_clapper_color_balance_type_get_type (void); -#define GST_TYPE_CLAPPER_COLOR_BALANCE_TYPE (gst_clapper_color_balance_type_get_type ()) - -/** - * GstClapperColorBalanceType: - * @GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS: brightness or black level. - * @GST_CLAPPER_COLOR_BALANCE_CONTRAST: contrast or luma gain. - * @GST_CLAPPER_COLOR_BALANCE_SATURATION: color saturation or chroma - * gain. - * @GST_CLAPPER_COLOR_BALANCE_HUE: hue or color balance. - */ -typedef enum -{ - GST_CLAPPER_COLOR_BALANCE_BRIGHTNESS, - GST_CLAPPER_COLOR_BALANCE_CONTRAST, - GST_CLAPPER_COLOR_BALANCE_SATURATION, - GST_CLAPPER_COLOR_BALANCE_HUE, -} GstClapperColorBalanceType; - -GST_CLAPPER_API -const gchar * gst_clapper_color_balance_type_get_name (GstClapperColorBalanceType type); - -/* ClapperSnapshotFormat */ - -/** - * GstClapperSnapshotFormat: - * @GST_CLAPPER_THUMBNAIL_RAW_NATIVE: RAW Native. - * @GST_CLAPPER_THUMBNAIL_RAW_xRGB: RAW xRGB. - * @GST_CLAPPER_THUMBNAIL_RAW_BGRx: RAW BGRx. - * @GST_CLAPPER_THUMBNAIL_JPG: JPG. - * @GST_CLAPPER_THUMBNAIL_PNG: PNG. - */ -typedef enum -{ - GST_CLAPPER_THUMBNAIL_RAW_NATIVE = 0, - GST_CLAPPER_THUMBNAIL_RAW_xRGB, - GST_CLAPPER_THUMBNAIL_RAW_BGRx, - GST_CLAPPER_THUMBNAIL_JPG, - GST_CLAPPER_THUMBNAIL_PNG -} GstClapperSnapshotFormat; - -#define GST_TYPE_CLAPPER (gst_clapper_get_type ()) -#define GST_IS_CLAPPER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER)) -#define GST_IS_CLAPPER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER)) -#define GST_CLAPPER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER, GstClapperClass)) -#define GST_CLAPPER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER, GstClapper)) -#define GST_CLAPPER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER, GstClapperClass)) -#define GST_CLAPPER_CAST(obj) ((GstClapper*)(obj)) - -#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC -G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstClapper, gst_object_unref) -#endif - -GST_CLAPPER_API -GType gst_clapper_get_type (void); - -GST_CLAPPER_API -void gst_clapper_gst_init (int *argc, char **argv[]); - -GST_CLAPPER_API -GstClapper * gst_clapper_new (GstClapperVideoRenderer *video_renderer, GstClapperSignalDispatcher *signal_dispatcher, - GstClapperMpris *mpris); - -GST_CLAPPER_API -void gst_clapper_play (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_pause (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_toggle_play (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_stop (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_seek (GstClapper *clapper, GstClockTime position); - -GST_CLAPPER_API -void gst_clapper_seek_offset (GstClapper *clapper, GstClockTime offset); - -GST_CLAPPER_API -GstClapperState - gst_clapper_get_state (GstClapper *clapper); - -GST_CLAPPER_API -GstClapperSeekMode - gst_clapper_get_seek_mode (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_seek_mode (GstClapper *clapper, GstClapperSeekMode mode); - -GST_CLAPPER_API -void gst_clapper_set_rate (GstClapper *clapper, gdouble rate); - -GST_CLAPPER_API -gdouble gst_clapper_get_rate (GstClapper *clapper); - -GST_CLAPPER_API -gchar * gst_clapper_get_uri (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_uri (GstClapper *clapper, const gchar *uri); - -GST_CLAPPER_API -gchar * gst_clapper_get_subtitle_uri (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_subtitle_uri (GstClapper *clapper, const gchar *uri); - -GST_CLAPPER_API -GstClockTime gst_clapper_get_position (GstClapper *clapper); - -GST_CLAPPER_API -GstClockTime gst_clapper_get_duration (GstClapper *clapper); - -GST_CLAPPER_API -gdouble gst_clapper_get_volume (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_volume (GstClapper *clapper, gdouble val); - -GST_CLAPPER_API -gboolean gst_clapper_get_mute (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_mute (GstClapper *clapper, gboolean val); - -GST_CLAPPER_API -GstElement * gst_clapper_get_pipeline (GstClapper *clapper); - -GST_CLAPPER_API -GstClapperMpris * - gst_clapper_get_mpris (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_video_track_enabled (GstClapper *clapper, gboolean enabled); - -GST_CLAPPER_API -void gst_clapper_set_audio_track_enabled (GstClapper *clapper, gboolean enabled); - -GST_CLAPPER_API -void gst_clapper_set_subtitle_track_enabled (GstClapper *clapper, gboolean enabled); - -GST_CLAPPER_API -gboolean gst_clapper_set_audio_track (GstClapper *clapper, gint stream_index); - -GST_CLAPPER_API -gboolean gst_clapper_set_video_track (GstClapper *clapper, gint stream_index); - -GST_CLAPPER_API -gboolean gst_clapper_set_subtitle_track (GstClapper *clapper, gint stream_index); - -GST_CLAPPER_API -GstClapperMediaInfo * - gst_clapper_get_media_info (GstClapper *clapper); - -GST_CLAPPER_API -GstClapperAudioInfo * - gst_clapper_get_current_audio_track (GstClapper *clapper); - -GST_CLAPPER_API -GstClapperVideoInfo * - gst_clapper_get_current_video_track (GstClapper *clapper); - -GST_CLAPPER_API -GstClapperSubtitleInfo * - gst_clapper_get_current_subtitle_track (GstClapper *clapper); - -GST_CLAPPER_API -gboolean gst_clapper_set_visualization (GstClapper *clapper, const gchar *name); - -GST_CLAPPER_API -void gst_clapper_set_visualization_enabled (GstClapper *clapper, gboolean enabled); - -GST_CLAPPER_API -gchar * gst_clapper_get_current_visualization (GstClapper *clapper); - -GST_CLAPPER_API -gboolean gst_clapper_has_color_balance (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_color_balance (GstClapper *clapper, GstClapperColorBalanceType type, gdouble value); - -GST_CLAPPER_API -gdouble gst_clapper_get_color_balance (GstClapper *clapper, GstClapperColorBalanceType type); - -GST_CLAPPER_API -GstVideoMultiviewFramePacking - gst_clapper_get_multiview_mode (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_multiview_mode (GstClapper *clapper, GstVideoMultiviewFramePacking mode); - -GST_CLAPPER_API -GstVideoMultiviewFlags - gst_clapper_get_multiview_flags (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_multiview_flags (GstClapper *clapper, GstVideoMultiviewFlags flags); - -GST_CLAPPER_API -gint64 gst_clapper_get_audio_video_offset (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_audio_video_offset (GstClapper *clapper, gint64 offset); - -GST_CLAPPER_API -gint64 gst_clapper_get_subtitle_video_offset (GstClapper *clapper); - -GST_CLAPPER_API -void gst_clapper_set_subtitle_video_offset (GstClapper *clapper, gint64 offset); - -GST_CLAPPER_API -GstSample * gst_clapper_get_video_snapshot (GstClapper *clapper, GstClapperSnapshotFormat format, const GstStructure *config); - -G_END_DECLS - -#endif /* __GST_CLAPPER_H__ */ diff --git a/lib/gst/clapper/gtk4/gstclapperglsink.c b/lib/gst/clapper/gtk4/gstclapperglsink.c deleted file mode 100644 index 14a99ecd..00000000 --- a/lib/gst/clapper/gtk4/gstclapperglsink.c +++ /dev/null @@ -1,759 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2020 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/** - * SECTION:gstclapperglsink - * @title: GstClapperGLSink - * - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include - -#include "gstclapperglsink.h" -#include "gstgtkutils.h" - -GST_DEBUG_CATEGORY (gst_debug_clapper_gl_sink); -#define GST_CAT_DEFAULT gst_debug_clapper_gl_sink - -#define GST_CLAPPER_GL_SINK_CAPS \ - "video/x-raw(" GST_CAPS_FEATURE_MEMORY_GL_MEMORY "), " \ - "format = (string) RGBA, " \ - "width = " GST_VIDEO_SIZE_RANGE ", " \ - "height = " GST_VIDEO_SIZE_RANGE ", " \ - "framerate = " GST_VIDEO_FPS_RANGE ", " \ - "texture-target = (string) { 2D, external-oes } " \ - " ; " \ - "video/x-raw(" GST_CAPS_FEATURE_MEMORY_GL_MEMORY "," \ - GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION "), " \ - "format = (string) RGBA, " \ - "width = " GST_VIDEO_SIZE_RANGE ", " \ - "height = " GST_VIDEO_SIZE_RANGE ", " \ - "framerate = " GST_VIDEO_FPS_RANGE ", " \ - "texture-target = (string) { 2D, external-oes } " - -static GstStaticPadTemplate gst_clapper_gl_sink_template = - GST_STATIC_PAD_TEMPLATE ("sink", - GST_PAD_SINK, - GST_PAD_ALWAYS, - GST_STATIC_CAPS (GST_CLAPPER_GL_SINK_CAPS)); - -static void gst_clapper_gl_sink_finalize (GObject * object); -static void gst_clapper_gl_sink_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * param_spec); -static void gst_clapper_gl_sink_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * param_spec); - -static gboolean gst_clapper_gl_sink_propose_allocation (GstBaseSink * bsink, - GstQuery * query); -static gboolean gst_clapper_gl_sink_query (GstBaseSink * bsink, GstQuery * query); -static gboolean gst_clapper_gl_sink_start (GstBaseSink * bsink); -static gboolean gst_clapper_gl_sink_stop (GstBaseSink * bsink); -static GstFlowReturn gst_clapper_gl_sink_wait_event (GstBaseSink * bsink, GstEvent * event); - -static GstStateChangeReturn -gst_clapper_gl_sink_change_state (GstElement * element, - GstStateChange transition); - -static void gst_clapper_gl_sink_get_times (GstBaseSink * bsink, GstBuffer * buf, - GstClockTime * start, GstClockTime * end); -static GstCaps *gst_clapper_gl_sink_get_caps (GstBaseSink * bsink, - GstCaps * filter); -static gboolean gst_clapper_gl_sink_set_caps (GstBaseSink * bsink, - GstCaps * caps); -static GstFlowReturn gst_clapper_gl_sink_show_frame (GstVideoSink * bsink, - GstBuffer * buf); - -static void -gst_clapper_gl_sink_navigation_interface_init (GstNavigationInterface * iface); - -#define gst_clapper_gl_sink_parent_class parent_class -G_DEFINE_TYPE_WITH_CODE (GstClapperGLSink, gst_clapper_gl_sink, - GST_TYPE_VIDEO_SINK, - G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION, - gst_clapper_gl_sink_navigation_interface_init); - GST_DEBUG_CATEGORY_INIT (gst_debug_clapper_gl_sink, - "clapperglsink", 0, "Clapper GL Sink")); - -static void -gst_clapper_gl_sink_class_init (GstClapperGLSinkClass * klass) -{ - GObjectClass *gobject_class; - GstElementClass *gstelement_class; - GstBaseSinkClass *gstbasesink_class; - GstVideoSinkClass *gstvideosink_class; - GstClapperGLSinkClass *gstclapperglsink_class; - - gobject_class = (GObjectClass *) klass; - gstelement_class = (GstElementClass *) klass; - gstbasesink_class = (GstBaseSinkClass *) klass; - gstvideosink_class = (GstVideoSinkClass *) klass; - gstclapperglsink_class = (GstClapperGLSinkClass *) klass; - - gobject_class->set_property = gst_clapper_gl_sink_set_property; - gobject_class->get_property = gst_clapper_gl_sink_get_property; - gobject_class->finalize = gst_clapper_gl_sink_finalize; - - g_object_class_install_property (gobject_class, PROP_WIDGET, - g_param_spec_object ("widget", "GTK Widget", - "The GtkWidget to place in the widget hierarchy " - "(must only be get from the GTK main thread)", - GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); - - gst_gtk_install_shared_properties (gobject_class); - - gstelement_class->change_state = gst_clapper_gl_sink_change_state; - - gstbasesink_class->get_caps = gst_clapper_gl_sink_get_caps; - gstbasesink_class->set_caps = gst_clapper_gl_sink_set_caps; - gstbasesink_class->get_times = gst_clapper_gl_sink_get_times; - gstbasesink_class->propose_allocation = gst_clapper_gl_sink_propose_allocation; - gstbasesink_class->query = gst_clapper_gl_sink_query; - gstbasesink_class->start = gst_clapper_gl_sink_start; - gstbasesink_class->stop = gst_clapper_gl_sink_stop; - gstbasesink_class->wait_event = gst_clapper_gl_sink_wait_event; - - gstvideosink_class->show_frame = gst_clapper_gl_sink_show_frame; - - gstclapperglsink_class->create_widget = gtk_clapper_gl_widget_new; - gstclapperglsink_class->window_title = "GTK4 GL Renderer"; - - gst_element_class_set_metadata (gstelement_class, - "GTK4 GL Video Sink", - "Sink/Video", "A video sink that renders to a GtkWidget using OpenGL", - "Matthew Waters , " - "Rafał Dzięgiel "); - - gst_element_class_add_static_pad_template (gstelement_class, - &gst_clapper_gl_sink_template); - - gst_type_mark_as_plugin_api (GST_TYPE_CLAPPER_GL_SINK, 0); -} - -static void -gst_clapper_gl_sink_init (GstClapperGLSink * clapper_sink) -{ - clapper_sink->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO; - clapper_sink->par_n = DEFAULT_PAR_N; - clapper_sink->par_d = DEFAULT_PAR_D; - clapper_sink->keep_last_frame = DEFAULT_KEEP_LAST_FRAME; - - clapper_sink->had_eos = FALSE; -} - -static void -gst_clapper_gl_sink_finalize (GObject * object) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (object); - - GST_DEBUG ("Finalizing Clapper GL sink"); - - GST_OBJECT_LOCK (clapper_sink); - if (clapper_sink->window && clapper_sink->window_destroy_id) - g_signal_handler_disconnect (clapper_sink->window, clapper_sink->window_destroy_id); - if (clapper_sink->widget && clapper_sink->widget_destroy_id) - g_signal_handler_disconnect (clapper_sink->widget, clapper_sink->widget_destroy_id); - - g_clear_object (&clapper_sink->widget); - GST_OBJECT_UNLOCK (clapper_sink); - - G_OBJECT_CLASS (parent_class)->finalize (object); -} - -static void -widget_destroy_cb (GtkWidget * widget, GstClapperGLSink * clapper_sink) -{ - GST_OBJECT_LOCK (clapper_sink); - g_clear_object (&clapper_sink->widget); - GST_OBJECT_UNLOCK (clapper_sink); -} - -static void -window_destroy_cb (GtkWidget * widget, GstClapperGLSink * clapper_sink) -{ - GST_OBJECT_LOCK (clapper_sink); - if (clapper_sink->widget) { - if (clapper_sink->widget_destroy_id) { - g_signal_handler_disconnect (clapper_sink->widget, - clapper_sink->widget_destroy_id); - clapper_sink->widget_destroy_id = 0; - } - g_clear_object (&clapper_sink->widget); - } - clapper_sink->window = NULL; - GST_OBJECT_UNLOCK (clapper_sink); -} - -static GtkClapperGLWidget * -gst_clapper_gl_sink_get_widget (GstClapperGLSink * clapper_sink) -{ - if (clapper_sink->widget != NULL) - return clapper_sink->widget; - - /* Ensure GTK is initialized, this has no side effect if it was already - * initialized. Also, we do that lazily, so the application can be first */ - if (!gtk_init_check ()) { - GST_ERROR_OBJECT (clapper_sink, "Could not ensure GTK initialization."); - return NULL; - } - - g_assert (GST_CLAPPER_GL_SINK_GET_CLASS (clapper_sink)->create_widget); - clapper_sink->widget = (GtkClapperGLWidget *) - GST_CLAPPER_GL_SINK_GET_CLASS (clapper_sink)->create_widget (); - - g_object_bind_property (clapper_sink, "force-aspect-ratio", clapper_sink->widget, - "force-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); - g_object_bind_property (clapper_sink, "pixel-aspect-ratio", clapper_sink->widget, - "pixel-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); - g_object_bind_property (clapper_sink, "keep-last-frame", clapper_sink->widget, - "keep-last-frame", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); - - /* Take the floating ref, other wise the destruction of the container will - * make this widget disappear possibly before we are done. */ - gst_object_ref_sink (clapper_sink->widget); - - clapper_sink->widget_destroy_id = g_signal_connect (clapper_sink->widget, "destroy", - G_CALLBACK (widget_destroy_cb), clapper_sink); - - /* back pointer */ - gtk_clapper_gl_widget_set_element (GTK_CLAPPER_GL_WIDGET (clapper_sink->widget), - GST_ELEMENT (clapper_sink)); - - return clapper_sink->widget; -} - -static void -gst_clapper_gl_sink_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (object); - - switch (prop_id) { - case PROP_WIDGET: - { - GObject *widget = NULL; - - GST_OBJECT_LOCK (clapper_sink); - if (clapper_sink->widget != NULL) - widget = G_OBJECT (clapper_sink->widget); - GST_OBJECT_UNLOCK (clapper_sink); - - if (!widget) - widget = - gst_gtk_invoke_on_main ((GThreadFunc) gst_clapper_gl_sink_get_widget, - clapper_sink); - - g_value_set_object (value, widget); - break; - } - case PROP_FORCE_ASPECT_RATIO: - g_value_set_boolean (value, clapper_sink->force_aspect_ratio); - break; - case PROP_PIXEL_ASPECT_RATIO: - gst_value_set_fraction (value, clapper_sink->par_n, clapper_sink->par_d); - break; - case PROP_KEEP_LAST_FRAME: - g_value_set_boolean (value, clapper_sink->keep_last_frame); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_gl_sink_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (object); - - switch (prop_id) { - case PROP_FORCE_ASPECT_RATIO: - clapper_sink->force_aspect_ratio = g_value_get_boolean (value); - break; - case PROP_PIXEL_ASPECT_RATIO: - clapper_sink->par_n = gst_value_get_fraction_numerator (value); - clapper_sink->par_d = gst_value_get_fraction_denominator (value); - break; - case PROP_KEEP_LAST_FRAME: - clapper_sink->keep_last_frame = g_value_get_boolean (value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gst_clapper_gl_sink_navigation_send_event (GstNavigation * navigation, - GstStructure * structure) -{ - GstClapperGLSink *sink = GST_CLAPPER_GL_SINK (navigation); - GstEvent *event; - GstPad *pad; - - event = gst_event_new_navigation (structure); - pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink)); - - GST_TRACE_OBJECT (sink, "navigation event %" GST_PTR_FORMAT, structure); - - if (GST_IS_PAD (pad) && GST_IS_EVENT (event)) { - if (!gst_pad_send_event (pad, gst_event_ref (event))) { - /* If upstream didn't handle the event we'll post a message with it - * for the application in case it wants to do something with it */ - gst_element_post_message (GST_ELEMENT_CAST (sink), - gst_navigation_message_new_event (GST_OBJECT_CAST (sink), event)); - } - gst_event_unref (event); - gst_object_unref (pad); - } -} - -static void -gst_clapper_gl_sink_navigation_interface_init (GstNavigationInterface * iface) -{ - iface->send_event = gst_clapper_gl_sink_navigation_send_event; -} - -static gboolean -gst_clapper_gl_sink_propose_allocation (GstBaseSink * bsink, GstQuery * query) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - GstBufferPool *pool = NULL; - GstStructure *config; - GstCaps *caps; - GstVideoInfo info; - guint size; - gboolean need_pool; - GstStructure *allocation_meta = NULL; - gint display_width, display_height; - - if (!clapper_sink->display || !clapper_sink->context) - return FALSE; - - gst_query_parse_allocation (query, &caps, &need_pool); - - if (caps == NULL) - goto no_caps; - - if (!gst_video_info_from_caps (&info, caps)) - goto invalid_caps; - - /* the normal size of a frame */ - size = info.size; - - if (need_pool) { - GST_DEBUG_OBJECT (clapper_sink, "create new pool"); - pool = gst_gl_buffer_pool_new (clapper_sink->context); - - config = gst_buffer_pool_get_config (pool); - gst_buffer_pool_config_set_params (config, caps, size, 0, 0); - gst_buffer_pool_config_add_option (config, - GST_BUFFER_POOL_OPTION_GL_SYNC_META); - - if (!gst_buffer_pool_set_config (pool, config)) { - gst_object_unref (pool); - goto config_failed; - } - } - - /* we need at least 2 buffer because we hold on to the last one */ - gst_query_add_allocation_pool (query, pool, size, 2, 0); - if (pool) - gst_object_unref (pool); - - GST_OBJECT_LOCK (clapper_sink); - display_width = clapper_sink->display_width; - display_height = clapper_sink->display_height; - GST_OBJECT_UNLOCK (clapper_sink); - - if (display_width != 0 && display_height != 0) { - GST_DEBUG_OBJECT (clapper_sink, "sending alloc query with size %dx%d", - display_width, display_height); - allocation_meta = gst_structure_new ("GstVideoOverlayCompositionMeta", - "width", G_TYPE_UINT, display_width, - "height", G_TYPE_UINT, display_height, NULL); - } - - gst_query_add_allocation_meta (query, - GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, allocation_meta); - - if (allocation_meta) - gst_structure_free (allocation_meta); - - /* we also support various metadata */ - gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, 0); - - if (clapper_sink->context->gl_vtable->FenceSync) - gst_query_add_allocation_meta (query, GST_GL_SYNC_META_API_TYPE, 0); - - return TRUE; - - /* ERRORS */ -no_caps: - { - GST_DEBUG_OBJECT (bsink, "no caps specified"); - return FALSE; - } -invalid_caps: - { - GST_DEBUG_OBJECT (bsink, "invalid caps specified"); - return FALSE; - } -config_failed: - { - GST_DEBUG_OBJECT (bsink, "failed setting config"); - return FALSE; - } -} - -static gboolean -gst_clapper_gl_sink_query (GstBaseSink * bsink, GstQuery * query) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - gboolean res = FALSE; - - switch (GST_QUERY_TYPE (query)) { - case GST_QUERY_CONTEXT: - res = gst_gl_handle_context_query ((GstElement *) clapper_sink, query, - clapper_sink->display, clapper_sink->context, clapper_sink->gtk_context); - break; - default: - res = GST_BASE_SINK_CLASS (parent_class)->query (bsink, query); - break; - } - - return res; -} - -static gboolean -gst_clapper_gl_sink_start_on_main (GstBaseSink * bsink) -{ - GstClapperGLSink *gst_sink = GST_CLAPPER_GL_SINK (bsink); - GstClapperGLSinkClass *klass = GST_CLAPPER_GL_SINK_GET_CLASS (bsink); - GtkWidget *toplevel; - GtkRoot *root; - - if (gst_clapper_gl_sink_get_widget (gst_sink) == NULL) - return FALSE; - - /* After this point, clapper_sink->widget will always be set */ - - root = gtk_widget_get_root (GTK_WIDGET (gst_sink->widget)); - if (!GTK_IS_ROOT (root)) { - GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (gst_sink->widget)); - if (parent) { - GtkWidget *temp_parent; - while ((temp_parent = gtk_widget_get_parent (parent))) - parent = temp_parent; - } - toplevel = (parent) ? parent : GTK_WIDGET (gst_sink->widget); - - /* sanity check */ - g_assert (klass->window_title); - - /* User did not add widget its own UI, let's popup a new GtkWindow to - * make gst-launch-1.0 work. */ - gst_sink->window = gtk_window_new (); - gtk_window_set_default_size (GTK_WINDOW (gst_sink->window), 640, 480); - gtk_window_set_title (GTK_WINDOW (gst_sink->window), klass->window_title); - gtk_window_set_child (GTK_WINDOW (gst_sink->window), toplevel); - - gst_sink->window_destroy_id = g_signal_connect ( - GTK_WINDOW (gst_sink->window), - "destroy", G_CALLBACK (window_destroy_cb), gst_sink); - } - - return TRUE; -} - -static gboolean -gst_clapper_gl_sink_start (GstBaseSink * bsink) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - GtkClapperGLWidget *clapper_widget; - - if (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) - gst_clapper_gl_sink_start_on_main, bsink))) - return FALSE; - - /* After this point, clapper_sink->widget will always be set */ - clapper_widget = GTK_CLAPPER_GL_WIDGET (clapper_sink->widget); - - if (!gtk_clapper_gl_widget_init_winsys (clapper_widget)) { - GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s", - "Failed to initialize OpenGL with GTK"), (NULL)); - return FALSE; - } - - if (!clapper_sink->display) - clapper_sink->display = gtk_clapper_gl_widget_get_display (clapper_widget); - if (!clapper_sink->context) - clapper_sink->context = gtk_clapper_gl_widget_get_context (clapper_widget); - if (!clapper_sink->gtk_context) - clapper_sink->gtk_context = gtk_clapper_gl_widget_get_gtk_context (clapper_widget); - - if (!clapper_sink->display || !clapper_sink->context || !clapper_sink->gtk_context) { - GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s", - "Failed to retrieve OpenGL context from GTK"), (NULL)); - return FALSE; - } - - gst_gl_element_propagate_display_context (GST_ELEMENT (bsink), - clapper_sink->display); - - return TRUE; -} - -static gboolean -gst_clapper_gl_sink_stop_on_main (GstBaseSink * bsink) -{ - GstClapperGLSink *gst_sink = GST_CLAPPER_GL_SINK (bsink); - - if (gst_sink->window) { - gtk_window_destroy (GTK_WINDOW (gst_sink->window)); - gst_sink->window = NULL; - gst_sink->widget = NULL; - } - - return TRUE; -} - -static gboolean -gst_clapper_gl_sink_stop (GstBaseSink * bsink) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - - if (clapper_sink->display) { - gst_object_unref (clapper_sink->display); - clapper_sink->display = NULL; - } - if (clapper_sink->context) { - gst_object_unref (clapper_sink->context); - clapper_sink->context = NULL; - } - if (clapper_sink->gtk_context) { - gst_object_unref (clapper_sink->gtk_context); - clapper_sink->gtk_context = NULL; - } - if (clapper_sink->window) - return ! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) - gst_clapper_gl_sink_stop_on_main, bsink); - - return TRUE; -} - -static void -gst_gtk_window_show_all_and_unref (GtkWidget * window) -{ - gtk_window_present (GTK_WINDOW (window)); - g_object_unref (window); -} - -static GstStateChangeReturn -gst_clapper_gl_sink_change_state (GstElement * element, GstStateChange transition) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (element); - GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; - - GST_DEBUG_OBJECT (element, "changing state: %s => %s", - gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)), - gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition))); - - ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); - if (ret == GST_STATE_CHANGE_FAILURE) - return ret; - - switch (transition) { - case GST_STATE_CHANGE_NULL_TO_READY: - GST_OBJECT_LOCK (clapper_sink); - clapper_sink->had_eos = FALSE; - if (clapper_sink->widget) { - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_sink->widget); - clapper_sink->widget->ignore_buffers = FALSE; - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_sink->widget); - } - GST_OBJECT_UNLOCK (clapper_sink); - break; - case GST_STATE_CHANGE_READY_TO_PAUSED:{ - GtkWindow *window = NULL; - - GST_OBJECT_LOCK (clapper_sink); - if (clapper_sink->window) - window = g_object_ref (GTK_WINDOW (clapper_sink->window)); - GST_OBJECT_UNLOCK (clapper_sink); - - if (window) { - gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) - gst_gtk_window_show_all_and_unref, window); - } - break; - } - case GST_STATE_CHANGE_READY_TO_NULL: - GST_OBJECT_LOCK (clapper_sink); - if (clapper_sink->widget) { - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_sink->widget); - clapper_sink->widget->ignore_buffers = - !clapper_sink->had_eos || !clapper_sink->keep_last_frame; - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_sink->widget); - } - GST_OBJECT_UNLOCK (clapper_sink); - /* Fall through to render black bg */ - case GST_STATE_CHANGE_PAUSED_TO_READY: - GST_OBJECT_LOCK (clapper_sink); - if (clapper_sink->widget) - gtk_clapper_gl_widget_set_buffer (clapper_sink->widget, NULL); - GST_OBJECT_UNLOCK (clapper_sink); - break; - default: - break; - } - - return ret; -} - -static void -gst_clapper_gl_sink_get_times (GstBaseSink * bsink, GstBuffer * buf, - GstClockTime * start, GstClockTime * end) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - - if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) { - *start = GST_BUFFER_TIMESTAMP (buf); - if (GST_BUFFER_DURATION_IS_VALID (buf)) - *end = *start + GST_BUFFER_DURATION (buf); - else { - if (GST_VIDEO_INFO_FPS_N (&clapper_sink->v_info) > 0) { - *end = *start + - gst_util_uint64_scale_int (GST_SECOND, - GST_VIDEO_INFO_FPS_D (&clapper_sink->v_info), - GST_VIDEO_INFO_FPS_N (&clapper_sink->v_info)); - } - } - } -} - -static GstCaps * -gst_clapper_gl_sink_get_caps (GstBaseSink * bsink, GstCaps * filter) -{ - GstCaps *tmp = NULL; - GstCaps *result = NULL; - - tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink)); - - if (filter) { - GST_DEBUG_OBJECT (bsink, "intersecting with filter caps %" GST_PTR_FORMAT, - filter); - - result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST); - gst_caps_unref (tmp); - } else { - result = tmp; - } - - result = gst_gl_overlay_compositor_add_caps (result); - - GST_DEBUG_OBJECT (bsink, "returning caps: %" GST_PTR_FORMAT, result); - - return result; -} - -static gboolean -gst_clapper_gl_sink_set_caps (GstBaseSink * bsink, GstCaps * caps) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - gboolean res = FALSE; - - GST_DEBUG ("set caps with %" GST_PTR_FORMAT, caps); - - if (!gst_video_info_from_caps (&clapper_sink->v_info, caps)) - return FALSE; - - GST_OBJECT_LOCK (clapper_sink); - - if (clapper_sink->widget == NULL) { - GST_OBJECT_UNLOCK (clapper_sink); - GST_ELEMENT_ERROR (clapper_sink, RESOURCE, NOT_FOUND, - ("%s", "Output widget was destroyed"), (NULL)); - return FALSE; - } - - if (!gtk_clapper_gl_widget_set_format (clapper_sink->widget, &clapper_sink->v_info)) { - GST_OBJECT_UNLOCK (clapper_sink); - return FALSE; - } - - res = gtk_clapper_gl_widget_update_output_format (clapper_sink->widget, caps); - GST_OBJECT_UNLOCK (clapper_sink); - - return res; -} - -static GstFlowReturn -gst_clapper_gl_sink_wait_event (GstBaseSink * bsink, GstEvent * event) -{ - GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink); - GstFlowReturn ret; - - ret = GST_BASE_SINK_CLASS (parent_class)->wait_event (bsink, event); - - switch (event->type) { - case GST_EVENT_EOS: - if (ret == GST_FLOW_OK) { - GST_OBJECT_LOCK (clapper_sink); - clapper_sink->had_eos = TRUE; - GST_OBJECT_UNLOCK (clapper_sink); - } - break; - default: - break; - } - - return ret; -} - -static GstFlowReturn -gst_clapper_gl_sink_show_frame (GstVideoSink * vsink, GstBuffer * buf) -{ - GstClapperGLSink *clapper_sink; - - GST_TRACE ("rendering buffer:%p", buf); - - clapper_sink = GST_CLAPPER_GL_SINK (vsink); - - GST_OBJECT_LOCK (clapper_sink); - - if (clapper_sink->widget == NULL) { - GST_OBJECT_UNLOCK (clapper_sink); - GST_ELEMENT_ERROR (clapper_sink, RESOURCE, NOT_FOUND, - ("%s", "Output widget was destroyed"), (NULL)); - return GST_FLOW_ERROR; - } - - gtk_clapper_gl_widget_set_buffer (clapper_sink->widget, buf); - - GST_OBJECT_UNLOCK (clapper_sink); - - return GST_FLOW_OK; -} diff --git a/lib/gst/clapper/gtk4/gstclapperglsink.h b/lib/gst/clapper/gtk4/gstclapperglsink.h deleted file mode 100644 index d277ab7b..00000000 --- a/lib/gst/clapper/gtk4/gstclapperglsink.h +++ /dev/null @@ -1,105 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2020 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_GL_SINK_H__ -#define __GST_CLAPPER_GL_SINK_H__ - -#include -#include -#include -#include -#include - -#include "gtkclapperglwidget.h" - -#define GST_TYPE_CLAPPER_GL_SINK (gst_clapper_gl_sink_get_type()) -#define GST_CLAPPER_GL_SINK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_GL_SINK,GstClapperGLSink)) -#define GST_CLAPPER_GL_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_CLAPPER_GL_SINK,GstClapperGLClass)) -#define GST_CLAPPER_GL_SINK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_GL_SINK, GstClapperGLSinkClass)) -#define GST_IS_CLAPPER_GL_SINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_GL_SINK)) -#define GST_IS_CLAPPER_GL_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_CLAPPER_GL_SINK)) -#define GST_CLAPPER_GL_SINK_CAST(obj) ((GstClapperGLSink*)(obj)) - -G_BEGIN_DECLS -typedef struct _GstClapperGLSink GstClapperGLSink; -typedef struct _GstClapperGLSinkClass GstClapperGLSinkClass; - -GType gst_clapper_gl_sink_get_type (void); - -/** - * GstClapperGLSink: - * - * Opaque #GstClapperGLSink object - */ -struct _GstClapperGLSink -{ - /* */ - GstVideoSink parent; - - GstVideoInfo v_info; - - GtkClapperGLWidget *widget; - - gboolean had_eos; - - /* properties */ - gboolean force_aspect_ratio; - gint par_n, par_d; - gboolean keep_last_frame; - - gboolean ignore_textures; - - GtkWidget *window; - gulong widget_destroy_id; - gulong window_destroy_id; - - GstGLDisplay *display; - GstGLContext *context; - GstGLContext *gtk_context; - - GstGLUpload *upload; - GstBuffer *uploaded_buffer; - - /* read/write with object lock */ - gint display_width, display_height; -}; - -/** - * GstClapperGLSinkClass: - * - * The #GstClapperGLSinkClass struct only contains private data - */ -struct _GstClapperGLSinkClass -{ - GstVideoSinkClass object_class; - - /* metadata */ - const gchar *window_title; - - /* virtuals */ - GtkWidget* (*create_widget) (void); -}; - -G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstClapperGLSink, gst_object_unref) - -G_END_DECLS - -#endif /* __GST_CLAPPER_GL_SINK_H__ */ diff --git a/lib/gst/clapper/gtk4/gstclapperglutils.c b/lib/gst/clapper/gtk4/gstclapperglutils.c deleted file mode 100644 index d74732cb..00000000 --- a/lib/gst/clapper/gtk4/gstclapperglutils.c +++ /dev/null @@ -1,103 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2015 Thibault Saunier - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -/* FIXME: remove these once their headers are public in gstreamer: - * https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/-/merge_requests/804 - */ - -#include "gstclapperglutils.h" - -static const gfloat identity_matrix[] = { - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -}; - -static const gfloat from_ndc_matrix[] = { - 0.5, 0.0, 0.0, 0.0, - 0.0, 0.5, 0.0, 0.0, - 0.0, 0.0, 0.5, 0.0, - 0.5, 0.5, 0.5, 1.0, -}; - -static const gfloat to_ndc_matrix[] = { - 2.0, 0.0, 0.0, 0.0, - 0.0, 2.0, 0.0, 0.0, - 0.0, 0.0, 2.0, 0.0, - -1.0, -1.0, -1.0, 1.0, -}; - -/* multiplies two 4x4 matrices, @a X @b, and stores the result in @result - * https://en.wikipedia.org/wiki/Matrix_multiplication - */ -static void -gst_gl_multiply_matrix4 (const gfloat * a, const gfloat * b, gfloat * result) -{ - int i, j, k; - gfloat tmp[16] = { 0.0f }; - - if (!a || !b || !result) - return; - for (i = 0; i < 4; i++) { /* column */ - for (j = 0; j < 4; j++) { /* row */ - for (k = 0; k < 4; k++) { - tmp[j + (i * 4)] += a[k + (i * 4)] * b[j + (k * 4)]; - } - } - } - - for (i = 0; i < 16; i++) - result[i] = tmp[i]; -} - -/* - * gst_clapper_gl_get_affine_transformation_meta_as_ndc: - * @meta: (nullable): a #GstVideoAffineTransformationMeta - * @matrix: (out): result of the 4x4 matrix - * - * Retrieves the stored 4x4 affine transformation matrix stored in @meta in - * NDC coordinates. if @meta is NULL, an identity matrix is returned. - * - * NDC is a left-handed coordinate system - * - x - [-1, 1] - +ve X moves right - * - y - [-1, 1] - +ve Y moves up - * - z - [-1, 1] - +ve Z moves into - */ -void -gst_clapper_gl_get_affine_transformation_meta_as_ndc (GstVideoAffineTransformationMeta * - meta, gfloat * matrix) -{ - if (!meta) { - int i; - - for (i = 0; i < 16; i++) { - matrix[i] = identity_matrix[i]; - } - } else { - float tmp[16]; - - /* change of basis multiplications */ - gst_gl_multiply_matrix4 (from_ndc_matrix, meta->matrix, tmp); - gst_gl_multiply_matrix4 (tmp, to_ndc_matrix, matrix); - } -} diff --git a/lib/gst/clapper/gtk4/gstclapperglutils.h b/lib/gst/clapper/gtk4/gstclapperglutils.h deleted file mode 100644 index f0d51fa1..00000000 --- a/lib/gst/clapper/gtk4/gstclapperglutils.h +++ /dev/null @@ -1,29 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2016 Matthew Waters - * Copyright (C) 2021 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_CLAPPER_GL_UTILS_H__ -#define __GST_CLAPPER_GL_UTILS_H__ - -#include - -void gst_clapper_gl_get_affine_transformation_meta_as_ndc (GstVideoAffineTransformationMeta *meta, gfloat *matrix); - -#endif /* __GST_CLAPPER_GL_UTILS_H__ */ diff --git a/lib/gst/clapper/gtk4/gstgtkutils.c b/lib/gst/clapper/gtk4/gstgtkutils.c deleted file mode 100644 index 10e9d720..00000000 --- a/lib/gst/clapper/gtk4/gstgtkutils.c +++ /dev/null @@ -1,95 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2015 Thibault Saunier - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#include -#include "gstgtkutils.h" - -struct invoke_context -{ - GThreadFunc func; - gpointer data; - GMutex lock; - GCond cond; - gboolean fired; - - gpointer res; -}; - -static gboolean -gst_gtk_invoke_func (struct invoke_context *info) -{ - g_mutex_lock (&info->lock); - info->res = info->func (info->data); - info->fired = TRUE; - g_cond_signal (&info->cond); - g_mutex_unlock (&info->lock); - - return G_SOURCE_REMOVE; -} - -gpointer -gst_gtk_invoke_on_main (GThreadFunc func, gpointer data) -{ - GMainContext *main_context = g_main_context_default (); - struct invoke_context info; - - g_mutex_init (&info.lock); - g_cond_init (&info.cond); - info.fired = FALSE; - info.func = func; - info.data = data; - - g_main_context_invoke (main_context, (GSourceFunc) gst_gtk_invoke_func, - &info); - - g_mutex_lock (&info.lock); - while (!info.fired) - g_cond_wait (&info.cond, &info.lock); - g_mutex_unlock (&info.lock); - - g_mutex_clear (&info.lock); - g_cond_clear (&info.cond); - - return info.res; -} - -void -gst_gtk_install_shared_properties (GObjectClass *gobject_class) -{ - g_object_class_install_property (gobject_class, PROP_FORCE_ASPECT_RATIO, - g_param_spec_boolean ("force-aspect-ratio", - "Force aspect ratio", - "When enabled, scaling will respect original aspect ratio", - DEFAULT_FORCE_ASPECT_RATIO, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); - - g_object_class_install_property (gobject_class, PROP_PIXEL_ASPECT_RATIO, - gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio", - "The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D, - G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); - - g_object_class_install_property (gobject_class, PROP_KEEP_LAST_FRAME, - g_param_spec_boolean ("keep-last-frame", - "Keep last frame", - "Keep showing last video frame after playback instead of black screen", - DEFAULT_KEEP_LAST_FRAME, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); -} diff --git a/lib/gst/clapper/gtk4/gstgtkutils.h b/lib/gst/clapper/gtk4/gstgtkutils.h deleted file mode 100644 index ced23142..00000000 --- a/lib/gst/clapper/gtk4/gstgtkutils.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2015 Thibault Saunier - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GST_GTK_UTILS_H__ -#define __GST_GTK_UTILS_H__ - -#define DEFAULT_FORCE_ASPECT_RATIO TRUE -#define DEFAULT_PAR_N 0 -#define DEFAULT_PAR_D 1 -#define DEFAULT_KEEP_LAST_FRAME FALSE - -#include -#include - -enum -{ - PROP_0, - PROP_WIDGET, - PROP_FORCE_ASPECT_RATIO, - PROP_PIXEL_ASPECT_RATIO, - PROP_KEEP_LAST_FRAME -}; - -gpointer gst_gtk_invoke_on_main (GThreadFunc func, gpointer data); - -void gst_gtk_install_shared_properties (GObjectClass *gobject_class); - -#endif /* __GST_GTK_UTILS_H__ */ diff --git a/lib/gst/clapper/gtk4/gtkclapperglwidget.c b/lib/gst/clapper/gtk4/gtkclapperglwidget.c deleted file mode 100644 index 5c0a348e..00000000 --- a/lib/gst/clapper/gtk4/gtkclapperglwidget.c +++ /dev/null @@ -1,1248 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2020 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include - -#include "gtkclapperglwidget.h" -#include "gstgtkutils.h" -#include "gstclapperglutils.h" - -#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11) -#include -#if GST_GL_HAVE_PLATFORM_EGL -#include -#endif -#if GST_GL_HAVE_PLATFORM_GLX -#include -#endif -#endif - -#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND) -#include -#include -#endif - -/** - * SECTION:gtkclapperglwidget - * @title: GtkClapperGLWidget - * @short_description: a #GtkGLArea that renders GStreamer video #GstBuffers - * @see_also: #GtkGLArea, #GstBuffer - * - * #GtkClapperGLWidget is a #GtkWidget that renders GStreamer video buffers. - */ - -GST_DEBUG_CATEGORY (gst_debug_clapper_gl_widget); -#define GST_CAT_DEFAULT gst_debug_clapper_gl_widget - -struct _GtkClapperGLWidgetPrivate -{ - gboolean initiated; - - GstGLDisplay *display; - GdkGLContext *gdk_context; - GstGLContext *other_context; - GstGLContext *context; - - GstGLTextureTarget texture_target; - guint gl_target; - - GstGLUpload *upload; - GstGLShader *shader; - - GLuint vao; - GLuint vertex_buffer; - GLint attr_position; - GLint attr_texture; - GLuint current_tex; - - GstGLOverlayCompositor *overlay_compositor; -}; - -static const GLfloat vertices[] = { - 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, - -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, - -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 0.0f, 1.0f, 1.0f -}; - -static const GLushort indices[] = { - 0, 1, 2, 0, 2, 3 -}; - -G_DEFINE_TYPE_WITH_CODE (GtkClapperGLWidget, gtk_clapper_gl_widget, GTK_TYPE_GL_AREA, - G_ADD_PRIVATE (GtkClapperGLWidget) - GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "gtkclapperglwidget", 0, - "GTK Clapper GL Widget")); - -static void -gtk_clapper_gl_widget_get_preferred_width (GtkWidget * widget, gint * min, - gint * natural) -{ - GtkClapperGLWidget *clapper_widget = (GtkClapperGLWidget *) widget; - gint video_width = clapper_widget->display_width; - - if (!clapper_widget->negotiated) - video_width = 10; - - if (min) - *min = 1; - if (natural) - *natural = video_width; -} - -static void -gtk_clapper_gl_widget_get_preferred_height (GtkWidget * widget, gint * min, - gint * natural) -{ - GtkClapperGLWidget *clapper_widget = (GtkClapperGLWidget *) widget; - gint video_height = clapper_widget->display_height; - - if (!clapper_widget->negotiated) - video_height = 10; - - if (min) - *min = 1; - if (natural) - *natural = video_height; -} - -static void -gtk_clapper_gl_widget_measure (GtkWidget * widget, GtkOrientation orientation, - gint for_size, gint * min, gint * natural, - gint * minimum_baseline, gint * natural_baseline) -{ - if (orientation == GTK_ORIENTATION_HORIZONTAL) - gtk_clapper_gl_widget_get_preferred_width (widget, min, natural); - else - gtk_clapper_gl_widget_get_preferred_height (widget, min, natural); - - *minimum_baseline = -1; - *natural_baseline = -1; -} - -static void -gtk_clapper_gl_widget_size_allocate (GtkWidget * widget, - gint width, gint height, gint baseline) -{ - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget); - gint scale_factor = gtk_widget_get_scale_factor (widget); - - clapper_widget->scaled_width = width * scale_factor; - clapper_widget->scaled_height = height * scale_factor; - - gtk_gl_area_queue_render (GTK_GL_AREA (widget)); -} - -static void -gtk_clapper_gl_widget_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec) -{ - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (object); - - switch (prop_id) { - case PROP_FORCE_ASPECT_RATIO: - clapper_widget->force_aspect_ratio = g_value_get_boolean (value); - break; - case PROP_PIXEL_ASPECT_RATIO: - clapper_widget->par_n = gst_value_get_fraction_numerator (value); - clapper_widget->par_d = gst_value_get_fraction_denominator (value); - break; - case PROP_KEEP_LAST_FRAME: - clapper_widget->keep_last_frame = g_value_get_boolean (value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gtk_clapper_gl_widget_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec) -{ - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (object); - - switch (prop_id) { - case PROP_FORCE_ASPECT_RATIO: - g_value_set_boolean (value, clapper_widget->force_aspect_ratio); - break; - case PROP_PIXEL_ASPECT_RATIO: - gst_value_set_fraction (value, clapper_widget->par_n, clapper_widget->par_d); - break; - case PROP_KEEP_LAST_FRAME: - g_value_set_boolean (value, clapper_widget->keep_last_frame); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static gboolean -_calculate_par (GtkClapperGLWidget * clapper_widget, GstVideoInfo * info) -{ - gboolean ok; - gint width, height; - gint par_n, par_d; - gint display_par_n, display_par_d; - - width = GST_VIDEO_INFO_WIDTH (info); - height = GST_VIDEO_INFO_HEIGHT (info); - - par_n = GST_VIDEO_INFO_PAR_N (info); - par_d = GST_VIDEO_INFO_PAR_D (info); - - if (!par_n) - par_n = 1; - - /* get display's PAR */ - if (clapper_widget->par_n != 0 && clapper_widget->par_d != 0) { - display_par_n = clapper_widget->par_n; - display_par_d = clapper_widget->par_d; - } else { - display_par_n = 1; - display_par_d = 1; - } - - ok = gst_video_calculate_display_ratio (&clapper_widget->display_ratio_num, - &clapper_widget->display_ratio_den, width, height, par_n, par_d, display_par_n, - display_par_d); - - if (ok) { - GST_LOG ("PAR: %u/%u DAR:%u/%u", par_n, par_d, display_par_n, - display_par_d); - return TRUE; - } - - return FALSE; -} - -static void -_apply_par (GtkClapperGLWidget * clapper_widget) -{ - guint display_ratio_num, display_ratio_den; - gint width, height; - - width = GST_VIDEO_INFO_WIDTH (&clapper_widget->v_info); - height = GST_VIDEO_INFO_HEIGHT (&clapper_widget->v_info); - - display_ratio_num = clapper_widget->display_ratio_num; - display_ratio_den = clapper_widget->display_ratio_den; - - if (height % display_ratio_den == 0) { - GST_DEBUG ("keeping video height"); - clapper_widget->display_width = (guint) - gst_util_uint64_scale_int (height, display_ratio_num, - display_ratio_den); - clapper_widget->display_height = height; - } else if (width % display_ratio_num == 0) { - GST_DEBUG ("keeping video width"); - clapper_widget->display_width = width; - clapper_widget->display_height = (guint) - gst_util_uint64_scale_int (width, display_ratio_den, display_ratio_num); - } else { - GST_DEBUG ("approximating while keeping video height"); - clapper_widget->display_width = (guint) - gst_util_uint64_scale_int (height, display_ratio_num, - display_ratio_den); - clapper_widget->display_height = height; - } - - GST_DEBUG ("scaling to %dx%d", clapper_widget->display_width, clapper_widget->display_height); -} - -static gboolean -_queue_draw (GtkClapperGLWidget * clapper_widget) -{ - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - clapper_widget->draw_id = 0; - - if (clapper_widget->pending_resize) { - clapper_widget->pending_resize = FALSE; - - clapper_widget->v_info = clapper_widget->pending_v_info; - clapper_widget->negotiated = TRUE; - - _apply_par (clapper_widget); - - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - - gtk_widget_queue_resize (GTK_WIDGET (clapper_widget)); - } else { - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - - gtk_gl_area_queue_render (GTK_GL_AREA (clapper_widget)); - } - - return G_SOURCE_REMOVE; -} - -static const gchar * -_gdk_key_to_navigation_string (guint keyval) -{ - /* TODO: expand */ - switch (keyval) { -#define KEY(key) case GDK_KEY_ ## key: return G_STRINGIFY(key) - KEY (Up); - KEY (Down); - KEY (Left); - KEY (Right); - KEY (Home); - KEY (End); -#undef KEY - default: - return NULL; - } -} - -static gboolean -_get_is_navigation_allowed (GstElement * element, GstState min_state) -{ - if (GST_IS_NAVIGATION (element)) { - GstState nav_state; - - GST_OBJECT_LOCK (element); - nav_state = element->current_state; - GST_OBJECT_UNLOCK (element); - - return nav_state >= min_state; - } - - return FALSE; -} - -static gboolean -gtk_clapper_gl_widget_key_event (GtkEventControllerKey * key_controller, - guint keyval, guint keycode, GdkModifierType state) -{ - GtkEventController *controller = GTK_EVENT_CONTROLLER (key_controller); - GtkWidget *widget = gtk_event_controller_get_widget (controller); - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget); - GstElement *element; - - if ((element = g_weak_ref_get (&clapper_widget->element))) { - if (_get_is_navigation_allowed (element, GST_STATE_PAUSED)) { - GdkEvent *event = gtk_event_controller_get_current_event (controller); - const gchar *str = _gdk_key_to_navigation_string (keyval); - - if (str) { - const gchar *key_type = - gdk_event_get_event_type (event) == - GDK_KEY_PRESS ? "key-press" : "key-release"; - gst_navigation_send_key_event (GST_NAVIGATION (element), key_type, str); - } - } - g_object_unref (element); - } - - return FALSE; -} - -static void -_fit_stream_to_allocated_size (GtkClapperGLWidget * clapper_widget, GstVideoRectangle * result) -{ - if (clapper_widget->force_aspect_ratio) { - GstVideoRectangle src, dst; - - src.x = 0; - src.y = 0; - src.w = clapper_widget->display_width; - src.h = clapper_widget->display_height; - - dst.x = 0; - dst.y = 0; - dst.w = clapper_widget->scaled_width; - dst.h = clapper_widget->scaled_height; - - gst_video_sink_center_rect (src, dst, result, TRUE); - } else { - result->x = 0; - result->y = 0; - result->w = clapper_widget->scaled_width; - result->h = clapper_widget->scaled_height; - } -} - -static void -_display_size_to_stream_size (GtkClapperGLWidget * clapper_widget, gdouble x, - gdouble y, gdouble * stream_x, gdouble * stream_y) -{ - gdouble stream_width, stream_height; - GstVideoRectangle result; - - _fit_stream_to_allocated_size (clapper_widget, &result); - - stream_width = (gdouble) GST_VIDEO_INFO_WIDTH (&clapper_widget->v_info); - stream_height = (gdouble) GST_VIDEO_INFO_HEIGHT (&clapper_widget->v_info); - - /* from display coordinates to stream coordinates */ - if (result.w > 0) - *stream_x = (x - result.x) / result.w * stream_width; - else - *stream_x = 0.; - - /* clip to stream size */ - if (*stream_x < 0.) - *stream_x = 0.; - if (*stream_x > GST_VIDEO_INFO_WIDTH (&clapper_widget->v_info)) - *stream_x = GST_VIDEO_INFO_WIDTH (&clapper_widget->v_info); - - /* same for y-axis */ - if (result.h > 0) - *stream_y = (y - result.y) / result.h * stream_height; - else - *stream_y = 0.; - - if (*stream_y < 0.) - *stream_y = 0.; - if (*stream_y > GST_VIDEO_INFO_HEIGHT (&clapper_widget->v_info)) - *stream_y = GST_VIDEO_INFO_HEIGHT (&clapper_widget->v_info); - - GST_TRACE ("transform %fx%f into %fx%f", x, y, *stream_x, *stream_y); -} - -static gboolean -gtk_clapper_gl_widget_button_event (GtkGestureClick * gesture, - gint n_press, gdouble x, gdouble y) -{ - GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture); - GtkWidget *widget = gtk_event_controller_get_widget (controller); - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget); - GstElement *element; - - if (clapper_widget->display_width == 0 || clapper_widget->display_height == 0) - return FALSE; - - if ((element = g_weak_ref_get (&clapper_widget->element))) { - if (_get_is_navigation_allowed (element, GST_STATE_PLAYING)) { - GdkEvent *event = gtk_event_controller_get_current_event (controller); - const gchar *key_type = - gdk_event_get_event_type (event) == GDK_BUTTON_PRESS - ? "mouse-button-press" : "mouse-button-release"; - gdouble stream_x, stream_y; - - _display_size_to_stream_size (clapper_widget, x, y, &stream_x, &stream_y); - - gst_navigation_send_mouse_event (GST_NAVIGATION (element), key_type, - /* Gesture is set to ignore other buttons so we do not have to check */ - GDK_BUTTON_PRIMARY, - stream_x, stream_y); - } - g_object_unref (element); - } - - return FALSE; -} - -static gboolean -gtk_clapper_gl_widget_motion_event (GtkEventControllerMotion * motion_controller, - gdouble x, gdouble y) -{ - GtkEventController *controller = GTK_EVENT_CONTROLLER (motion_controller); - GtkWidget *widget = gtk_event_controller_get_widget (controller); - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget); - GstElement *element; - - if ((x == clapper_widget->last_pos_x && y == clapper_widget->last_pos_y) || - clapper_widget->display_width == 0 || clapper_widget->display_height == 0) - return FALSE; - - if ((element = g_weak_ref_get (&clapper_widget->element))) { - if (_get_is_navigation_allowed (element, GST_STATE_PLAYING)) { - gdouble stream_x, stream_y; - - clapper_widget->last_pos_x = x; - clapper_widget->last_pos_y = y; - _display_size_to_stream_size (clapper_widget, x, y, &stream_x, &stream_y); - - gst_navigation_send_mouse_event (GST_NAVIGATION (element), "mouse-move", - 0, stream_x, stream_y); - } - g_object_unref (element); - } - - return FALSE; -} - -static void -gtk_clapper_gl_widget_settings_changed (GtkGLArea * glarea) -{ - GST_DEBUG ("GTK settings changed, queued render"); - gtk_gl_area_queue_render (glarea); -} - -static void -gtk_clapper_gl_widget_bind_buffer (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - const GstGLFuncs *gl = priv->context->gl_vtable; - - gl->BindBuffer (GL_ARRAY_BUFFER, priv->vertex_buffer); - - /* Load the vertex position */ - gl->VertexAttribPointer (priv->attr_position, 3, GL_FLOAT, GL_FALSE, - 5 * sizeof (GLfloat), (void *) 0); - - /* Load the texture coordinate */ - gl->VertexAttribPointer (priv->attr_texture, 2, GL_FLOAT, GL_FALSE, - 5 * sizeof (GLfloat), (void *) (3 * sizeof (GLfloat))); - - gl->EnableVertexAttribArray (priv->attr_position); - gl->EnableVertexAttribArray (priv->attr_texture); -} - -static void -gtk_clapper_gl_widget_unbind_buffer (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - const GstGLFuncs *gl = priv->context->gl_vtable; - - gl->BindBuffer (GL_ARRAY_BUFFER, 0); - - gl->DisableVertexAttribArray (priv->attr_position); - gl->DisableVertexAttribArray (priv->attr_texture); -} - -static void -gtk_clapper_gl_widget_init_redisplay (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - const GstGLFuncs *gl = priv->context->gl_vtable; - GError *error = NULL; - GstGLSLStage *frag_stage, *vert_stage; - - gst_gl_insert_debug_marker (priv->other_context, "initializing redisplay"); - - vert_stage = gst_glsl_stage_new_with_string (priv->context, - GL_VERTEX_SHADER, GST_GLSL_VERSION_NONE, - GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY, - gst_gl_shader_string_vertex_mat4_vertex_transform); - if (priv->texture_target == GST_GL_TEXTURE_TARGET_EXTERNAL_OES) { - gchar *frag_str; - - frag_str = - gst_gl_shader_string_fragment_external_oes_get_default - (priv->context, GST_GLSL_VERSION_NONE, - GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY); - frag_stage = gst_glsl_stage_new_with_string (priv->context, - GL_FRAGMENT_SHADER, GST_GLSL_VERSION_NONE, - GST_GLSL_PROFILE_ES | GST_GLSL_PROFILE_COMPATIBILITY, frag_str); - - g_free (frag_str); - } else { - frag_stage = gst_glsl_stage_new_default_fragment (priv->context); - } - - if (!vert_stage || !frag_stage) { - GST_ERROR ("Failed to retrieve fragment shader for texture target"); - if (vert_stage) - gst_object_unref (vert_stage); - if (frag_stage) - gst_object_unref (frag_stage); - return; - } - - if (!(priv->shader = - gst_gl_shader_new_link_with_stages (priv->context, &error, - vert_stage, frag_stage, NULL))) { - GST_ERROR ("Failed to initialize shader: %s", error->message); - return; - } - - priv->attr_position = - gst_gl_shader_get_attribute_location (priv->shader, "a_position"); - priv->attr_texture = - gst_gl_shader_get_attribute_location (priv->shader, "a_texcoord"); - - if (gl->GenVertexArrays) { - gl->GenVertexArrays (1, &priv->vao); - gl->BindVertexArray (priv->vao); - } - - gl->GenBuffers (1, &priv->vertex_buffer); - gl->BindBuffer (GL_ARRAY_BUFFER, priv->vertex_buffer); - gl->BufferData (GL_ARRAY_BUFFER, 4 * 5 * sizeof (GLfloat), vertices, - GL_STATIC_DRAW); - - if (gl->GenVertexArrays) { - gtk_clapper_gl_widget_bind_buffer (clapper_widget); - gl->BindVertexArray (0); - } - - gl->BindBuffer (GL_ARRAY_BUFFER, 0); - - if (!priv->overlay_compositor) - priv->overlay_compositor = - gst_gl_overlay_compositor_new (priv->other_context); - - priv->initiated = TRUE; -} - -static inline void -_draw_black (GstGLContext * context) -{ - const GstGLFuncs *gl = context->gl_vtable; - - gst_gl_insert_debug_marker (context, "rendering black"); - gl->ClearColor (0.0, 0.0, 0.0, 1.0); - gl->Clear (GL_COLOR_BUFFER_BIT); -} - -static inline void -_draw_black_with_gdk (GdkGLContext * gdk_context) -{ - GST_DEBUG ("rendering empty frame with gdk context %p", gdk_context); - glClearColor (0.0, 0.0, 0.0, 1.0); - glClear (GL_COLOR_BUFFER_BIT); -} - -static gboolean -gtk_clapper_gl_widget_render (GtkGLArea * widget, GdkGLContext * context) -{ - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget); - - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - const GstGLFuncs *gl; - - GstVideoAffineTransformationMeta *af_meta; - gfloat matrix[16]; - - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - - /* Draw black with GDK context when priv is not available yet. - GTK calls render with GDK context already active. */ - if (!priv->context || !priv->other_context || clapper_widget->ignore_buffers) { - _draw_black_with_gdk (context); - goto done; - } - - gst_gl_context_activate (priv->other_context, TRUE); - - if (!priv->initiated || !clapper_widget->negotiated) { - if (!priv->initiated) - gtk_clapper_gl_widget_init_redisplay (clapper_widget); - - _draw_black (priv->other_context); - goto done; - } - - /* Upload latest buffer */ - if (clapper_widget->pending_buffer) { - GstBuffer *buffer = clapper_widget->pending_buffer; - GstVideoFrame gl_frame; - GstGLSyncMeta *sync_meta; - - if (!gst_video_frame_map (&gl_frame, &clapper_widget->v_info, buffer, - GST_MAP_READ | GST_MAP_GL)) { - _draw_black (priv->other_context); - goto done; - } - - priv->current_tex = *(guint *) gl_frame.data[0]; - gst_gl_overlay_compositor_upload_overlays (priv->overlay_compositor, - buffer); - - sync_meta = gst_buffer_get_gl_sync_meta (buffer); - if (sync_meta) { - /* XXX: the set_sync() seems to be needed for resizing */ - gst_gl_sync_meta_set_sync_point (sync_meta, priv->context); - gst_gl_sync_meta_wait (sync_meta, priv->other_context); - } - - gst_video_frame_unmap (&gl_frame); - - if (clapper_widget->buffer) - gst_buffer_unref (clapper_widget->buffer); - - /* Keep the buffer to ensure current_tex stay valid */ - clapper_widget->buffer = buffer; - clapper_widget->pending_buffer = NULL; - } - - GST_DEBUG ("rendering buffer %p with gdk context %p", - clapper_widget->buffer, context); - - /* Draw texture */ - gl = priv->context->gl_vtable; - - if (clapper_widget->force_aspect_ratio) { - GstVideoRectangle src, dst, result; - - gl->ClearColor (0.0, 0.0, 0.0, 1.0); - gl->Clear (GL_COLOR_BUFFER_BIT); - - src.x = 0; - src.y = 0; - src.w = clapper_widget->display_width; - src.h = clapper_widget->display_height; - - dst.x = 0; - dst.y = 0; - dst.w = clapper_widget->scaled_width; - dst.h = clapper_widget->scaled_height; - - gst_video_sink_center_rect (src, dst, &result, TRUE); - - gl->Viewport (result.x, result.y, result.w, result.h); - } - - gst_gl_shader_use (priv->shader); - - if (gl->BindVertexArray) - gl->BindVertexArray (priv->vao); - - gtk_clapper_gl_widget_bind_buffer (clapper_widget); - - gl->ActiveTexture (GL_TEXTURE0); - gl->BindTexture (priv->gl_target, priv->current_tex); - gst_gl_shader_set_uniform_1i (priv->shader, "tex", 0); - - af_meta = gst_buffer_get_video_affine_transformation_meta ( - clapper_widget->buffer); - gst_clapper_gl_get_affine_transformation_meta_as_ndc (af_meta, matrix); - gst_gl_shader_set_uniform_matrix_4fv (priv->shader, - "u_transformation", 1, FALSE, matrix); - - gl->DrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices); - - if (gl->BindVertexArray) - gl->BindVertexArray (0); - else - gtk_clapper_gl_widget_unbind_buffer (clapper_widget); - - gl->BindTexture (priv->gl_target, 0); - - /* Draw subtitles */ - gst_gl_overlay_compositor_draw_overlays (priv->overlay_compositor); - -done: - if (priv->other_context) - gst_gl_context_activate (priv->other_context, FALSE); - - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return FALSE; -} - -static void -_cleanup_gl_private (GtkClapperGLWidgetPrivate * priv) -{ - const GstGLFuncs *gl = priv->other_context->gl_vtable; - - if (priv->vao) { - gl->DeleteVertexArrays (1, &priv->vao); - priv->vao = 0; - } - if (priv->vertex_buffer) { - gl->DeleteBuffers (1, &priv->vertex_buffer); - priv->vertex_buffer = 0; - } - if (priv->upload) { - gst_object_unref (priv->upload); - priv->upload = NULL; - } - if (priv->shader) { - gst_object_unref (priv->shader); - priv->shader = NULL; - } - if (priv->overlay_compositor) - gst_gl_overlay_compositor_free_overlays (priv->overlay_compositor); -} - -static void -_cleanup_gl_thread (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - - if (!priv->gdk_context) - priv->gdk_context = gtk_gl_area_get_context (GTK_GL_AREA (clapper_widget)); - - if (priv->gdk_context == NULL) - return; - - gdk_gl_context_make_current (priv->gdk_context); - gst_gl_context_activate (priv->other_context, TRUE); - - _cleanup_gl_private (priv); - - gst_gl_context_activate (priv->other_context, FALSE); - gdk_gl_context_clear_current (); - - priv->initiated = FALSE; -} - -static void -_reset_gl (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - - if (!priv->gdk_context) - priv->gdk_context = gtk_gl_area_get_context (GTK_GL_AREA (clapper_widget)); - - if (priv->gdk_context == NULL) - return; - - gdk_gl_context_make_current (priv->gdk_context); - gst_gl_context_activate (priv->other_context, TRUE); - - _cleanup_gl_private (priv); - - if (priv->overlay_compositor) - gst_object_unref (priv->overlay_compositor); - - gst_gl_context_activate (priv->other_context, FALSE); - - gst_object_unref (priv->other_context); - priv->other_context = NULL; - - gdk_gl_context_clear_current (); - - g_object_unref (priv->gdk_context); - priv->gdk_context = NULL; -} - -static void -gtk_clapper_gl_widget_finalize (GObject * object) -{ - GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (object); - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - - if (priv->other_context) - gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) _reset_gl, clapper_widget); - - if (priv->context) - gst_object_unref (priv->context); - - if (priv->display) - gst_object_unref (priv->display); - - if (clapper_widget->draw_id) - g_source_remove (clapper_widget->draw_id); - - gst_buffer_replace (&clapper_widget->pending_buffer, NULL); - gst_buffer_replace (&clapper_widget->buffer, NULL); - g_mutex_clear (&clapper_widget->lock); - g_weak_ref_clear (&clapper_widget->element); - - G_OBJECT_CLASS (gtk_clapper_gl_widget_parent_class)->finalize (object); -} - -void -gtk_clapper_gl_widget_set_element (GtkClapperGLWidget * clapper_widget, - GstElement * element) -{ - g_weak_ref_set (&clapper_widget->element, element); -} - -gboolean -gtk_clapper_gl_widget_set_format (GtkClapperGLWidget * clapper_widget, - GstVideoInfo * v_info) -{ - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - - if (gst_video_info_is_equal (&clapper_widget->pending_v_info, v_info)) { - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return TRUE; - } - - if (!_calculate_par (clapper_widget, v_info)) { - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return FALSE; - } - - clapper_widget->pending_resize = TRUE; - clapper_widget->pending_v_info = *v_info; - - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - - return TRUE; -} - -void -gtk_clapper_gl_widget_set_buffer (GtkClapperGLWidget * clapper_widget, - GstBuffer * buffer) -{ - g_return_if_fail (GTK_IS_CLAPPER_GL_WIDGET (clapper_widget)); - - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - - gst_buffer_replace (&clapper_widget->pending_buffer, buffer); - - if (!clapper_widget->draw_id) { - clapper_widget->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT, - (GSourceFunc) _queue_draw, clapper_widget, NULL); - } - - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); -} - -static gboolean -_wrap_current_gl (GstGLDisplay * display, GstGLPlatform platform, - GstGLContext ** other_context) -{ - GstGLAPI gl_api = GST_GL_API_NONE; - guint gl_major = 0, gl_minor = 0; - - gl_api = gst_gl_context_get_current_gl_api (platform, &gl_major, &gl_minor); - - if (gl_api) { - const gboolean is_es = gl_api & (GST_GL_API_GLES1 | GST_GL_API_GLES2); - gchar *gl_api_str = gst_gl_api_to_string (gl_api); - guintptr gl_handle = 0; - - GST_INFO ("Using GL API: %s, ver: %d.%d", gl_api_str, gl_major, gl_minor); - g_free (gl_api_str); - - if (is_es && platform == GST_GL_PLATFORM_EGL && !g_getenv ("GST_GL_API")) { - GST_DEBUG ("No GST_GL_API env and GTK is using EGL GLES2, enforcing it"); - gst_gl_display_filter_gl_api (display, GST_GL_API_GLES2); - } - - gl_handle = gst_gl_context_get_current_gl_context (platform); - if (gl_handle) { - if ((*other_context = gst_gl_context_new_wrapped (display, - gl_handle, platform, gl_api))) - return TRUE; - } - } - - return FALSE; -} - -static void -_get_gl_context (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - GstGLPlatform platform = GST_GL_PLATFORM_NONE; - - gtk_widget_realize (GTK_WIDGET (clapper_widget)); - - if (priv->other_context) - gst_object_unref (priv->other_context); - priv->other_context = NULL; - - if (priv->gdk_context) - g_object_unref (priv->gdk_context); - - priv->gdk_context = gtk_gl_area_get_context (GTK_GL_AREA (clapper_widget)); - if (priv->gdk_context == NULL) { - GError *error = gtk_gl_area_get_error (GTK_GL_AREA (clapper_widget)); - - GST_ERROR_OBJECT (clapper_widget, "Error creating GdkGLContext : %s", - error ? error->message : "No error set by Gdk"); - g_clear_error (&error); - return; - } - - g_object_ref (priv->gdk_context); - gdk_gl_context_make_current (priv->gdk_context); - -#if GST_GL_HAVE_WINDOW_WAYLAND && GST_GL_HAVE_PLATFORM_EGL && defined (GDK_WINDOWING_WAYLAND) - if (GST_IS_GL_DISPLAY_WAYLAND (priv->display)) { - platform = GST_GL_PLATFORM_EGL; - GST_DEBUG ("Using EGL on Wayland"); - goto have_platform; - } -#endif -#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11) -#if GST_GL_HAVE_PLATFORM_EGL - if (GST_IS_GL_DISPLAY_EGL (priv->display)) { - platform = GST_GL_PLATFORM_EGL; - GST_DEBUG ("Using EGL on x11"); - goto have_platform; - } -#endif -#if GST_GL_HAVE_PLATFORM_GLX - if (GST_IS_GL_DISPLAY_X11 (priv->display)) { - platform = GST_GL_PLATFORM_GLX; - GST_DEBUG ("Using GLX on x11"); - goto have_platform; - } -#endif -#endif - - GST_ERROR ("Unknown GL platform"); - return; - -have_platform: - - if (_wrap_current_gl (priv->display, platform, &priv->other_context)) { - GError *error = NULL; - - GST_INFO ("Retrieved Gdk OpenGL context %" GST_PTR_FORMAT, - priv->other_context); - gst_gl_context_activate (priv->other_context, TRUE); - if (!gst_gl_context_fill_info (priv->other_context, &error)) { - GST_ERROR ("Failed to retrieve gdk context info: %s", error->message); - g_clear_error (&error); - g_object_unref (priv->other_context); - priv->other_context = NULL; - } else { - gst_gl_context_activate (priv->other_context, FALSE); - } - } else { - GST_WARNING ("Could not retrieve Gdk OpenGL context"); - } -} - -static void -gtk_clapper_gl_widget_class_init (GtkClapperGLWidgetClass * klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - GtkWidgetClass *widget_class = (GtkWidgetClass *) klass; - GtkGLAreaClass *gl_area_class = (GtkGLAreaClass *) klass; - - gobject_class->set_property = gtk_clapper_gl_widget_set_property; - gobject_class->get_property = gtk_clapper_gl_widget_get_property; - gobject_class->finalize = gtk_clapper_gl_widget_finalize; - - gst_gtk_install_shared_properties (gobject_class); - - widget_class->measure = gtk_clapper_gl_widget_measure; - widget_class->size_allocate = gtk_clapper_gl_widget_size_allocate; - - gl_area_class->render = gtk_clapper_gl_widget_render; -} - -static void -gtk_clapper_gl_widget_init (GtkClapperGLWidget * clapper_widget) -{ - GdkDisplay *display; - GtkClapperGLWidgetPrivate *priv; - GtkWidget *widget = GTK_WIDGET (clapper_widget); - - clapper_widget->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO; - clapper_widget->par_n = DEFAULT_PAR_N; - clapper_widget->par_d = DEFAULT_PAR_D; - clapper_widget->keep_last_frame = DEFAULT_KEEP_LAST_FRAME; - clapper_widget->ignore_buffers = FALSE; - clapper_widget->last_pos_x = 0; - clapper_widget->last_pos_y = 0; - - gst_video_info_init (&clapper_widget->v_info); - gst_video_info_init (&clapper_widget->pending_v_info); - - g_weak_ref_init (&clapper_widget->element, NULL); - g_mutex_init (&clapper_widget->lock); - - clapper_widget->key_controller = gtk_event_controller_key_new (); - g_signal_connect (clapper_widget->key_controller, "key-pressed", - G_CALLBACK (gtk_clapper_gl_widget_key_event), NULL); - g_signal_connect (clapper_widget->key_controller, "key-released", - G_CALLBACK (gtk_clapper_gl_widget_key_event), NULL); - - clapper_widget->motion_controller = gtk_event_controller_motion_new (); - g_signal_connect (clapper_widget->motion_controller, "motion", - G_CALLBACK (gtk_clapper_gl_widget_motion_event), NULL); - - clapper_widget->click_gesture = gtk_gesture_click_new (); - g_signal_connect (clapper_widget->click_gesture, "pressed", - G_CALLBACK (gtk_clapper_gl_widget_button_event), NULL); - g_signal_connect (clapper_widget->click_gesture, "released", - G_CALLBACK (gtk_clapper_gl_widget_button_event), NULL); - - /* Otherwise widget in grid will appear as a 1x1px - * video which might be misleading for users */ - gtk_widget_set_hexpand (widget, TRUE); - gtk_widget_set_vexpand (widget, TRUE); - - gtk_widget_set_focusable (widget, TRUE); - gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (clapper_widget->click_gesture), - GDK_BUTTON_PRIMARY); - - gtk_widget_add_controller (widget, clapper_widget->key_controller); - gtk_widget_add_controller (widget, clapper_widget->motion_controller); - gtk_widget_add_controller (widget, GTK_EVENT_CONTROLLER (clapper_widget->click_gesture)); - - gtk_widget_set_can_focus (widget, TRUE); - - clapper_widget->priv = priv = gtk_clapper_gl_widget_get_instance_private (clapper_widget); - - display = gdk_display_get_default (); - -#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11) - if (GDK_IS_X11_DISPLAY (display)) { - gpointer display_ptr; -#if GST_GL_HAVE_PLATFORM_EGL && GTK_CHECK_VERSION(4,3,1) - display_ptr = gdk_x11_display_get_egl_display (display); - if (display_ptr) - priv->display = (GstGLDisplay *) - gst_gl_display_egl_new_with_egl_display (display_ptr); -#endif -#if GST_GL_HAVE_PLATFORM_GLX - if (!priv->display) { - display_ptr = gdk_x11_display_get_xdisplay (display); - priv->display = (GstGLDisplay *) - gst_gl_display_x11_new_with_display (display_ptr); - } -#endif - } -#endif -#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND) - if (GDK_IS_WAYLAND_DISPLAY (display)) { - struct wl_display *wayland_display = - gdk_wayland_display_get_wl_display (display); - priv->display = (GstGLDisplay *) - gst_gl_display_wayland_new_with_display (wayland_display); - } -#endif - - (void) display; - - if (!priv->display) - priv->display = gst_gl_display_new (); - - GST_INFO ("Created %" GST_PTR_FORMAT, priv->display); - - priv->texture_target = GST_GL_TEXTURE_TARGET_NONE; - priv->gl_target = 0; - - gtk_gl_area_set_auto_render (GTK_GL_AREA (widget), FALSE); - - g_signal_connect_swapped (gtk_widget_get_settings (widget), "notify", - G_CALLBACK (gtk_clapper_gl_widget_settings_changed), GTK_GL_AREA (widget)); -} - -GtkWidget * -gtk_clapper_gl_widget_new (void) -{ - return (GtkWidget *) g_object_new (GTK_TYPE_CLAPPER_GL_WIDGET, NULL); -} - -gboolean -gtk_clapper_gl_widget_init_winsys (GtkClapperGLWidget * clapper_widget) -{ - GtkClapperGLWidgetPrivate *priv = clapper_widget->priv; - GError *error = NULL; - - g_return_val_if_fail (GTK_IS_CLAPPER_GL_WIDGET (clapper_widget), FALSE); - g_return_val_if_fail (priv->display != NULL, FALSE); - - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - - if (priv->display && priv->gdk_context && priv->other_context) { - GST_TRACE ("have already initialized contexts"); - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return TRUE; - } - - if (!priv->other_context) { - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) _get_gl_context, clapper_widget); - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - } - - if (!GST_IS_GL_CONTEXT (priv->other_context)) { - GST_FIXME ("Could not retrieve Gdk OpenGL context"); - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return FALSE; - } - - GST_OBJECT_LOCK (priv->display); - if (!gst_gl_display_create_context (priv->display, priv->other_context, - &priv->context, &error)) { - GST_WARNING ("Could not create OpenGL context: %s", - error ? error->message : "Unknown"); - g_clear_error (&error); - GST_OBJECT_UNLOCK (priv->display); - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return FALSE; - } - gst_gl_display_add_context (priv->display, priv->context); - GST_OBJECT_UNLOCK (priv->display); - - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return TRUE; -} - -GstGLContext * -gtk_clapper_gl_widget_get_gtk_context (GtkClapperGLWidget * clapper_widget) -{ - if (!clapper_widget->priv->other_context) - return NULL; - - return gst_object_ref (clapper_widget->priv->other_context); -} - -GstGLContext * -gtk_clapper_gl_widget_get_context (GtkClapperGLWidget * clapper_widget) -{ - if (!clapper_widget->priv->context) - return NULL; - - return gst_object_ref (clapper_widget->priv->context); -} - -GstGLDisplay * -gtk_clapper_gl_widget_get_display (GtkClapperGLWidget * clapper_widget) -{ - if (!clapper_widget->priv->display) - return NULL; - - return gst_object_ref (clapper_widget->priv->display); -} - -gboolean -gtk_clapper_gl_widget_update_output_format (GtkClapperGLWidget * clapper_widget, - GstCaps * caps) -{ - GtkClapperGLWidgetPrivate *priv; - GstGLTextureTarget previous_target; - GstStructure *structure; - const gchar *target_str; - gboolean cleanup_gl; - - GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget); - priv = clapper_widget->priv; - - previous_target = priv->texture_target; - structure = gst_caps_get_structure (caps, 0); - target_str = gst_structure_get_string (structure, "texture-target"); - - if (!target_str) - target_str = GST_GL_TEXTURE_TARGET_2D_STR; - - priv->texture_target = gst_gl_texture_target_from_string (target_str); - if (!priv->texture_target) - goto fail; - - GST_DEBUG_OBJECT (clapper_widget, "Using texture-target: %s", target_str); - priv->gl_target = gst_gl_texture_target_to_gl (priv->texture_target); - - cleanup_gl = (previous_target != GST_GL_TEXTURE_TARGET_NONE && - priv->texture_target != previous_target); - - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - if (cleanup_gl) - gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) _cleanup_gl_thread, clapper_widget); - - return TRUE; - -fail: - GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget); - return FALSE; -} diff --git a/lib/gst/clapper/gtk4/gtkclapperglwidget.h b/lib/gst/clapper/gtk4/gtkclapperglwidget.h deleted file mode 100644 index f9f2d303..00000000 --- a/lib/gst/clapper/gtk4/gtkclapperglwidget.h +++ /dev/null @@ -1,113 +0,0 @@ -/* - * GStreamer - * Copyright (C) 2015 Matthew Waters - * Copyright (C) 2020 Rafał Dzięgiel - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef __GTK_CLAPPER_GL_WIDGET_H__ -#define __GTK_CLAPPER_GL_WIDGET_H__ - -#include -#include -#include -#include - -G_BEGIN_DECLS - -GType gtk_clapper_gl_widget_get_type (void); -#define GTK_TYPE_CLAPPER_GL_WIDGET (gtk_clapper_gl_widget_get_type()) -#define GTK_CLAPPER_GL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GTK_TYPE_CLAPPER_GL_WIDGET,GtkClapperGLWidget)) -#define GTK_CLAPPER_GL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GTK_TYPE_CLAPPER_GL_WIDGET,GtkClapperGLWidgetClass)) -#define GTK_IS_CLAPPER_GL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GTK_TYPE_CLAPPER_GL_WIDGET)) -#define GTK_IS_CLAPPER_GL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GTK_TYPE_CLAPPER_GL_WIDGET)) -#define GTK_CLAPPER_GL_WIDGET_CAST(obj) ((GtkClapperGLWidget*)(obj)) -#define GTK_CLAPPER_GL_WIDGET_LOCK(w) g_mutex_lock(&((GtkClapperGLWidget*)(w))->lock) -#define GTK_CLAPPER_GL_WIDGET_UNLOCK(w) g_mutex_unlock(&((GtkClapperGLWidget*)(w))->lock) - -typedef struct _GtkClapperGLWidget GtkClapperGLWidget; -typedef struct _GtkClapperGLWidgetClass GtkClapperGLWidgetClass; -typedef struct _GtkClapperGLWidgetPrivate GtkClapperGLWidgetPrivate; - -struct _GtkClapperGLWidget -{ - /* */ - GtkGLArea parent; - GtkClapperGLWidgetPrivate *priv; - - /* properties */ - gboolean force_aspect_ratio; - gint par_n, par_d; - gboolean keep_last_frame; - - gint display_width; - gint display_height; - - /* Widget dimensions */ - gint scaled_width; - gint scaled_height; - - /* Position coords */ - gdouble last_pos_x; - gdouble last_pos_y; - - gboolean negotiated; - gboolean ignore_buffers; - GstBuffer *pending_buffer; - GstBuffer *buffer; - GstVideoInfo v_info; - - /* resize */ - gboolean pending_resize; - GstVideoInfo pending_v_info; - guint display_ratio_num; - guint display_ratio_den; - - /*< private >*/ - GMutex lock; - GWeakRef element; - - /* event controllers */ - GtkEventController *key_controller; - GtkEventController *motion_controller; - GtkGesture *click_gesture; - - /* Pending draw idles callback */ - guint draw_id; -}; - -struct _GtkClapperGLWidgetClass -{ - GtkGLAreaClass parent_class; -}; - -/* API */ -gboolean gtk_clapper_gl_widget_set_format (GtkClapperGLWidget * widget, GstVideoInfo * v_info); -void gtk_clapper_gl_widget_set_buffer (GtkClapperGLWidget * widget, GstBuffer * buffer); -void gtk_clapper_gl_widget_set_element (GtkClapperGLWidget * widget, GstElement * element); - -GtkWidget * gtk_clapper_gl_widget_new (void); - -gboolean gtk_clapper_gl_widget_init_winsys (GtkClapperGLWidget * widget); -GstGLDisplay * gtk_clapper_gl_widget_get_display (GtkClapperGLWidget * widget); -GstGLContext * gtk_clapper_gl_widget_get_context (GtkClapperGLWidget * widget); -GstGLContext * gtk_clapper_gl_widget_get_gtk_context (GtkClapperGLWidget * widget); -gboolean gtk_clapper_gl_widget_update_output_format (GtkClapperGLWidget * widget, GstCaps * caps); - -G_END_DECLS - -#endif /* __GTK_CLAPPER_GL_WIDGET_H__ */ diff --git a/lib/gst/clapper/meson.build b/lib/gst/clapper/meson.build deleted file mode 100644 index 2ce5e78e..00000000 --- a/lib/gst/clapper/meson.build +++ /dev/null @@ -1,120 +0,0 @@ -gstclapper_sources = [ - 'gstclapper.c', - 'gstclapper-signal-dispatcher.c', - 'gstclapper-video-renderer.c', - 'gstclapper-media-info.c', - 'gstclapper-g-main-context-signal-dispatcher.c', - 'gstclapper-video-overlay-video-renderer.c', - 'gstclapper-visualization.c', - 'gstclapper-mpris.c', - 'gstclapper-gtk4-plugin.c', - - 'gtk4/gstclapperglsink.c', - 'gtk4/gstgtkutils.c', - 'gtk4/gtkclapperglwidget.c', - 'gtk4/gstclapperglutils.c', -] -gstclapper_headers = [ - 'clapper.h', - 'clapper-prelude.h', - 'gstclapper.h', - 'gstclapper-types.h', - 'gstclapper-signal-dispatcher.h', - 'gstclapper-video-renderer.h', - 'gstclapper-media-info.h', - 'gstclapper-g-main-context-signal-dispatcher.h', - 'gstclapper-video-overlay-video-renderer.h', - 'gstclapper-visualization.h', - 'gstclapper-mpris.h', - 'gstclapper-gtk4-plugin.h', -] -gstclapper_defines = [ - '-DHAVE_CONFIG_H', - '-DBUILDING_GST_CLAPPER', - '-DGST_USE_UNSTABLE_API', - '-DHAVE_GTK_GL', -] -gtk_deps = [gstgl_dep, gstglproto_dep] -have_gtk_gl_windowing = false - -gtk4_dep = dependency('gtk4', required: true) - -if not gtk4_dep.version().version_compare('>=4.0.0') - error('GTK4 version on this system is too old') -endif - -if not gir.found() - error('Cannot build lib without GIR support') -endif - -if gst_gl_have_window_x11 and (gst_gl_have_platform_egl or gst_gl_have_platform_glx) - gtk_x11_dep = dependency('gtk4-x11', required: false) - if gtk_x11_dep.found() - gtk_deps += gtk_x11_dep - if gst_gl_have_platform_glx - gtk_deps += gstglx11_dep - endif - have_gtk_gl_windowing = true - endif -endif - -if gst_gl_have_window_wayland and gst_gl_have_platform_egl - gtk_wayland_dep = dependency('gtk4-wayland', required: false) - if gtk_wayland_dep.found() - gtk_deps += [gtk_wayland_dep, gstglwayland_dep] - have_gtk_gl_windowing = true - endif -endif - -if gst_gl_have_platform_egl - gtk_deps += gstglegl_dep -endif - -if not have_gtk_gl_windowing - error('GTK4 widget requires GL windowing') -endif - -gstclapper_mpris_gdbus = gnome.gdbus_codegen('gstclapper-mpris-gdbus', - sources: '../../../data/gstclapper-mpris-gdbus.xml', - interface_prefix: 'org.mpris.', - namespace: 'GstClapperMpris' -) - -gstclapper_deps = [ - gtk4_dep, glib_dep, gio_dep, - gstbase_dep, gstvideo_dep, gstaudio_dep, - gsttag_dep, gstpbutils_dep, libm -] + gtk_deps - -if os_unix - gstclapper_deps += giounix_dep -else - gstclapper_deps += giowin_dep -endif - -gstclapper = library('gstclapper-' + api_version, - gstclapper_sources + gstclapper_mpris_gdbus, - c_args: gstclapper_defines, - link_args: noseh_link_args, - include_directories: [configinc, libsinc], - version: libversion, - install: true, - install_dir: pkglibdir, - dependencies: gstclapper_deps, -) - -clapper_gir = gnome.generate_gir(gstclapper, - sources: gstclapper_sources + gstclapper_headers, - namespace: 'GstClapper', - nsversion: api_version, - identifier_prefix: 'Gst', - symbol_prefix: 'gst', - export_packages: 'gstreamer-clapper-1.0', - includes: ['Gst-1.0', 'GstPbutils-1.0', 'GstBase-1.0', 'GstVideo-1.0', - 'GstAudio-1.0', 'GstTag-1.0'], - install: true, - install_dir_typelib: join_paths(pkglibdir, 'girepository-1.0'), - extra_args: gir_init_section + ['-DGST_USE_UNSTABLE_API'], - dependencies: [gstbase_dep, gstvideo_dep, gstaudio_dep, - gsttag_dep, gstpbutils_dep] -) diff --git a/lib/gst/meson.build b/lib/gst/meson.build deleted file mode 100644 index d58f4ac2..00000000 --- a/lib/gst/meson.build +++ /dev/null @@ -1,5 +0,0 @@ -if get_option('lib') - subdir('clapper') -endif - -subdir('plugin') diff --git a/lib/gst/plugin/handlers/gl/meson.build b/lib/gst/plugin/handlers/gl/meson.build deleted file mode 100644 index 1a8409e4..00000000 --- a/lib/gst/plugin/handlers/gl/meson.build +++ /dev/null @@ -1,83 +0,0 @@ -gst_clapper_gl_ch_dep = dependency('', required: false) - -build_gl_ch = ( - not get_option('glimporter').disabled() - or not get_option('gluploader').disabled() -) -gl_support_required = ( - get_option('glimporter').enabled() - or get_option('gluploader').enabled() -) - -gst_plugin_gl_ch_deps = [gst_clapper_sink_dep, gstgl_dep, gstglproto_dep] -have_gtk_gl_windowing = false - -if gst_gl_have_window_x11 and (gst_gl_have_platform_egl or gst_gl_have_platform_glx) - gtk_x11_dep = dependency('gtk4-x11', required: false) - if gtk_x11_dep.found() - gst_plugin_gl_ch_deps += gtk_x11_dep - if gst_gl_have_platform_glx - gst_plugin_gl_ch_deps += gstglx11_dep - endif - have_gtk_gl_windowing = true - endif -endif - -if gst_gl_have_window_wayland and gst_gl_have_platform_egl - gtk_wayland_dep = dependency('gtk4-wayland', required: false) - if gtk_wayland_dep.found() - gst_plugin_gl_ch_deps += [gtk_wayland_dep, gstglwayland_dep] - have_gtk_gl_windowing = true - endif -endif - -if gst_gl_have_window_win32 and (gst_gl_have_platform_egl or gst_gl_have_platform_wgl) - gtk_win32_dep = dependency('gtk4-win32', required: false) - if gtk_win32_dep.found() - gst_plugin_gl_ch_deps += gtk_win32_dep - have_gtk_gl_windowing = true - endif -endif - -if gst_gl_have_window_cocoa and gst_gl_have_platform_cgl - gtk_macos_dep = dependency('gtk4-macos', required: false) - if gtk_macos_dep.found() - gst_plugin_gl_ch_deps += gtk_macos_dep - have_gtk_gl_windowing = true - endif -endif - -if not have_gtk_gl_windowing - if gl_support_required - error('GL-based importer was enabled, but support for current GL windowing is missing') - endif - build_gl_ch = false -endif - -if gst_gl_have_platform_egl - gst_plugin_gl_ch_deps += gstglegl_dep -endif - -foreach dep : gst_plugin_gl_ch_deps - if not dep.found() - if gl_support_required - error('GL-based importer was enabled, but required dependencies were not found') - endif - build_gl_ch = false - endif -endforeach - -if build_gl_ch - gst_clapper_gl_ch_dep = declare_dependency( - link_with: library('gstclapperglcontexthandler', - 'gstclapperglcontexthandler.c', - c_args: gst_clapper_plugin_args, - include_directories: configinc, - dependencies: gst_plugin_gl_ch_deps, - version: libversion, - install: true, - ), - include_directories: configinc, - dependencies: gst_plugin_gl_ch_deps, - ) -endif diff --git a/lib/meson.build b/lib/meson.build deleted file mode 100644 index c930620b..00000000 --- a/lib/meson.build +++ /dev/null @@ -1,274 +0,0 @@ -glib_req = '>= 2.68.0' -gst_req = '>= 1.20.0' - -api_version = '1.0' -libversion = meson.project_version() - -cc = meson.get_compiler('c') -cxx = meson.get_compiler('cpp') - -cdata = configuration_data() - -os_unix = host_machine.system() != 'windows' - -if cc.get_id() == 'msvc' - msvc_args = [ - # Ignore several spurious warnings for things gstreamer does very commonly - # If a warning is completely useless and spammy, use '/wdXXXX' to suppress it - # If a warning is harmless but hard to fix, use '/woXXXX' so it's shown once - # NOTE: Only add warnings here if you are sure they're spurious - '/wd4018', # implicit signed/unsigned conversion - '/wd4146', # unary minus on unsigned (beware INT_MIN) - '/wd4244', # lossy type conversion (e.g. double -> int) - '/wd4305', # truncating type conversion (e.g. double -> float) - cc.get_supported_arguments(['/utf-8']), # set the input encoding to utf-8 - - # Enable some warnings on MSVC to match GCC/Clang behaviour - '/w14062', # enumerator 'identifier' in switch of enum 'enumeration' is not handled - '/w14101', # 'identifier' : unreferenced local variable - '/w14189', # 'identifier' : local variable is initialized but not referenced - ] - add_project_arguments(msvc_args, language: ['c', 'cpp']) - noseh_link_args = ['/SAFESEH:NO'] -else - if cxx.has_argument('-Wno-non-virtual-dtor') - add_project_arguments('-Wno-non-virtual-dtor', language: 'cpp') - endif - noseh_link_args = [] -endif - -if cc.has_link_argument('-Wl,-Bsymbolic-functions') - add_project_link_arguments('-Wl,-Bsymbolic-functions', language: 'c') -endif - -# Symbol visibility -if cc.get_id() == 'msvc' - export_define = '__declspec(dllexport) extern' -else - export_define = 'extern' -endif - -# Passing this through the command line would be too messy -cdata.set('GST_API_EXPORT', export_define) - -# Disable strict aliasing -if cc.has_argument('-fno-strict-aliasing') - add_project_arguments('-fno-strict-aliasing', language: 'c') -endif -if cxx.has_argument('-fno-strict-aliasing') - add_project_arguments('-fno-strict-aliasing', language: 'cpp') -endif - -if not get_option('deprecated-glib-api') - message('Disabling deprecated GLib API') - add_project_arguments('-DG_DISABLE_DEPRECATED', language: 'c') -endif - -if not get_option('devel-checks') - message('Disabling GLib cast checks') - add_project_arguments('-DG_DISABLE_CAST_CHECKS', language: 'c') - - message('Disabling GLib asserts') - add_project_arguments('-DG_DISABLE_ASSERT', language: 'c') - - message('Disabling GLib checks') - add_project_arguments('-DG_DISABLE_CHECKS', language: 'c') -endif - -check_headers = [ - ['HAVE_DLFCN_H', 'dlfcn.h'], - ['HAVE_FCNTL_H', 'fcntl.h'], - ['HAVE_INTTYPES_H', 'inttypes.h'], - ['HAVE_MEMORY_H', 'memory.h'], - ['HAVE_NETINET_IN_H', 'netinet/in.h'], - ['HAVE_NETINET_IP_H', 'netinet/ip.h'], - ['HAVE_NETINET_TCP_H', 'netinet/tcp.h'], - ['HAVE_PTHREAD_H', 'pthread.h'], - ['HAVE_STDINT_H', 'stdint.h'], - ['HAVE_STDLIB_H', 'stdlib.h'], - ['HAVE_STRINGS_H', 'strings.h'], - ['HAVE_STRING_H', 'string.h'], - ['HAVE_SYS_PARAM_H', 'sys/param.h'], - ['HAVE_SYS_SOCKET_H', 'sys/socket.h'], - ['HAVE_SYS_STAT_H', 'sys/stat.h'], - ['HAVE_SYS_TIME_H', 'sys/time.h'], - ['HAVE_SYS_TYPES_H', 'sys/types.h'], - ['HAVE_SYS_UTSNAME_H', 'sys/utsname.h'], - ['HAVE_UNISTD_H', 'unistd.h'], -] - -foreach h : check_headers - if cc.has_header(h.get(1)) - cdata.set(h.get(0), 1) - endif -endforeach - -check_functions = [ - ['HAVE_DCGETTEXT', 'dcgettext'], - ['HAVE_GETPAGESIZE', 'getpagesize'], - ['HAVE_GMTIME_R', 'gmtime_r'], - ['HAVE_MEMFD_CREATE', 'memfd_create'], - ['HAVE_MMAP', 'mmap'], - ['HAVE_PIPE2', 'pipe2'], - ['HAVE_GETRUSAGE', 'getrusage', '#include'], -] - -foreach f : check_functions - prefix = '' - if f.length() == 3 - prefix = f.get(2) - endif - if cc.has_function(f.get(1), prefix: prefix) - cdata.set(f.get(0), 1) - endif -endforeach - -cdata.set('SIZEOF_CHAR', cc.sizeof('char')) -cdata.set('SIZEOF_INT', cc.sizeof('int')) -cdata.set('SIZEOF_LONG', cc.sizeof('long')) -cdata.set('SIZEOF_SHORT', cc.sizeof('short')) -cdata.set('SIZEOF_VOIDP', cc.sizeof('void*')) - -cdata.set_quoted('VERSION', libversion) -cdata.set_quoted('PACKAGE', 'clapper') -cdata.set_quoted('PACKAGE_VERSION', libversion) -cdata.set_quoted('PACKAGE_BUGREPORT', 'https://github.com/Rafostar/clapper/issues/new') -cdata.set_quoted('PACKAGE_NAME', 'GStreamer Clapper Libs') -cdata.set_quoted('GST_API_VERSION', api_version) -cdata.set_quoted('GST_LICENSE', 'LGPL') -cdata.set_quoted('LIBDIR', pkglibdir) -cdata.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) - -warning_flags = [ - '-Wmissing-declarations', - '-Wredundant-decls', - '-Wwrite-strings', - '-Wformat', - '-Wformat-security', - '-Winit-self', - '-Wmissing-include-dirs', - '-Waddress', - '-Wno-multichar', - '-Wvla', - '-Wpointer-arith', -] -warning_c_flags = [ - '-Wmissing-prototypes', - '-Wdeclaration-after-statement', - '-Wold-style-definition', -] -warning_cxx_flags = [ - '-Wformat-nonliteral', -] - -foreach extra_arg : warning_c_flags - if cc.has_argument (extra_arg) - add_project_arguments([extra_arg], language: 'c') - endif -endforeach - -foreach extra_arg : warning_cxx_flags - if cxx.has_argument (extra_arg) - add_project_arguments([extra_arg], language: 'cpp') - endif -endforeach - -foreach extra_arg : warning_flags - if cc.has_argument (extra_arg) - add_project_arguments([extra_arg], language: 'c') - endif - if cxx.has_argument (extra_arg) - add_project_arguments([extra_arg], language: 'cpp') - endif -endforeach - -cdata.set_quoted('GST_PACKAGE_NAME', 'gst-plugin-clapper') -cdata.set_quoted('GST_PACKAGE_ORIGIN', 'https://github.com/Rafostar/clapper') - -# Mandatory GST deps -gst_dep = dependency('gstreamer-1.0', version: gst_req, - fallback: ['gstreamer', 'gst_dep']) -gstbase_dep = dependency('gstreamer-base-1.0', version: gst_req, - fallback: ['gstreamer', 'gst_base_dep']) -gstpbutils_dep = dependency('gstreamer-pbutils-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'pbutils_dep']) -gstaudio_dep = dependency('gstreamer-audio-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'audio_dep']) -gsttag_dep = dependency('gstreamer-tag-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'tag_dep']) -gstvideo_dep = dependency('gstreamer-video-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'video_dep']) - -# GStreamer OpenGL -gstgl_dep = dependency('gstreamer-gl-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'gstgl_dep'], required: true) -gstglx11_dep = dependency('', required: false) -gstglwayland_dep = dependency('', required: false) -gstglegl_dep = dependency('', required: false) - -gst_gl_apis = gstgl_dep.get_pkgconfig_variable('gl_apis') -gst_gl_winsys = gstgl_dep.get_pkgconfig_variable('gl_winsys') -gst_gl_platforms = gstgl_dep.get_pkgconfig_variable('gl_platforms') - -message('GStreamer OpenGL window systems: @0@'.format(gst_gl_winsys)) -message('GStreamer OpenGL platforms: @0@'.format(gst_gl_platforms)) -message('GStreamer OpenGL apis: @0@'.format(gst_gl_apis)) - -foreach ws : ['x11', 'wayland', 'android', 'cocoa', 'eagl', 'win32', 'dispmanx', 'viv_fb'] - set_variable('gst_gl_have_window_@0@'.format(ws), gst_gl_winsys.contains(ws)) -endforeach - -foreach p : ['glx', 'egl', 'cgl', 'eagl', 'wgl'] - set_variable('gst_gl_have_platform_@0@'.format(p), gst_gl_platforms.contains(p)) -endforeach - -foreach api : ['gl', 'gles2'] - set_variable('gst_gl_have_api_@0@'.format(api), gst_gl_apis.contains(api)) -endforeach - -gstglproto_dep = dependency('gstreamer-gl-prototypes-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'gstglproto_dep'], required: true) -if gst_gl_have_window_x11 - gstglx11_dep = dependency('gstreamer-gl-x11-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'gstglx11_dep'], required: true) -endif -if gst_gl_have_window_wayland - gstglwayland_dep = dependency('gstreamer-gl-wayland-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'gstglwayland_dep'], required: true) -endif -if gst_gl_have_platform_egl - gstglegl_dep = dependency('gstreamer-gl-egl-1.0', version: gst_req, - fallback: ['gst-plugins-base', 'gstglegl_dep'], required: true) -endif - -libm = cc.find_library('m', required: false) -glib_dep = dependency('glib-2.0', version: glib_req, fallback: ['glib', 'libglib_dep']) -gmodule_dep = dependency('gmodule-2.0', fallback: ['glib', 'libgmodule_dep']) -gio_dep = dependency('gio-2.0', fallback: ['glib', 'libgio_dep']) - -if os_unix - giounix_dep = dependency('gio-unix-2.0', version: glib_req, fallback: ['glib', 'libgio_dep']) -else - giowin_dep = dependency('gio-windows-2.0', version: glib_req, fallback : ['glib', 'libgio_dep']) -endif - -cdata.set('DISABLE_ORC', 1) -cdata.set('GST_ENABLE_EXTRA_CHECKS', get_option('devel-checks')) - -configinc = include_directories('.') -libsinc = include_directories('gst') - -gir = find_program('g-ir-scanner', required: false) -gir_init_section = ['--add-init-section=extern void gst_init(gint*,gchar**);' + \ - 'g_setenv("GST_REGISTRY_1.0", "@0@", TRUE);'.format(meson.current_build_dir() + '/gir_empty_registry.reg') + \ - 'g_setenv("GST_PLUGIN_PATH_1_0", "", TRUE);' + \ - 'g_setenv("GST_PLUGIN_SYSTEM_PATH_1_0", "", TRUE);' + \ - 'gst_init(NULL,NULL);', '--quiet' -] - -gst_clapper_plugin_libdir = join_paths(get_option('prefix'), libdir, 'clapper-@0@'.format(api_version), 'gst', 'plugin') -gst_clapper_importers_libdir = join_paths(gst_clapper_plugin_libdir, 'importers') -cdata.set_quoted('CLAPPER_SINK_IMPORTER_PATH', gst_clapper_importers_libdir) - -subdir('gst') -configure_file(output: 'config.h', configuration: cdata) diff --git a/meson.build b/meson.build index 36a184a0..e3b02f7c 100644 --- a/meson.build +++ b/meson.build @@ -1,39 +1,140 @@ -project('com.github.rafostar.Clapper', 'c', 'cpp', +project('clapper', 'c', version: '0.5.2', - meson_version: '>= 0.50.0', - license: 'GPL-3.0-or-later', + meson_version: '>= 0.64.0', + license: 'LGPL-2.1-or-later AND GPL-3.0-or-later', # LGPL-2.1+ for libs and gst-plugin, GPL-3.0+ for app default_options: [ 'warning_level=1', - 'buildtype=debugoptimized' - ] + 'buildtype=debugoptimized', + ], ) +glib_req = '>= 2.76.0' +gst_req = '>= 1.20.0' +gtk4_req = '>= 4.10.0' + +clapper_version = meson.project_version().split('-')[0] +version_array = clapper_version.split('.') +clapper_version_suffix = '-' + version_array[0] + '.0' + +clapper_api_name = meson.project_name() + clapper_version_suffix + gnome = import('gnome') +pkgconfig = import('pkgconfig') i18n = import('i18n') -python = import('python') -bindir = join_paths(get_option('prefix'), get_option('bindir')) -libdir = join_paths(get_option('prefix'), get_option('libdir')) -datadir = join_paths(get_option('prefix'), get_option('datadir')) +prefix = get_option('prefix') +bindir = get_option('bindir') +datadir = get_option('datadir') +libdir = get_option('libdir') +localedir = get_option('localedir') +includedir = get_option('includedir') +optimization = get_option('optimization') -pkglibdir = join_paths(libdir, meson.project_name()) -pkgdatadir = join_paths(datadir, meson.project_name()) +clapper_libdir = join_paths(prefix, libdir, clapper_api_name) +build_optimized = optimization in ['2', '3', 's'] -subdir('lib') +gst_dep = dependency('gstreamer-1.0', + version: gst_req, + required: false, +) +gst_base_dep = dependency('gstreamer-base-1.0', + version: gst_req, + required: false, +) +gst_video_dep = dependency('gstreamer-video-1.0', + version: gst_req, + required: false, +) +gst_audio_dep = dependency('gstreamer-audio-1.0', + version: gst_req, + required: false, +) +gst_pbutils_dep = dependency('gstreamer-pbutils-1.0', + version: gst_req, + required: false, +) +gst_tag_dep = dependency('gstreamer-tag-1.0', + version: gst_req, + required: false, +) +glib_dep = dependency('glib-2.0', + version: glib_req, + required: false, +) +gobject_dep = dependency('gobject-2.0', + version: glib_req, + required: false, +) +gio_dep = dependency('gio-2.0', + version: glib_req, + required: false, +) +gmodule_dep = dependency('gmodule-2.0', + version: glib_req, + required: false, +) +gtk4_dep = dependency('gtk4', + version: gtk4_req, + required: false, +) -if get_option('player') - subdir('bin') - subdir('data') - subdir('po') +cc = meson.get_compiler('c') +libm = cc.find_library('m', required: false) - install_subdir('src', install_dir: pkgdatadir) - install_subdir('extras', install_dir: pkgdatadir) - install_subdir('css', install_dir: pkgdatadir) - install_subdir('ui', install_dir: pkgdatadir) +warning_flags = [ + '-Wmissing-declarations', + '-Wredundant-decls', + '-Wwrite-strings', + '-Wformat', + '-Wformat-security', + '-Winit-self', + '-Wmissing-include-dirs', + '-Waddress', + '-Wno-multichar', + '-Wvla', + '-Wpointer-arith', + '-Wmissing-prototypes', + '-Wdeclaration-after-statement', + '-Wold-style-definition', + '-Wsign-compare', +] - python_bin = python.find_installation('python3') - if not python_bin.found() - error('No valid python3 binary found') +foreach extra_arg : warning_flags + if cc.has_argument (extra_arg) + add_project_arguments([extra_arg], language: 'c') endif - meson.add_install_script('build-aux/meson/postinstall.py') +endforeach + +if build_optimized + message('Disabling GLib cast checks') + add_project_arguments('-DG_DISABLE_CAST_CHECKS', language: 'c') + + message('Disabling GLib asserts') + add_project_arguments('-DG_DISABLE_ASSERT', language: 'c') + + message('Disabling GLib checks') + add_project_arguments('-DG_DISABLE_CHECKS', language: 'c') endif + +subdir('src') +subdir('doc') + +summary({ + 'prefix': prefix, + 'bindir': bindir, + 'datadir': datadir, + 'libdir': libdir, + 'localedir': localedir, + 'includedir': includedir, + 'optimization': optimization, +}, section: 'Directories') + +summary('clapper', build_clapper ? 'Yes' : 'No', section: 'Build') +summary('gst-plugin', build_gst_plugin ? 'Yes' : 'No', section: 'Build') +summary('introspection', build_gir ? 'Yes' : 'No', section: 'Build') +summary('vapi', build_vapi ? 'Yes' : 'No', section: 'Build') +summary('doc', build_doc ? 'Yes' : 'No', section: 'Build') + +foreach name : clapper_possible_features + summary(name, clapper_available_features.contains(name) ? 'Yes' : 'No', section: 'Features') +endforeach diff --git a/meson_options.txt b/meson_options.txt index b7509bd9..61d4e4d4 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,19 +1,48 @@ -option('player', - type: 'boolean', - value: true, - description: 'Build Clapper player' -) -option('lib', - type: 'boolean', - value: true, - description: 'Build GstClapper lib' +# Build +option('clapper', + type: 'feature', + value: 'auto', + description: 'Build Clapper library' ) option('gst-plugin', type: 'feature', value: 'auto', description: 'Build GStreamer plugin (includes GTK video sink element)' ) +option('introspection', + type: 'feature', + value: 'auto', + description: 'Build GObject Introspection data' +) +option('vapi', + type: 'feature', + value: 'auto', + description: 'Build Vala bindings' +) +option('doc', + type: 'boolean', + value: false, + description: 'Build documentation' +) +# Features +option('discoverer', + type: 'feature', + value: 'auto', + description: 'Build Clapper Discoverer feature' +) +option('mpris', + type: 'feature', + value: 'auto', + description: 'Build Clapper MPRIS feature' +) +option('server', + type: 'feature', + value: 'auto', + description: 'Build Clapper Server feature' +) + +# GStreamer plugin options option('glimporter', type: 'feature', value: 'auto', @@ -29,14 +58,3 @@ option('rawimporter', value: 'auto', description: 'Build RAW system memory importer for clappersink' ) - -option('devel-checks', - type: 'boolean', - value: false, - description: 'GStreamer GLib checks and asserts such as API guards (disable for stable releases)' -) -option('deprecated-glib-api', - type: 'boolean', - value: true, - description: 'Allow using of deprecated GLib API' -) diff --git a/src/actions.js b/src/actions.js deleted file mode 100644 index c399b137..00000000 --- a/src/actions.js +++ /dev/null @@ -1,111 +0,0 @@ -const { Gtk } = imports.gi; -const Dialogs = imports.src.dialogs; -const Prefs = imports.src.prefs; -const Misc = imports.src.misc; - -var actions = { - open_local: ['O'], - export_playlist: ['E'], - open_uri: ['U'], - prefs: ['comma'], - shortcuts: ['F1', 'question'], - about: null, - progress_forward: ['Right'], - progress_backward: ['Left'], - next_chapter: ['Right'], - prev_chapter: ['Left'], - next_track: ['Right'], - prev_track: ['Left'], - volume_up: ['Up'], - volume_down: ['Down'], - mute: ['M', 'M'], - toggle_play: ['space'], - change_repeat: ['R'], - reveal_controls: ['Return'], - toggle_fullscreen: ['F11', 'F'], - leave_fullscreen: ['Escape'], - quit: ['Q', 'Q'], -}; - -function handleAction(action, window) -{ - const clapperWidget = window.child; - if(!clapperWidget) return; - - const { player } = clapperWidget; - let bool = false; - - switch(action.name) { - case 'open_local': - case 'export_playlist': - new Dialogs.FileChooser(window, action.name); - break; - case 'open_uri': - new Dialogs.UriDialog(window); - break; - case 'prefs': - new Prefs.PrefsWindow(window); - break; - case 'shortcuts': - if(!window.get_help_overlay()) { - const helpBuilder = Misc.getBuilderForName('help-overlay.ui'); - window.set_help_overlay(helpBuilder.get_object('help_overlay')); - } - clapperWidget.activate_action('win.show-help-overlay', null); - break; - case 'about': - new Dialogs.AboutDialog(window); - break; - case 'progress_forward': - bool = true; - case 'progress_backward': - player.adjust_position(bool); - if( - clapperWidget.isReleaseKeyEnabled - && clapperWidget.isFullscreenMode - ) - clapperWidget.revealControls(); - /* Actual seek is handled on release */ - clapperWidget.isReleaseKeyEnabled = true; - if(!clapperWidget.has_focus) - clapperWidget.grab_focus(); - break; - case 'volume_up': - bool = true; - case 'volume_down': - player.adjust_volume(bool); - break; - case 'mute': - player.mute ^= true; - break; - case 'next_track': - player.playlistWidget.nextTrack(); - break; - case 'prev_track': - player.playlistWidget.prevTrack(); - break; - case 'reveal_controls': - if(clapperWidget.isFullscreenMode) - clapperWidget.revealControls(); - break; - case 'leave_fullscreen': - if(!clapperWidget.isFullscreenMode) - break; - case 'toggle_fullscreen': - clapperWidget.toggleFullscreen(); - break; - case 'change_repeat': - player.playlistWidget.changeRepeatMode(); - break; - case 'quit': - clapperWidget.activate_action('window.close', null); - break; - case 'toggle_play': - case 'next_chapter': - case 'prev_chapter': - player[action.name](); - break; - default: - break; - } -} diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 4f9e572a..00000000 --- a/src/app.js +++ /dev/null @@ -1,126 +0,0 @@ -const { Gio, GObject, Gtk } = imports.gi; -const { Widget } = imports.src.widget; -const Debug = imports.src.debug; -const FileOps = imports.src.fileOps; -const Misc = imports.src.misc; -const Actions = imports.src.actions; - -const { debug } = Debug; -const { settings } = Misc; - -var App = GObject.registerClass({ - GTypeName: 'ClapperApp', -}, -class ClapperApp extends Gtk.Application -{ - _init() - { - super._init({ - application_id: Misc.appId, - flags: Gio.ApplicationFlags.HANDLES_OPEN, - }); - - this.doneFirstActivate = false; - this.isFileAppend = false; - this.mapSignal = null; - } - - vfunc_open(files, hint) - { - super.vfunc_open(files, hint); - - this.activate(); - this._openFilesAsync(files).catch(debug); - } - - vfunc_activate() - { - super.vfunc_activate(); - - if(!this.doneFirstActivate) - this._onFirstActivate(); - - this.active_window.present(); - } - - async _openFilesAsync(files) - { - const urisArr = []; - - for(let file of files) { - const uri = file.get_uri(); - if(!uri.startsWith('file:')) { - urisArr.push(uri); - continue; - } - - /* If file is not a dir its URI will be returned in an array */ - const uris = await FileOps.getDirFilesUrisPromise(file).catch(debug); - if(uris && uris.length) - urisArr.push(...uris); - } - - const [playlist, subs] = Misc.parsePlaylistFiles(urisArr); - const { player } = this.active_window.get_child(); - const action = (this.isFileAppend) ? 'append' : 'set'; - - if(playlist && playlist.length) - player[`${action}_playlist`](playlist); - if(subs) - player.set_subtitles(subs); - - /* Restore default behavior */ - this.isFileAppend = false; - } - - _onFirstActivate() - { - const window = new Gtk.ApplicationWindow({ - application: this, - title: Misc.appName, - }); - - window.add_css_class('adwrounded'); - - if(!settings.get_boolean('render-shadows')) - window.add_css_class('gpufriendly'); - - window.add_css_class('gpufriendlyfs'); - - const clapperWidget = new Widget(); - const dummyHeaderbar = new Gtk.Box({ - can_focus: false, - focusable: false, - visible: false, - }); - - window.add_css_class('nobackground'); - window.set_child(clapperWidget); - window.set_titlebar(dummyHeaderbar); - - for(let name in Actions.actions) { - const simpleAction = new Gio.SimpleAction({ name }); - simpleAction.connect('activate', (action) => - Actions.handleAction(action, window) - ); - this.add_action(simpleAction); - - const accels = Actions.actions[name]; - if(accels) - this.set_accels_for_action(`app.${name}`, accels); - } - - this.mapSignal = window.connect('map', this._onWindowMap.bind(this)); - this.doneFirstActivate = true; - } - - _onWindowMap(window) - { - window.disconnect(this.mapSignal); - this.mapSignal = null; - - debug('window mapped'); - - window.child._onWindowMap(window); - } -}); diff --git a/src/buttons.js b/src/buttons.js deleted file mode 100644 index f6c5d30c..00000000 --- a/src/buttons.js +++ /dev/null @@ -1,260 +0,0 @@ -const { GObject, Gtk } = imports.gi; -const Misc = imports.src.misc; - -var CustomButton = GObject.registerClass({ - GTypeName: 'ClapperCustomButton', -}, -class ClapperCustomButton extends Gtk.Button -{ - _init(opts) - { - opts = opts || {}; - - const defaults = { - halign: Gtk.Align.CENTER, - valign: Gtk.Align.CENTER, - can_focus: false, - }; - Object.assign(opts, defaults); - - super._init(opts); - - this.add_css_class('flat'); - this.add_css_class('clappercontrolsbutton'); - } - - vfunc_clicked() - { - const clapperWidget = this.get_ancestor(Gtk.Grid); - - if(clapperWidget.isFullscreenMode) - clapperWidget.revealControls(); - } -}); - -var IconToggleButton = GObject.registerClass({ - GTypeName: 'ClapperIconToggleButton', -}, -class ClapperIconToggleButton extends CustomButton -{ - _init(primaryIcon, secondaryIcon) - { - super._init({ - icon_name: primaryIcon, - }); - - this.primaryIcon = primaryIcon; - this.secondaryIcon = secondaryIcon; - } - - setPrimaryIcon() - { - this.icon_name = this.primaryIcon; - } - - setSecondaryIcon() - { - this.icon_name = this.secondaryIcon; - } -}); - -var PopoverSeparator = GObject.registerClass({ - GTypeName: 'ClapperPopoverSeparator', - Template: Misc.getResourceUri('ui/popover-separator.ui'), - InternalChildren: ['middle_label'], - Properties: { - 'label': GObject.ParamSpec.string( - 'label', - 'Middle label', - 'Text to set in the middle', - GObject.ParamFlags.WRITABLE, - null - ), - } -}, -class ClapperPopoverSeparator extends Gtk.Box -{ - _init(opts) - { - super._init(); - - if(!opts.label) - this.visible = false; - - this.label = opts.label; - } - - set label(value) - { - this._middle_label.label = value || ""; - - if(value) - this.visible = true; - } -}); - -var PopoverButtonBase = GObject.registerClass({ - GTypeName: 'ClapperPopoverButtonBase', -}, -class ClapperPopoverButtonBase extends Gtk.MenuButton -{ - _init(opts = {}) - { - super._init(opts); - - if(opts.icon_name) - this.icon_name = opts.icon_name; - else if(opts.label) - this.label = opts.label; - - this.toggleButton = this.get_first_child(); - this.toggleButton.add_css_class('clappercontrolsbutton'); - - this.set_create_popup_func(this._onPopoverOpened); - this.popover.connect('closed', this._onPopoverClosed.bind(this)); - } - - _onPopoverOpened(self) - { - const clapperWidget = self.get_ancestor(Gtk.Grid); - - if(clapperWidget.isFullscreenMode) { - clapperWidget.revealControls(); - clapperWidget.isPopoverOpen = true; - } - } - - _onPopoverClosed(popover) - { - const clapperWidget = this.get_ancestor(Gtk.Grid); - - /* Set again timeout as popover is now closed */ - if(clapperWidget.isFullscreenMode) - clapperWidget.revealControls(); - - clapperWidget.isPopoverOpen = false; - } -}); - -var ElapsedTimeButton = GObject.registerClass({ - GTypeName: 'ClapperElapsedTimeButton', - Template: Misc.getResourceUri('ui/elapsed-time-button.ui'), - Children: ['scrolledWindow', 'speedScale'], -}, -class ClapperElapsedTimeButton extends PopoverButtonBase -{ - _init(opts) - { - super._init(opts); - - this.setInitialState(); - this.popover.add_css_class('elapsedpopover'); - - this.scrolledWindow.max_content_height = 150; - } - - set label(value) - { - this.toggleButton.label = value; - } - - get label() - { - return this.toggleButton.label; - } - - setInitialState() - { - this.label = `00${Misc.timeColon}00∕00${Misc.timeColon}00`; - } - - setFullscreenMode(isFullscreen, isMobileMonitor) - { - this.scrolledWindow.max_content_height = (isFullscreen && !isMobileMonitor) - ? 190 : 150; - } -}); - -var TrackSelectButton = GObject.registerClass({ - GTypeName: 'ClapperTrackSelectButton', - Template: Misc.getResourceUri('ui/track-select-button.ui'), - Children: ['popoverBox'], - InternalChildren: ['scrolled_window', 'decoder_separator'], -}, -class ClapperTrackSelectButton extends PopoverButtonBase -{ - _init(opts) - { - super._init(opts); - - this._scrolled_window.max_content_height = 220; - } - - setFullscreenMode(isFullscreen, isMobileMonitor) - { - this._scrolled_window.max_content_height = (isFullscreen && !isMobileMonitor) - ? 290 : 220; - } - - setDecoder(decoder) - { - this._decoder_separator.label = _('Decoder: %s').format(decoder); - } -}); - -var VolumeButton = GObject.registerClass({ - GTypeName: 'ClapperVolumeButton', - Template: Misc.getResourceUri('ui/volume-button.ui'), - Children: ['volumeScale'], - Properties: { - 'muted': GObject.ParamSpec.boolean( - 'muted', - 'Set muted', - 'Mark scale as muted', - GObject.ParamFlags.WRITABLE, - false - ), - } -}, -class ClapperVolumeButton extends PopoverButtonBase -{ - _init(opts) - { - super._init(opts); - this._isMuted = false; - } - - set muted(isMuted) - { - this._isMuted = isMuted; - this._onVolumeScaleValueChanged(this.volumeScale); - } - - _onVolumeScaleValueChanged(scale) - { - const volume = scale.get_value(); - const cssClass = 'overamp'; - const hasOveramp = (scale.has_css_class(cssClass)); - - if(volume > 1) { - if(!hasOveramp) - scale.add_css_class(cssClass); - } - else { - if(hasOveramp) - scale.remove_css_class(cssClass); - } - - const icon = (volume <= 0 || this._isMuted) - ? 'muted' - : (volume <= 0.3) - ? 'low' - : (volume <= 0.7) - ? 'medium' - : (volume <= 1) - ? 'high' - : 'overamplified'; - - this.icon_name = `audio-volume-${icon}-symbolic`; - } -}); diff --git a/src/controls.js b/src/controls.js deleted file mode 100644 index ce82e591..00000000 --- a/src/controls.js +++ /dev/null @@ -1,493 +0,0 @@ -const { GLib, GObject, Gdk, Gtk } = imports.gi; -const Buttons = imports.src.buttons; -const Debug = imports.src.debug; -const Misc = imports.src.misc; -const Revealers = imports.src.revealers; - -const { debug } = Debug; -const { settings } = Misc; - -var Controls = GObject.registerClass({ - GTypeName: 'ClapperControls', -}, -class ClapperControls extends Gtk.Box -{ - _init() - { - super._init({ - orientation: Gtk.Orientation.HORIZONTAL, - valign: Gtk.Align.END, - can_focus: false, - }); - - this.minFullViewWidth = 560; - - this.currentPosition = 0; - this.isPositionDragging = false; - this.isMobile = false; - - this.showHours = false; - this.durationFormatted = `00${Misc.timeColon}00`; - this.revealersArr = []; - this.chapters = null; - - this.chapterShowId = null; - this.chapterHideId = null; - - this.togglePlayButton = new Buttons.IconToggleButton( - 'play-symbolic', - 'pause-symbolic' - ); - this.togglePlayButton.connect( - 'clicked', this._onTogglePlayClicked.bind(this) - ); - this.append(this.togglePlayButton); - - const elapsedRevealer = new Revealers.ButtonsRevealer('SLIDE_RIGHT'); - this.elapsedButton = new Buttons.ElapsedTimeButton(); - elapsedRevealer.append(this.elapsedButton); - elapsedRevealer.reveal_child = true; - this.append(elapsedRevealer); - this.revealersArr.push(elapsedRevealer); - - this._addPositionScale(); - - const revealTracksButton = new Buttons.IconToggleButton( - 'go-previous-symbolic', - 'go-next-symbolic' - ); - revealTracksButton.add_css_class('narrowbutton'); - const tracksRevealer = new Revealers.ButtonsRevealer( - 'SLIDE_LEFT', revealTracksButton - ); - this.visualizationsButton = new Buttons.TrackSelectButton({ - icon_name: 'display-projector-symbolic', - visible: false, - }); - tracksRevealer.append(this.visualizationsButton); - this.videoTracksButton = new Buttons.TrackSelectButton({ - icon_name: 'emblem-videos-symbolic', - visible: false, - }); - tracksRevealer.append(this.videoTracksButton); - this.audioTracksButton = new Buttons.TrackSelectButton({ - icon_name: 'emblem-music-symbolic', - visible: false, - }); - tracksRevealer.append(this.audioTracksButton); - this.subtitleTracksButton = new Buttons.TrackSelectButton({ - icon_name: 'media-view-subtitles-symbolic', - visible: false, - }); - tracksRevealer.append(this.subtitleTracksButton); - - this.revealTracksRevealer = new Revealers.ButtonsRevealer('SLIDE_LEFT'); - this.revealTracksRevealer.append(revealTracksButton); - this.revealTracksRevealer.set_visible(false); - this.append(this.revealTracksRevealer); - - tracksRevealer.set_reveal_child(true); - this.revealersArr.push(tracksRevealer); - this.append(tracksRevealer); - - this.volumeButton = new Buttons.VolumeButton(); - this.append(this.volumeButton); - - this.unfullscreenButton = new Buttons.CustomButton({ - icon_name: 'view-restore-symbolic', - }); - this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this)); - this.unfullscreenButton.set_visible(false); - this.append(this.unfullscreenButton); - - this.add_css_class('clappercontrols'); - this.realizeSignal = this.connect('realize', this._onRealize.bind(this)); - } - - setFullscreenMode(isFullscreen, isMobileMonitor) - { - /* Allow recheck on next resize */ - this.isMobile = null; - - this.elapsedButton.setFullscreenMode(isFullscreen, isMobileMonitor); - this.visualizationsButton.setFullscreenMode(isFullscreen, isMobileMonitor); - this.videoTracksButton.setFullscreenMode(isFullscreen, isMobileMonitor); - this.audioTracksButton.setFullscreenMode(isFullscreen, isMobileMonitor); - this.subtitleTracksButton.setFullscreenMode(isFullscreen, isMobileMonitor); - - this.unfullscreenButton.visible = isFullscreen; - } - - setLiveMode(isLive, isSeekable) - { - if(isLive) - this.elapsedButton.label = 'LIVE'; - - this.positionScale.visible = isSeekable; - } - - setInitialState() - { - this.currentPosition = 0; - this.positionScale.set_value(0); - this.positionScale.visible = false; - - this.elapsedButton.setInitialState(); - this.togglePlayButton.setPrimaryIcon(); - - for(let type of ['video', 'audio', 'subtitle']) - this[`${type}TracksButton`].visible = false; - - this.visualizationsButton.visible = false; - } - - updateElapsedLabel(value) - { - value = value || 0; - - const elapsed = Misc.getFormattedTime(value, this.showHours) - + '∕' + this.durationFormatted; - - this.elapsedButton.label = elapsed; - } - - addCheckButtons(box, array, activeId) - { - let group = null; - let child = box.get_first_child(); - let i = 0; - - while(child || i < array.length) { - if(i >= array.length) { - if(child.visible) { - debug(`hiding unused ${child.type} checkButton nr: ${i}`); - child.visible = false; - } - i++; - child = child.get_next_sibling(); - continue; - } - - const el = array[i]; - let checkButton; - - if(child) { - checkButton = child; - debug(`reusing ${el.type} checkButton nr: ${i}`); - } - else { - debug(`creating new ${el.type} checkButton nr: ${i}`); - checkButton = new Gtk.CheckButton({ - group: group, - }); - checkButton.connect( - 'toggled', - this._onCheckButtonToggled.bind(this) - ); - box.append(checkButton); - } - - checkButton.label = el.label; - debug(`checkButton label: ${checkButton.label}`); - checkButton.type = el.type; - debug(`checkButton type: ${checkButton.type}`); - checkButton.activeId = el.activeId; - debug(`checkButton id: ${checkButton.activeId}`); - checkButton.visible = true; - - if(checkButton.activeId === activeId) { - checkButton.set_active(true); - debug(`activated ${el.type} checkButton nr: ${i}`); - } - if(!group) - group = checkButton; - - i++; - if(child) - child = child.get_next_sibling(); - } - } - - _handleTrackChange(checkButton) - { - const clapperWidget = this.get_ancestor(Gtk.Grid); - - if(checkButton.activeId < 0) { - return clapperWidget.player[ - `set_${checkButton.type}_track_enabled` - ](false); - } - - const setTrack = `set_${checkButton.type}_track`; - - clapperWidget.player[setTrack](checkButton.activeId); - clapperWidget.player[`${setTrack}_enabled`](true); - } - - _handleVisualizationChange(checkButton) - { - const clapperWidget = this.get_ancestor(Gtk.Grid); - const isEnabled = clapperWidget.player.get_visualization_enabled(); - - if(!checkButton.activeId) { - if(isEnabled) { - clapperWidget.player.set_visualization_enabled(false); - debug('disabled visualizations'); - } - return; - } - - const currVis = clapperWidget.player.get_current_visualization(); - - if(currVis === checkButton.activeId) - return; - - debug(`set visualization: ${checkButton.activeId}`); - clapperWidget.player.set_visualization(checkButton.activeId); - - if(!isEnabled) { - clapperWidget.player.set_visualization_enabled(true); - debug('enabled visualizations'); - } - } - - _addPositionScale() - { - this.positionScale = new Gtk.Scale({ - orientation: Gtk.Orientation.HORIZONTAL, - value_pos: Gtk.PositionType.LEFT, - draw_value: false, - hexpand: true, - valign: Gtk.Align.CENTER, - can_focus: false, - visible: false, - }); - const scrollController = new Gtk.EventControllerScroll(); - scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); - scrollController.connect('scroll', this._onPositionScaleScroll.bind(this)); - this.positionScale.add_controller(scrollController); - - this.positionScale.add_css_class('positionscale'); - this.positionScaleValueSignal = this.positionScale.connect( - 'value-changed', this._onPositionScaleValueChanged.bind(this) - ); - - /* GTK4 is missing pressed/released signals for GtkRange/GtkScale. - * We cannot add controllers, cause it already has them, so we - * workaround this by observing css classes it currently has */ - this.positionScaleDragSignal = this.positionScale.connect( - 'notify::css-classes', this._onPositionScaleDragging.bind(this) - ); - - this.positionAdjustment = this.positionScale.get_adjustment(); - this.positionAdjustment.set_page_increment(0); - this.positionAdjustment.set_step_increment(8); - - const chapterLabel = new Gtk.Label(); - chapterLabel.add_css_class('chapterlabel'); - - this.chapterPopover = new Gtk.Popover({ - position: Gtk.PositionType.TOP, - autohide: false, - child: chapterLabel, - }); - const box = new Gtk.Box({ - orientation: Gtk.Orientation.VERTICAL, - hexpand: true, - valign: Gtk.Align.CENTER, - can_focus: false, - }); - - box.append(this.chapterPopover); - box.append(this.positionScale); - this.append(box); - } - - _setChapterVisible(isVisible) - { - const type = (isVisible) ? 'Show' : 'Hide'; - const anti = (isVisible) ? 'Hide' : 'Show'; - - if(this[`chapter${anti}Id`]) { - GLib.source_remove(this[`chapter${anti}Id`]); - this[`chapter${anti}Id`] = null; - } - - if( - this[`chapter${type}Id`] - || (!isVisible && this.chapterPopover.visible === isVisible) - ) - return; - - debug(`changing chapter visibility to: ${isVisible}`); - - this[`chapter${type}Id`] = GLib.idle_add( - GLib.PRIORITY_DEFAULT_IDLE + 20, - () => { - if(isVisible) { - const [start, end] = this.positionScale.get_slider_range(); - const controlsHeight = this.parent.get_height(); - const scaleBoxHeight = this.positionScale.parent.get_height(); - const [isShared, destX, destY] = this.positionScale.translate_coordinates( - this.positionScale.parent, 0, 0 - ); - const clapperWidget = this.get_ancestor(Gtk.Grid); - - /* Half of slider width, values are defined in CSS */ - const sliderOffset = ( - clapperWidget.isFullscreenMode && !clapperWidget.isMobileMonitor - ) ? 10 : 9; - - this.chapterPopover.set_pointing_to(new Gdk.Rectangle({ - x: destX + end - sliderOffset, - y: -(controlsHeight - scaleBoxHeight) / 2, - width: 0, - height: 0, - })); - } - - this.chapterPopover.visible = isVisible; - this[`chapter${type}Id`] = null; - - debug(`chapter visible: ${isVisible}`); - - return GLib.SOURCE_REMOVE; - } - ); - } - - _onRealize() - { - this.disconnect(this.realizeSignal); - this.realizeSignal = null; - - const clapperWidget = this.get_ancestor(Gtk.Grid); - const scrollController = new Gtk.EventControllerScroll(); - scrollController.set_flags( - Gtk.EventControllerScrollFlags.VERTICAL - | Gtk.EventControllerScrollFlags.DISCRETE - ); - scrollController.connect('scroll', clapperWidget._onScroll.bind(clapperWidget)); - this.volumeButton.add_controller(scrollController); - - const initialVolume = (settings.get_boolean('volume-custom')) - ? settings.get_int('volume-value') / 100 - : settings.get_double('volume-last'); - - clapperWidget.player.volume = initialVolume; - clapperWidget.player.bind_property('mute', this.volumeButton, 'muted', - GObject.BindingFlags.DEFAULT - ); - } - - _onPlayerResize(width, height) - { - const isMobile = (width < this.minFullViewWidth); - if(this.isMobile === isMobile) - return; - - for(let revealer of this.revealersArr) - revealer.set_reveal_child(!isMobile); - - this.revealTracksRevealer.set_reveal_child(isMobile); - this.isMobile = isMobile; - } - - _onUnfullscreenClicked(button) - { - const root = button.get_root(); - root.unfullscreen(); - } - - _onCheckButtonToggled(checkButton) - { - if(!checkButton.get_active()) - return; - - switch(checkButton.type) { - case 'video': - case 'audio': - case 'subtitle': - this._handleTrackChange(checkButton); - break; - case 'visualization': - this._handleVisualizationChange(checkButton); - break; - default: - break; - } - } - - _onTogglePlayClicked() - { - /* Parent of controls changes, so get ancestor instead */ - const { player } = this.get_ancestor(Gtk.Grid); - player.toggle_play(); - } - - _onPositionScaleScroll(controller, dx, dy) - { - const clapperWidget = this.get_ancestor(Gtk.Grid); - clapperWidget._onScroll(controller, dx || dy, 0); - } - - _onPositionScaleValueChanged(scale) - { - const scaleValue = scale.get_value(); - const positionSeconds = Math.round(scaleValue); - - this.currentPosition = positionSeconds; - this.updateElapsedLabel(positionSeconds); - - if(this.chapters && this.isPositionDragging) { - const chapter = this.chapters[scaleValue]; - const isChapter = (chapter != null); - - if(isChapter) - this.chapterPopover.child.label = chapter; - - this._setChapterVisible(isChapter); - } - } - - _onPositionScaleDragging(scale) - { - const isPositionDragging = scale.has_css_class('dragging'); - - if(this.isPositionDragging === isPositionDragging) - return; - - const clapperWidget = this.get_ancestor(Gtk.Grid); - if(!clapperWidget) return; - - if(clapperWidget.isFullscreenMode) { - clapperWidget.revealControls(); - - if(isPositionDragging) - clapperWidget._clearTimeout('hideControls'); - } - - if((this.isPositionDragging = isPositionDragging)) - return; - - const scaleValue = scale.get_value(); - const isChapterSeek = this.chapterPopover.visible; - - if(!isChapterSeek) { - const positionSeconds = Math.round(scaleValue); - clapperWidget.player.seek_seconds(positionSeconds); - } - else { - clapperWidget.player.seek_chapter(scaleValue); - this._setChapterVisible(false); - } - } - - _onCloseRequest() - { - debug('controls close request'); - - this.positionScale.disconnect(this.positionScaleValueSignal); - this.positionScale.disconnect(this.positionScaleDragSignal); - } -}); diff --git a/src/dbus.js b/src/dbus.js deleted file mode 100644 index fe0c71ce..00000000 --- a/src/dbus.js +++ /dev/null @@ -1,57 +0,0 @@ -const { Gio } = imports.gi; -const Debug = imports.src.debug; - -const { debug } = Debug; - -const ShellProxyWrapper = Gio.DBusProxy.makeProxyWrapper(` - - - - - - - - -` -); - -let shellProxy = null; - -debug('creating GNOME Shell DBus proxy'); -new ShellProxyWrapper( - Gio.DBus.session, - 'org.gnome.Shell', - '/org/gnome/Shell', - (proxy, err) => { - if(err) { - debug(err); - - return; - } - shellProxy = proxy; - debug('GNOME Shell DBus proxy is ready'); - }, - null, - Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION - | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS -); - -function shellWindowEval(fn, isEnabled) -{ - if(!shellProxy) - return; - - const un = (isEnabled) ? '' : 'un'; - - debug(`changing ${fn}`); - shellProxy.EvalRemote( - `global.display.focus_window.${un}${fn}()`, - (out) => { - const debugMsg = (out[0]) - ? `window ${fn}: ${isEnabled}` - : new Error(out[1]); - - debug(debugMsg); - } - ); -} diff --git a/src/debug.js b/src/debug.js deleted file mode 100644 index 751e6185..00000000 --- a/src/debug.js +++ /dev/null @@ -1,55 +0,0 @@ -const { GLib } = imports.gi; -const { Debug } = imports.extras.debug; -const { Ink } = imports.extras.ink; - -const G_DEBUG_ENV = GLib.getenv('G_MESSAGES_DEBUG'); - -const clapperDebugger = new Debug.Debugger('Clapper', { - name_printer: new Ink.Printer({ - font: Ink.Font.BOLD, - color: Ink.Color.MAGENTA - }), - time_printer: new Ink.Printer({ - color: Ink.Color.ORANGE - }), - high_precision: true, -}); -clapperDebugger.enabled = ( - clapperDebugger.enabled - || G_DEBUG_ENV != null - && G_DEBUG_ENV.includes('Clapper') -); - -function _logStructured(debuggerName, msg, level) -{ - GLib.log_structured( - debuggerName, level, { - MESSAGE: msg, - SYSLOG_IDENTIFIER: debuggerName.toLowerCase() - }); -} - -function _debug(debuggerName, msg) -{ - if(msg.message) { - _logStructured( - debuggerName, - msg.message, - GLib.LogLevelFlags.LEVEL_CRITICAL - ); - - return; - } - - clapperDebugger.debug(msg); -} - -function debug(msg) -{ - _debug('Clapper', msg); -} - -function warn(msg) -{ - _logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_WARNING); -} diff --git a/src/dialogs.js b/src/dialogs.js deleted file mode 100644 index a113ccb1..00000000 --- a/src/dialogs.js +++ /dev/null @@ -1,314 +0,0 @@ -const { Adw, Gdk, Gio, GObject, Gst, Gtk } = imports.gi; -const System = imports.system; -const Debug = imports.src.debug; -const FileOps = imports.src.fileOps; -const Misc = imports.src.misc; - -const { debug } = Debug; - -var FileChooser = GObject.registerClass({ - GTypeName: 'ClapperFileChooser', -}, -class ClapperFileChooser extends Gtk.FileChooserNative -{ - _init(window, purpose) - { - super._init({ - transient_for: window, - modal: true, - }); - - switch(purpose) { - case 'open_local': - this._prepareOpenLocal(); - break; - case 'export_playlist': - this._prepareExportPlaylist(); - break; - default: - debug(new Error(`unknown file chooser purpose: ${purpose}`)); - break; - } - - this.chooserPurpose = purpose; - - /* File chooser closes itself when nobody is holding its ref */ - this.ref(); - this.show(); - } - - _prepareOpenLocal() - { - this.select_multiple = true; - - const filter = new Gtk.FileFilter({ - name: 'Media Files', - }); - filter.add_mime_type('video/*'); - filter.add_mime_type('audio/*'); - filter.add_mime_type('application/claps'); - Misc.subsMimes.forEach(mime => filter.add_mime_type(mime)); - this.add_filter(filter); - } - - _prepareExportPlaylist() - { - this.action = Gtk.FileChooserAction.SAVE; - this.set_current_name('playlist.claps'); - - const filter = new Gtk.FileFilter({ - name: 'Playlist Files', - }); - filter.add_mime_type('application/claps'); - this.add_filter(filter); - } - - vfunc_response(respId) - { - debug('closing file chooser dialog'); - - if(respId === Gtk.ResponseType.ACCEPT) { - switch(this.chooserPurpose) { - case 'open_local': - this._handleOpenLocal(); - break; - case 'export_playlist': - this._handleExportPlaylist(); - break; - } - } - - this.unref(); - this.destroy(); - } - - _handleOpenLocal() - { - const files = this.get_files(); - const filesArray = []; - - let index = 0; - let file; - - while((file = files.get_item(index))) { - filesArray.push(file); - index++; - } - - const { application } = this.transient_for; - const isHandlesOpen = Boolean( - application.flags & Gio.ApplicationFlags.HANDLES_OPEN - ); - - /* Remote app does not handle open */ - if(isHandlesOpen) - application.open(filesArray, ""); - else - application._openFilesAsync(filesArray); - } - - _handleExportPlaylist() - { - const file = this.get_file(); - const { playlistWidget } = this.transient_for.child.player; - const playlist = playlistWidget.getPlaylist(true); - - FileOps.saveFileSimplePromise(file, playlist.join('\n')) - .then(() => { - debug(`exported playlist to file: ${file.get_path()}`); - }) - .catch(err => { - debug(err); - }); - } -}); - -var UriDialog = GObject.registerClass({ - GTypeName: 'ClapperUriDialog', -}, -class ClapperUriDialog extends Gtk.Dialog -{ - _init(window) - { - super._init({ - transient_for: window, - modal: true, - use_header_bar: true, - title: _('Open URI'), - default_width: 420, - }); - - const contentBox = this.get_content_area(); - contentBox.vexpand = true; - contentBox.add_css_class('uridialogbox'); - - const linkEntry = new Gtk.Entry({ - activates_default: true, - truncate_multiline: true, - height_request: 38, - hexpand: true, - valign: Gtk.Align.CENTER, - input_purpose: Gtk.InputPurpose.URL, - placeholder_text: _('Enter or drop URI here'), - }); - linkEntry.connect('notify::text', this._onTextNotify.bind(this)); - contentBox.append(linkEntry); - - this.add_button(_('Cancel'), Gtk.ResponseType.CANCEL); - this.add_button(_('Open'), Gtk.ResponseType.OK); - - this.set_default_response(Gtk.ResponseType.OK); - this.set_response_sensitive(Gtk.ResponseType.OK, false); - - const display = Gdk.Display.get_default(); - const clipboard = (display) ? display.get_clipboard() : null; - - if(clipboard) - clipboard.read_text_async(null, this._readTextAsyncCb.bind(this)); - - this.show(); - } - - vfunc_response(respId) - { - if(respId === Gtk.ResponseType.OK) { - const contentBox = this.get_content_area(); - const linkEntry = contentBox.get_last_child(); - const { player } = this.transient_for.child; - - player.set_playlist([linkEntry.text]); - } - - this.destroy(); - } - - _onTextNotify(entry) - { - const isUriValid = (entry.text.length) - ? Gst.uri_is_valid(entry.text) - : false; - - this.set_response_sensitive(Gtk.ResponseType.OK, isUriValid); - } - - _readTextAsyncCb(clipboard, result) - { - let uri = null; - - try { - uri = clipboard.read_text_finish(result); - } - catch(err) { - debug(`could not read clipboard: ${err.message}`); - } - - if(!uri || !Gst.uri_is_valid(uri)) - return; - - const contentBox = this.get_content_area(); - const linkEntry = contentBox.get_last_child(); - - linkEntry.set_text(uri); - linkEntry.select_region(0, -1); - } -}); - -var ResumeDialog = GObject.registerClass({ - GTypeName: 'ClapperResumeDialog', -}, -class ClapperResumeDialog extends Gtk.MessageDialog -{ - _init(window, resumeInfo) - { - const percentage = Math.round((resumeInfo.time / resumeInfo.duration) * 100); - - const msg = [ - '' + _('Title') + `: ${resumeInfo.title}`, - '' + _('Completed') + `: ${percentage}%` - ].join('\n'); - - super._init({ - transient_for: window, - modal: true, - message_type: Gtk.MessageType.QUESTION, - buttons: Gtk.ButtonsType.YES_NO, - text: _('Resume playback?'), - secondary_use_markup: true, - secondary_text: msg, - }); - - this.resumeInfo = resumeInfo; - this.set_default_response(Gtk.ResponseType.YES); - - this.show(); - } - - vfunc_response(respId) - { - const { player } = this.transient_for.child; - - if(respId === Gtk.ResponseType.YES) - player.seek_seconds(this.resumeInfo.time); - - this.destroy(); - } -}); - -var AboutDialog = GObject.registerClass({ - GTypeName: 'ClapperAboutDialog', -}, -class ClapperAboutDialog extends Gtk.AboutDialog -{ - _init(window) - { - const gstVer = [ - Gst.VERSION_MAJOR, Gst.VERSION_MINOR, Gst.VERSION_MICRO - ].join('.'); - - const gtkVer = [ - Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION - ].join('.'); - - /* TODO: This is as of Alpha2 still broken, requires: - * https://gitlab.gnome.org/GNOME/libadwaita/-/merge_requests/230 - * can be simplified later in future */ - const adwVer = Adw.MAJOR_VERSION ? [ - Adw.MAJOR_VERSION, Adw.MINOR_VERSION, Adw.MICRO_VERSION - ].join('.') : '1.0.0'; - - const gjsVerStr = String(System.version); - let gjsVer = ''; - - gjsVer += gjsVerStr.charAt(0) + '.'; - gjsVer += gjsVerStr.charAt(1) + gjsVerStr.charAt(2) + '.'; - if(gjsVerStr.charAt(3) !== '0') - gjsVer += gjsVerStr.charAt(3); - gjsVer += gjsVerStr.charAt(4); - - const osInfo = [ - _('GTK version: %s').format(gtkVer), - _('Adwaita version: %s').format(adwVer), - _('GStreamer version: %s').format(gstVer), - _('GJS version: %s').format(gjsVer) - ].join('\n'); - - super._init({ - transient_for: window, - destroy_with_parent: true, - modal: true, - program_name: Misc.appName, - comments: _('A GNOME media player powered by GStreamer'), - version: pkg.version, - authors: ['Rafał Dzięgiel'], - artists: ['Rafał Dzięgiel'], - /* TRANSLATORS: Put your name(s) here for credits or leave untranslated */ - translator_credits: _('translator-credits'), - license_type: Gtk.License.GPL_3_0, - logo_icon_name: 'com.github.rafostar.Clapper', - website: 'https://rafostar.github.io/clapper', - system_information: osInfo, - }); - - this.show(); - } -}); diff --git a/src/fileOps.js b/src/fileOps.js deleted file mode 100644 index b3c3d7b7..00000000 --- a/src/fileOps.js +++ /dev/null @@ -1,227 +0,0 @@ -const { Gio, GLib } = imports.gi; -const ByteArray = imports.byteArray; -const Debug = imports.src.debug; -const Misc = imports.src.misc; - -const { debug } = Debug; - -/* FIXME: Use Gio._LocalFilePrototype once we are safe to assume - * that GJS with https://gitlab.gnome.org/GNOME/gjs/-/commit/ec9385b8 is used. */ -const LocalFilePrototype = Gio.File.new_for_path('/').constructor.prototype; - -Gio._promisify(LocalFilePrototype, 'load_bytes_async', 'load_bytes_finish'); -Gio._promisify(LocalFilePrototype, 'make_directory_async', 'make_directory_finish'); -Gio._promisify(LocalFilePrototype, 'replace_contents_bytes_async', 'replace_contents_finish'); -Gio._promisify(LocalFilePrototype, 'query_info_async', 'query_info_finish'); -Gio._promisify(LocalFilePrototype, 'enumerate_children_async', 'enumerate_children_finish'); - -Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish'); -Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish'); - -function createCacheDirPromise() -{ - const dir = Gio.File.new_for_path( - GLib.get_user_cache_dir() + '/' + Misc.appId - ); - - return createDirPromise(dir); -} - -function createTempDirPromise() -{ - const dir = Gio.File.new_for_path( - GLib.get_tmp_dir() + '/' + Misc.appId - ); - - return createDirPromise(dir); -} - -/* Creates dir and resolves with it */ -function createDirPromise(dir) -{ - return new Promise((resolve, reject) => { - if(dir.query_exists(null)) - return resolve(dir); - - dir.make_directory_async( - GLib.PRIORITY_DEFAULT, - null - ) - .then(success => { - if(success) - return resolve(dir); - - reject(new Error(`could not create dir: ${dir.get_path()}`)); - }) - .catch(err => reject(err)); - }); -} - -/* Simple save data to GioFile */ -function saveFileSimplePromise(file, data) -{ - return file.replace_contents_bytes_async( - GLib.Bytes.new_take(data), - null, - false, - Gio.FileCreateFlags.NONE, - null - ); -} - -/* Saves file in optional subdirectory and resolves with it */ -function saveFilePromise(place, subdirName, fileName, data) -{ - return new Promise(async (resolve, reject) => { - let folderPath = GLib[`get_${place}_dir`]() + '/' + Misc.appId; - - if(subdirName) - folderPath += `/${subdirName}`; - - const destDir = Gio.File.new_for_path(folderPath); - const destPath = folderPath + '/' + fileName; - - debug(`saving file: ${destPath}`); - - const checkFolders = (subdirName) - ? [destDir.get_parent(), destDir] - : [destDir]; - - for(let dir of checkFolders) { - const createdDir = await createDirPromise(dir).catch(debug); - if(!createdDir) - return reject(new Error(`could not create dir: ${dir.get_path()}`)); - } - - const destFile = destDir.get_child(fileName); - saveFileSimplePromise(destFile, data) - .then(() => { - debug(`saved file: ${destPath}`); - resolve(destFile); - }) - .catch(err => reject(err)); - }); -} - -function getFileContentsPromise(place, subdirName, fileName) -{ - return new Promise((resolve, reject) => { - let destPath = GLib[`get_${place}_dir`]() + '/' + Misc.appId; - - if(subdirName) - destPath += `/${subdirName}`; - - destPath += `/${fileName}`; - - const file = Gio.File.new_for_path(destPath); - debug(`reading data from: ${destPath}`); - - if(!file.query_exists(null)) { - debug(`no such file: ${file.get_path()}`); - return resolve(null); - } - - file.load_bytes_async(null) - .then(result => { - const data = result[0].get_data(); - if(!data || !data.length) - return reject(new Error('source file is empty')); - - debug(`read data from: ${destPath}`); - - if(data instanceof Uint8Array) - resolve(ByteArray.toString(data)); - else - resolve(data); - }) - .catch(err => reject(err)); - }); -} - -function _getDirUrisPromise(dir, isDeep) -{ - return new Promise(async (resolve, reject) => { - const enumerator = await dir.enumerate_children_async( - 'standard::name,standard::type', - Gio.FileQueryInfoFlags.NONE, - GLib.PRIORITY_DEFAULT, - null - ).catch(debug); - - if(!enumerator) - return reject(new Error('could not create file enumerator')); - - const dirPath = dir.get_path(); - const arr = []; - - debug(`enumerating files in dir: ${dirPath}`); - - while(true) { - const infos = await enumerator.next_files_async( - 1, - GLib.PRIORITY_DEFAULT, - null - ).catch(debug); - - if(!infos || !infos.length) - break; - - const fileUri = dir.get_uri() + '/' + infos[0].get_name(); - - if(infos[0].get_file_type() !== Gio.FileType.DIRECTORY) { - arr.push(fileUri); - continue; - } - if(!isDeep) - continue; - - const subDir = Misc.getFileFromLocalUri(fileUri); - const subDirUris = await _getDirUrisPromise(subDir, isDeep).catch(debug); - - if(subDirUris && subDirUris.length) - arr.push(...subDirUris); - } - - const isClosed = await enumerator.close_async( - GLib.PRIORITY_DEFAULT, - null - ).catch(debug); - - if(isClosed) - debug(`closed enumerator for dir: ${dirPath}`); - else - debug(new Error(`could not close file enumerator for dir: ${dirPath}`)); - - resolve(arr); - }); -} - -/* Either GioFile or URI for dir arg */ -function getDirFilesUrisPromise(dir, isDeep) -{ - return new Promise(async (resolve, reject) => { - if(!dir.get_path) - dir = Misc.getFileFromLocalUri(dir); - if(!dir) - return reject(new Error('invalid directory')); - - const fileInfo = await dir.query_info_async( - 'standard::type', - Gio.FileQueryInfoFlags.NONE, - GLib.PRIORITY_DEFAULT, - null - ).catch(debug); - - if(!fileInfo) - return reject(new Error('no file type info')); - - if(fileInfo.get_file_type() !== Gio.FileType.DIRECTORY) - return resolve([dir.get_uri()]); - - const arr = await _getDirUrisPromise(dir, isDeep).catch(debug); - if(!arr || !arr.length) - return reject(new Error('enumerated files list is empty')); - - resolve(arr.sort()); - }); -} diff --git a/src/headerbar.js b/src/headerbar.js deleted file mode 100644 index b1551ccf..00000000 --- a/src/headerbar.js +++ /dev/null @@ -1,299 +0,0 @@ -const { GObject, Gtk } = imports.gi; -const Debug = imports.src.debug; -const Misc = imports.src.misc; - -const { debug } = Debug; - -var HeaderBar = GObject.registerClass({ - GTypeName: 'ClapperHeaderBar', -}, -class ClapperHeaderBar extends Gtk.Box -{ - _init() - { - super._init({ - can_focus: false, - orientation: Gtk.Orientation.HORIZONTAL, - spacing: 6, - margin_top: 6, - margin_start: 6, - margin_end: 6, - }); - this.add_css_class('osdheaderbar'); - - this.isMaximized = false; - this.isMenuOnLeft = true; - - const uiBuilder = Misc.getBuilderForName('clapper.ui'); - - this.menuWidget = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - valign: Gtk.Align.CENTER, - spacing: 6, - }); - - this.menuButton = new Gtk.MenuButton({ - icon_name: 'open-menu-symbolic', - valign: Gtk.Align.CENTER, - can_focus: false, - }); - const menuToggleButton = this.menuButton.get_first_child(); - menuToggleButton.add_css_class('osd'); - const mainMenuModel = uiBuilder.get_object('mainMenu'); - const mainMenuPopover = new HeaderBarPopover(mainMenuModel); - this.menuButton.set_popover(mainMenuPopover); - this.menuButton.add_css_class('circular'); - this.menuWidget.append(this.menuButton); - - this.extraButtonsBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - valign: Gtk.Align.CENTER, - }); - this.extraButtonsBox.add_css_class('linked'); - - const floatButton = new Gtk.Button({ - icon_name: 'pip-in-symbolic', - can_focus: false, - }); - floatButton.add_css_class('osd'); - floatButton.add_css_class('circular'); - floatButton.add_css_class('linkedleft'); - floatButton.connect('clicked', - this._onFloatButtonClicked.bind(this) - ); - this.extraButtonsBox.append(floatButton); - - const separator = new Gtk.Separator({ - orientation: Gtk.Orientation.VERTICAL, - }); - separator.add_css_class('linkseparator'); - this.extraButtonsBox.append(separator); - - const fullscreenButton = new Gtk.Button({ - icon_name: 'view-fullscreen-symbolic', - can_focus: false, - }); - fullscreenButton.add_css_class('osd'); - fullscreenButton.add_css_class('circular'); - fullscreenButton.add_css_class('linkedright'); - fullscreenButton.connect('clicked', - this._onFullscreenButtonClicked.bind(this) - ); - this.extraButtonsBox.append(fullscreenButton); - this.menuWidget.append(this.extraButtonsBox); - - this.spacerWidget = new Gtk.Box({ - hexpand: true, - }); - - this.minimizeWidget = this._getWindowButton('minimize'); - this.maximizeWidget = this._getWindowButton('maximize'); - this.closeWidget = this._getWindowButton('close'); - - const gtkSettings = Gtk.Settings.get_default(); - this._onLayoutUpdate(gtkSettings); - - gtkSettings.connect( - 'notify::gtk-decoration-layout', - this._onLayoutUpdate.bind(this) - ); - } - - setMenuOnLeft(isOnLeft) - { - if(this.isMenuOnLeft === isOnLeft) - return; - - if(isOnLeft) { - this.menuWidget.reorder_child_after( - this.extraButtonsBox, this.menuButton - ); - } - else { - this.menuWidget.reorder_child_after( - this.menuButton, this.extraButtonsBox - ); - } - - this.isMenuOnLeft = isOnLeft; - } - - setMaximized(isMaximized) - { - if(this.isMaximized === isMaximized) - return; - - this.maximizeWidget.icon_name = (isMaximized) - ? 'window-restore-symbolic' - : 'window-maximize-symbolic'; - - this.isMaximized = isMaximized; - } - - _onLayoutUpdate(gtkSettings) - { - const gtkLayout = gtkSettings.gtk_decoration_layout; - - this._replaceButtons(gtkLayout); - } - - _replaceButtons(gtkLayout) - { - const modLayout = gtkLayout.replace(':', ',spacer,'); - const layoutArr = modLayout.split(','); - - let lastWidget = null; - - let showMinimize = false; - let showMaximize = false; - let showClose = false; - - let menuAdded = false; - let spacerAdded = false; - - debug(`headerbar layout: ${modLayout}`); - - for(let name of layoutArr) { - /* Menu might be named "appmenu" */ - if(!menuAdded && (!name || name === 'appmenu' || name === 'icon')) - name = 'menu'; - - const widget = this[`${name}Widget`]; - if(!widget) continue; - - if(!widget.parent) - this.append(widget); - else - this.reorder_child_after(widget, lastWidget); - - switch(name) { - case 'spacer': - spacerAdded = true; - break; - case 'minimize': - showMinimize = true; - break; - case 'maximize': - showMaximize = true; - break; - case 'close': - showClose = true; - break; - case 'menu': - this.setMenuOnLeft(!spacerAdded); - menuAdded = true; - break; - default: - break; - } - - lastWidget = widget; - } - - this.minimizeWidget.visible = showMinimize; - this.maximizeWidget.visible = showMaximize; - this.closeWidget.visible = showClose; - } - - _getWindowButton(name) - { - const button = new Gtk.Button({ - icon_name: `window-${name}-symbolic`, - valign: Gtk.Align.CENTER, - can_focus: false, - }); - button.add_css_class('osd'); - button.add_css_class('circular'); - - if(name === 'maximize') - name = 'toggle-maximized'; - - button.connect('clicked', - this._onWindowButtonActivate.bind(this, name) - ); - - return button; - } - - _updateFloatIcon(isFloating) - { - const floatButton = this.extraButtonsBox.get_first_child(); - if(!floatButton) return; - - const iconName = (isFloating) - ? 'pip-out-symbolic' - : 'pip-in-symbolic'; - - if(floatButton.icon_name !== iconName) - floatButton.icon_name = iconName; - } - - _onWindowButtonActivate(action) - { - this.activate_action(`window.${action}`, null); - } - - _onFloatButtonClicked(button) - { - const clapperWidget = this.root.child; - const { controlsRevealer } = clapperWidget; - - controlsRevealer.toggleReveal(); - - /* Reset timer to not disappear during click */ - clapperWidget._setHideControlsTimeout(); - - this._updateFloatIcon(!controlsRevealer.reveal_child); - } - - _onFullscreenButtonClicked(button) - { - this.root.fullscreen(); - } -}); - -var HeaderBarPopover = GObject.registerClass({ - GTypeName: 'ClapperHeaderBarPopover', -}, -class ClapperHeaderBarPopover extends Gtk.PopoverMenu -{ - _init(model) - { - super._init({ - menu_model: model, - }); - - this.connect('map', this._onMap.bind(this)); - this.connect('closed', this._onClosed.bind(this)); - } - - _onMap() - { - const { child } = this.root; - - if( - !child - || !child.player - || !child.player.widget - ) - return; - - child.revealControls(); - child.isPopoverOpen = true; - } - - _onClosed() - { - const { child } = this.root; - - if( - !child - || !child.player - || !child.player.widget - ) - return; - - child.revealControls(); - child.isPopoverOpen = false; - } -}); diff --git a/src/lib/clapper/clapper-app-bus-private.h b/src/lib/clapper/clapper-app-bus-private.h new file mode 100644 index 00000000..b64ac862 --- /dev/null +++ b/src/lib/clapper/clapper-app-bus-private.h @@ -0,0 +1,54 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_APP_BUS (clapper_app_bus_get_type()) +#define CLAPPER_APP_BUS_CAST(obj) ((ClapperAppBus *)(obj)) + +/** + * ClapperAppBus: + */ +G_DECLARE_FINAL_TYPE (ClapperAppBus, clapper_app_bus, CLAPPER, APP_BUS, GstBus) + +void clapper_app_bus_initialize (void); + +ClapperAppBus * clapper_app_bus_new (void); + +void clapper_app_bus_forward_message (ClapperAppBus *app_bus, GstMessage *msg); + +void clapper_app_bus_post_prop_notify (ClapperAppBus *app_bus, GstObject *src, GParamSpec *pspec); + +void clapper_app_bus_post_refresh_streams (ClapperAppBus *app_bus, GstObject *src); + +void clapper_app_bus_post_refresh_timeline (ClapperAppBus *app_bus, GstObject *src); + +void clapper_app_bus_post_simple_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id); + +void clapper_app_bus_post_desc_with_details_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id, const gchar *desc, const gchar *details); + +void clapper_app_bus_post_error_signal (ClapperAppBus *app_bus, GstObject *src, guint signal_id, GError *error, const gchar *debug_info); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-app-bus.c b/src/lib/clapper/clapper-app-bus.c new file mode 100644 index 00000000..ab505b35 --- /dev/null +++ b/src/lib/clapper/clapper-app-bus.c @@ -0,0 +1,307 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include + +#include "clapper-bus-private.h" +#include "clapper-app-bus-private.h" +#include "clapper-player-private.h" +#include "clapper-media-item-private.h" +#include "clapper-timeline-private.h" + +#define GST_CAT_DEFAULT clapper_app_bus_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperAppBus +{ + GstBus parent; +}; + +#define parent_class clapper_app_bus_parent_class +G_DEFINE_TYPE (ClapperAppBus, clapper_app_bus, GST_TYPE_BUS); + +enum +{ + CLAPPER_APP_BUS_STRUCTURE_UNKNOWN = 0, + CLAPPER_APP_BUS_STRUCTURE_PROP_NOTIFY, + CLAPPER_APP_BUS_STRUCTURE_REFRESH_STREAMS, + CLAPPER_APP_BUS_STRUCTURE_REFRESH_TIMELINE, + CLAPPER_APP_BUS_STRUCTURE_SIMPLE_SIGNAL, + CLAPPER_APP_BUS_STRUCTURE_DESC_WITH_DETAILS_SIGNAL, + CLAPPER_APP_BUS_STRUCTURE_ERROR_SIGNAL +}; + +static ClapperBusQuark _structure_quarks[] = { + {"unknown", 0}, + {"prop-notify", 0}, + {"refresh-streams", 0}, + {"refresh-timeline", 0}, + {"simple-signal", 0}, + {"desc-with-details-signal", 0}, + {"error-signal", 0}, + {NULL, 0} +}; + +enum +{ + CLAPPER_APP_BUS_FIELD_UNKNOWN = 0, + CLAPPER_APP_BUS_FIELD_PSPEC, + CLAPPER_APP_BUS_FIELD_SIGNAL_ID, + CLAPPER_APP_BUS_FIELD_DESC, + CLAPPER_APP_BUS_FIELD_DETAILS, + CLAPPER_APP_BUS_FIELD_ERROR, + CLAPPER_APP_BUS_FIELD_DEBUG_INFO +}; + +static ClapperBusQuark _field_quarks[] = { + {"unknown", 0}, + {"pspec", 0}, + {"signal-id", 0}, + {"desc", 0}, + {"details", 0}, + {"error", 0}, + {"debug-info", 0}, + {NULL, 0} +}; + +#define _STRUCTURE_QUARK(q) (_structure_quarks[CLAPPER_APP_BUS_STRUCTURE_##q].quark) +#define _FIELD_QUARK(q) (_field_quarks[CLAPPER_APP_BUS_FIELD_##q].quark) +#define _MESSAGE_SRC_GOBJECT(msg) ((GObject *) GST_MESSAGE_SRC (msg)) + +void +clapper_app_bus_initialize (void) +{ + gint i; + + for (i = 0; _structure_quarks[i].name; ++i) + _structure_quarks[i].quark = g_quark_from_static_string (_structure_quarks[i].name); + for (i = 0; _field_quarks[i].name; ++i) + _field_quarks[i].quark = g_quark_from_static_string (_field_quarks[i].name); +} + +void +clapper_app_bus_forward_message (ClapperAppBus *self, GstMessage *msg) +{ + gst_bus_post (GST_BUS_CAST (self), gst_message_ref (msg)); +} + +/* FIXME: It should be faster to wait for gst_message_new_property_notify() from + * playbin bus and forward them to app bus instead of connecting to notify + * signals of playbin, so change into using gst_message_new_property_notify() here too */ +void +clapper_app_bus_post_prop_notify (ClapperAppBus *self, + GstObject *src, GParamSpec *pspec) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (PROP_NOTIFY), + _FIELD_QUARK (PSPEC), G_TYPE_PARAM, pspec, + NULL); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_prop_notify_msg (GstMessage *msg, const GstStructure *structure) +{ + GParamSpec *pspec = NULL; + + gst_structure_id_get (structure, + _FIELD_QUARK (PSPEC), G_TYPE_PARAM, &pspec, + NULL); + g_object_notify_by_pspec (_MESSAGE_SRC_GOBJECT (msg), pspec); + + g_param_spec_unref (pspec); +} + +void +clapper_app_bus_post_refresh_streams (ClapperAppBus *self, GstObject *src) +{ + GstStructure *structure = gst_structure_new_id_empty (_STRUCTURE_QUARK (REFRESH_STREAMS)); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_refresh_streams_msg (GstMessage *msg, const GstStructure *structure) +{ + ClapperPlayer *player = CLAPPER_PLAYER_CAST (GST_MESSAGE_SRC (msg)); + clapper_player_refresh_streams (player); +} + +void +clapper_app_bus_post_refresh_timeline (ClapperAppBus *self, GstObject *src) +{ + GstStructure *structure = gst_structure_new_id_empty (_STRUCTURE_QUARK (REFRESH_TIMELINE)); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_refresh_timeline_msg (GstMessage *msg, const GstStructure *structure) +{ + ClapperMediaItem *item = CLAPPER_MEDIA_ITEM_CAST (GST_MESSAGE_SRC (msg)); + ClapperTimeline *timeline = clapper_media_item_get_timeline (item); + + clapper_timeline_refresh (timeline); +} + +void +clapper_app_bus_post_simple_signal (ClapperAppBus *self, GstObject *src, guint signal_id) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SIMPLE_SIGNAL), + _FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, signal_id, + NULL); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_simple_signal_msg (GstMessage *msg, const GstStructure *structure) +{ + guint signal_id = 0; + + gst_structure_id_get (structure, + _FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, &signal_id, + NULL); + g_signal_emit (_MESSAGE_SRC_GOBJECT (msg), signal_id, 0); +} + +void +clapper_app_bus_post_desc_with_details_signal (ClapperAppBus *self, + GstObject *src, guint signal_id, + const gchar *desc, const gchar *details) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (DESC_WITH_DETAILS_SIGNAL), + _FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, signal_id, + _FIELD_QUARK (DESC), G_TYPE_STRING, desc, + _FIELD_QUARK (DETAILS), G_TYPE_STRING, details, + NULL); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_desc_with_details_signal_msg (GstMessage *msg, const GstStructure *structure) +{ + guint signal_id = 0; + gchar *desc = NULL, *details = NULL; + + gst_structure_id_get (structure, + _FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, &signal_id, + _FIELD_QUARK (DESC), G_TYPE_STRING, &desc, + _FIELD_QUARK (DETAILS), G_TYPE_STRING, &details, + NULL); + g_signal_emit (_MESSAGE_SRC_GOBJECT (msg), signal_id, 0, desc, details); + + g_free (desc); + g_free (details); +} + +void +clapper_app_bus_post_error_signal (ClapperAppBus *self, + GstObject *src, guint signal_id, + GError *error, const gchar *debug_info) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (ERROR_SIGNAL), + _FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, signal_id, + _FIELD_QUARK (ERROR), G_TYPE_ERROR, error, + _FIELD_QUARK (DEBUG_INFO), G_TYPE_STRING, debug_info, + NULL); + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application (src, structure)); +} + +static inline void +_handle_error_signal_msg (GstMessage *msg, const GstStructure *structure) +{ + guint signal_id = 0; + GError *error = NULL; + gchar *debug_info = NULL; + + gst_structure_id_get (structure, + _FIELD_QUARK (SIGNAL_ID), G_TYPE_UINT, &signal_id, + _FIELD_QUARK (ERROR), G_TYPE_ERROR, &error, + _FIELD_QUARK (DEBUG_INFO), G_TYPE_STRING, &debug_info, + NULL); + g_signal_emit (_MESSAGE_SRC_GOBJECT (msg), signal_id, 0, error, debug_info); + + g_clear_error (&error); + g_free (debug_info); +} + +static gboolean +clapper_app_bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G_GNUC_UNUSED) +{ + if (G_LIKELY (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_APPLICATION)) { + const GstStructure *structure = gst_message_get_structure (msg); + GQuark quark = gst_structure_get_name_id (structure); + + if (quark == _STRUCTURE_QUARK (PROP_NOTIFY)) + _handle_prop_notify_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (REFRESH_STREAMS)) + _handle_refresh_streams_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (REFRESH_TIMELINE)) + _handle_refresh_timeline_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (SIMPLE_SIGNAL)) + _handle_simple_signal_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (ERROR_SIGNAL)) + _handle_error_signal_msg (msg, structure); + else if (quark == _STRUCTURE_QUARK (DESC_WITH_DETAILS_SIGNAL)) + _handle_desc_with_details_signal_msg (msg, structure); + } + + return G_SOURCE_CONTINUE; +} + +/* + * clapper_app_bus_new: + * + * Returns: (transfer full): a new #ClapperAppBus instance + */ +ClapperAppBus * +clapper_app_bus_new (void) +{ + GstBus *app_bus; + + app_bus = GST_BUS_CAST (g_object_new (CLAPPER_TYPE_APP_BUS, NULL)); + gst_object_ref_sink (app_bus); + + gst_bus_add_watch (app_bus, (GstBusFunc) clapper_app_bus_message_func, NULL); + + return CLAPPER_APP_BUS_CAST (app_bus); +} + +static void +clapper_app_bus_init (ClapperAppBus *self) +{ +} + +static void +clapper_app_bus_finalize (GObject *object) +{ + ClapperAppBus *self = CLAPPER_APP_BUS_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_app_bus_class_init (ClapperAppBusClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperappbus", 0, + "Clapper App Bus"); + + gobject_class->finalize = clapper_app_bus_finalize; +} diff --git a/src/lib/clapper/clapper-audio-stream-private.h b/src/lib/clapper/clapper-audio-stream-private.h new file mode 100644 index 00000000..e731e725 --- /dev/null +++ b/src/lib/clapper/clapper-audio-stream-private.h @@ -0,0 +1,32 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-audio-stream.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperStream * clapper_audio_stream_new (GstStream *gst_stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-audio-stream.c b/src/lib/clapper/clapper-audio-stream.c new file mode 100644 index 00000000..7041ac9e --- /dev/null +++ b/src/lib/clapper/clapper-audio-stream.c @@ -0,0 +1,429 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperAudioStream: + * + * Represents an audio stream within media. + */ + +#include + +#include "clapper-audio-stream-private.h" +#include "clapper-stream-private.h" + +#define GST_CAT_DEFAULT clapper_audio_stream_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperAudioStream +{ + ClapperStream parent; + + gchar *codec; + guint bitrate; + gchar *sample_format; + gint sample_rate; + gint channels; + gchar *lang_code; + gchar *lang_name; +}; + +#define parent_class clapper_audio_stream_parent_class +G_DEFINE_TYPE (ClapperAudioStream, clapper_audio_stream, CLAPPER_TYPE_STREAM); + +enum +{ + PROP_0, + PROP_CODEC, + PROP_BITRATE, + PROP_SAMPLE_FORMAT, + PROP_SAMPLE_RATE, + PROP_CHANNELS, + PROP_LANG_CODE, + PROP_LANG_NAME, + PROP_LAST +}; + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void +_update_using_caps (ClapperAudioStream *self, GstCaps *caps) +{ + ClapperStream *stream = CLAPPER_STREAM_CAST (self); + GstStructure *structure; + gint sample_rate = 0, channels = 0; + + if (gst_caps_get_size (caps) == 0) + return; + + structure = gst_caps_get_structure (caps, 0); + + clapper_stream_set_string_prop (stream, param_specs[PROP_SAMPLE_FORMAT], &self->sample_format, + gst_structure_get_string (structure, "format")); + + gst_structure_get_int (structure, "rate", &sample_rate); + clapper_stream_set_int_prop (stream, param_specs[PROP_SAMPLE_RATE], &self->sample_rate, sample_rate); + + gst_structure_get_int (structure, "channels", &channels); + clapper_stream_set_int_prop (stream, param_specs[PROP_CHANNELS], &self->channels, channels); +} + +static void +_update_using_tags (ClapperAudioStream *self, GstTagList *tags) +{ + ClapperStream *stream = CLAPPER_STREAM_CAST (self); + gchar *codec = NULL, *lang_code = NULL, *lang_name = NULL; + guint bitrate = 0; + + gst_tag_list_get_string_index (tags, GST_TAG_AUDIO_CODEC, 0, &codec); + clapper_stream_take_string_prop (stream, param_specs[PROP_CODEC], &self->codec, codec); + + gst_tag_list_get_uint_index (tags, GST_TAG_BITRATE, 0, &bitrate); + clapper_stream_set_uint_prop (stream, param_specs[PROP_BITRATE], &self->bitrate, bitrate); + + /* Prefer code (and name from it), fallback to extracted name */ + if (!gst_tag_list_get_string_index (tags, GST_TAG_LANGUAGE_CODE, 0, &lang_code)) + gst_tag_list_get_string_index (tags, GST_TAG_LANGUAGE_NAME, 0, &lang_name); + + clapper_stream_take_string_prop (stream, param_specs[PROP_LANG_CODE], &self->lang_code, lang_code); + clapper_stream_take_string_prop (stream, param_specs[PROP_LANG_NAME], &self->lang_name, lang_name); +} + +ClapperStream * +clapper_audio_stream_new (GstStream *gst_stream) +{ + ClapperAudioStream *audio_stream; + + audio_stream = g_object_new (CLAPPER_TYPE_AUDIO_STREAM, + "stream-type", CLAPPER_STREAM_TYPE_AUDIO, NULL); + gst_object_ref_sink (audio_stream); + + clapper_stream_set_gst_stream (CLAPPER_STREAM_CAST (audio_stream), gst_stream); + + return CLAPPER_STREAM_CAST (audio_stream); +} + +/** + * clapper_audio_stream_get_codec: + * @stream: a #ClapperAudioStream + * + * Get codec used to encode @stream. + * + * Returns: (transfer full) (nullable): the audio codec of stream + * or %NULL if undetermined. + */ +gchar * +clapper_audio_stream_get_codec (ClapperAudioStream *self) +{ + gchar *codec; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + codec = g_strdup (self->codec); + GST_OBJECT_UNLOCK (self); + + return codec; +} + +/** + * clapper_audio_stream_get_bitrate: + * @stream: a #ClapperAudioStream + * + * Get bitrate of audio @stream. + * + * Returns: the bitrate of audio stream. + */ +guint +clapper_audio_stream_get_bitrate (ClapperAudioStream *self) +{ + guint bitrate; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + bitrate = self->bitrate; + GST_OBJECT_UNLOCK (self); + + return bitrate; +} + +/** + * clapper_audio_stream_get_sample_format: + * @stream: a #ClapperAudioStream + * + * Get sample format of audio @stream. + * + * Returns: (transfer full) (nullable): the sample format of stream + * or %NULL if undetermined. + */ +gchar * +clapper_audio_stream_get_sample_format (ClapperAudioStream *self) +{ + gchar *sample_format; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + sample_format = g_strdup (self->sample_format); + GST_OBJECT_UNLOCK (self); + + return sample_format; +} + +/** + * clapper_audio_stream_get_sample_rate: + * @stream: a #ClapperAudioStream + * + * Get sample rate of audio @stream (in Hz). + * + * Returns: the sample rate of audio stream. + */ +gint +clapper_audio_stream_get_sample_rate (ClapperAudioStream *self) +{ + gint sample_rate; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + sample_rate = self->sample_rate; + GST_OBJECT_UNLOCK (self); + + return sample_rate; +} + +/** + * clapper_audio_stream_get_channels: + * @stream: a #ClapperAudioStream + * + * Get number of audio channels in @stream. + * + * Returns: the number of audio channels. + */ +gint +clapper_audio_stream_get_channels (ClapperAudioStream *self) +{ + gint channels; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + channels = self->channels; + GST_OBJECT_UNLOCK (self); + + return channels; +} + +/** + * clapper_audio_stream_get_lang_code: + * @stream: a #ClapperAudioStream + * + * Get an ISO-639 language code of the @stream. + * + * Returns: (transfer full) (nullable): the language code of audio stream. + */ +gchar * +clapper_audio_stream_get_lang_code (ClapperAudioStream *self) +{ + gchar *lang_code; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + lang_code = g_strdup (self->lang_code); + GST_OBJECT_UNLOCK (self); + + return lang_code; +} + +/** + * clapper_audio_stream_get_lang_name: + * @stream: a #ClapperAudioStream + * + * Get an ISO-639 language code of the @stream. + * + * Returns: (transfer full) (nullable): the language code of audio stream. + */ +gchar * +clapper_audio_stream_get_lang_name (ClapperAudioStream *self) +{ + gchar *lang_name = NULL; + + g_return_val_if_fail (CLAPPER_IS_AUDIO_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + + /* Prefer from code as its translated to user locale, + * otherwise try to fallback to the one sent in tags */ + if (self->lang_code) + lang_name = g_strdup (gst_tag_get_language_name (self->lang_code)); + if (!lang_name) + lang_name = g_strdup (self->lang_name); + + GST_OBJECT_UNLOCK (self); + + return lang_name; +} + +static void +clapper_audio_stream_init (ClapperAudioStream *self) +{ +} + +static void +clapper_audio_stream_finalize (GObject *object) +{ + ClapperAudioStream *self = CLAPPER_AUDIO_STREAM_CAST (object); + + g_free (self->codec); + g_free (self->sample_format); + g_free (self->lang_code); + g_free (self->lang_name); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_audio_stream_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperAudioStream *self = CLAPPER_AUDIO_STREAM_CAST (object); + + switch (prop_id) { + case PROP_CODEC: + g_value_take_string (value, clapper_audio_stream_get_codec (self)); + break; + case PROP_BITRATE: + g_value_set_uint (value, clapper_audio_stream_get_bitrate (self)); + break; + case PROP_SAMPLE_FORMAT: + g_value_take_string (value, clapper_audio_stream_get_sample_format (self)); + break; + case PROP_SAMPLE_RATE: + g_value_set_int (value, clapper_audio_stream_get_sample_rate (self)); + break; + case PROP_CHANNELS: + g_value_set_int (value, clapper_audio_stream_get_channels (self)); + break; + case PROP_LANG_CODE: + g_value_take_string (value, clapper_audio_stream_get_lang_code (self)); + break; + case PROP_LANG_NAME: + g_value_take_string (value, clapper_audio_stream_get_lang_name (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_audio_stream_internal_stream_updated (ClapperStream *stream, + GstCaps *caps, GstTagList *tags) +{ + ClapperAudioStream *self = CLAPPER_AUDIO_STREAM_CAST (stream); + + CLAPPER_STREAM_CLASS (parent_class)->internal_stream_updated (stream, caps, tags); + + if (caps) + _update_using_caps (self, caps); + if (tags) + _update_using_tags (self, tags); +} + +static void +clapper_audio_stream_class_init (ClapperAudioStreamClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperStreamClass *stream_class = (ClapperStreamClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperaudiostream", 0, + "Clapper Audio Stream"); + + gobject_class->get_property = clapper_audio_stream_get_property; + gobject_class->finalize = clapper_audio_stream_finalize; + + stream_class->internal_stream_updated = clapper_audio_stream_internal_stream_updated; + + /** + * ClapperAudioStream:codec: + * + * Stream codec. + */ + param_specs[PROP_CODEC] = g_param_spec_string ("codec", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperAudioStream:bitrate: + * + * Stream bitrate. + */ + param_specs[PROP_BITRATE] = g_param_spec_uint ("bitrate", + NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperAudioStream:sample-format: + * + * Stream sample format. + */ + param_specs[PROP_SAMPLE_FORMAT] = g_param_spec_string ("sample-format", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperAudioStream:sample-rate: + * + * Stream sample rate (in Hz). + */ + param_specs[PROP_SAMPLE_RATE] = g_param_spec_int ("sample-rate", + NULL, NULL, 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperAudioStream:channels: + * + * Stream number of audio channels. + */ + param_specs[PROP_CHANNELS] = g_param_spec_int ("channels", + NULL, NULL, 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperAudioStream:lang-code: + * + * Stream language code in ISO-639 format. + */ + param_specs[PROP_LANG_CODE] = g_param_spec_string ("lang-code", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperAudioStream:lang-name: + * + * Stream full language name determined from lang code. + */ + param_specs[PROP_LANG_NAME] = g_param_spec_string ("lang-name", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-audio-stream.h b/src/lib/clapper/clapper-audio-stream.h new file mode 100644 index 00000000..bde0f11a --- /dev/null +++ b/src/lib/clapper/clapper-audio-stream.h @@ -0,0 +1,51 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_AUDIO_STREAM (clapper_audio_stream_get_type()) +#define CLAPPER_AUDIO_STREAM_CAST(obj) ((ClapperAudioStream *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperAudioStream, clapper_audio_stream, CLAPPER, AUDIO_STREAM, ClapperStream) + +gchar * clapper_audio_stream_get_codec (ClapperAudioStream *stream); + +guint clapper_audio_stream_get_bitrate (ClapperAudioStream *stream); + +gchar * clapper_audio_stream_get_sample_format (ClapperAudioStream *stream); + +gint clapper_audio_stream_get_sample_rate (ClapperAudioStream *stream); + +gint clapper_audio_stream_get_channels (ClapperAudioStream *stream); + +gchar * clapper_audio_stream_get_lang_code (ClapperAudioStream *stream); + +gchar * clapper_audio_stream_get_lang_name (ClapperAudioStream *stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-bus-private.h b/src/lib/clapper/clapper-bus-private.h new file mode 100644 index 00000000..05bf8dcc --- /dev/null +++ b/src/lib/clapper/clapper-bus-private.h @@ -0,0 +1,32 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +typedef struct +{ + const gchar *name; + GQuark quark; +} ClapperBusQuark; + +G_END_DECLS diff --git a/src/lib/clapper/clapper-enums-private.h b/src/lib/clapper/clapper-enums-private.h new file mode 100644 index 00000000..36b454fe --- /dev/null +++ b/src/lib/clapper/clapper-enums-private.h @@ -0,0 +1,69 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +typedef enum +{ + CLAPPER_PLAYER_PLAY_FLAG_VIDEO = (1 << 0), + CLAPPER_PLAYER_PLAY_FLAG_AUDIO = (1 << 1), + CLAPPER_PLAYER_PLAY_FLAG_TEXT = (1 << 2), + CLAPPER_PLAYER_PLAY_FLAG_VIS = (1 << 3), + CLAPPER_PLAYER_PLAY_FLAG_SOFT_VOLUME = (1 << 4), + CLAPPER_PLAYER_PLAY_FLAG_NATIVE_AUDIO = (1 << 5), + CLAPPER_PLAYER_PLAY_FLAG_NATIVE_VIDEO = (1 << 6), + CLAPPER_PLAYER_PLAY_FLAG_DOWNLOAD = (1 << 7), + CLAPPER_PLAYER_PLAY_FLAG_BUFFERING = (1 << 8), + CLAPPER_PLAYER_PLAY_FLAG_DEINTERLACE = (1 << 9), + CLAPPER_PLAYER_PLAY_FLAG_SOFT_COLORBALANCE = (1 << 10), + CLAPPER_PLAYER_PLAY_FLAG_FORCE_FILTERS = (1 << 11), + CLAPPER_PLAYER_PLAY_FLAG_FORCE_SW_DECODERS = (1 << 12) +} ClapperPlayerPlayFlags; + +typedef enum +{ + CLAPPER_FEATURES_MANAGER_EVENT_UNKNOWN = 0, + CLAPPER_FEATURES_MANAGER_EVENT_FEATURE_ADDED, + CLAPPER_FEATURES_MANAGER_EVENT_FEATURE_PROPERTY_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_STATE_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_POSITION_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_SPEED_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_VOLUME_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_MUTE_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_PLAYED_ITEM_CHANGED, + CLAPPER_FEATURES_MANAGER_EVENT_ITEM_UPDATED, + CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_ADDED, + CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_REMOVED, + CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_REPOSITIONED, + CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_CLEARED, + CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_PROGRESSION_CHANGED +} ClapperFeaturesManagerEvent; + +typedef enum +{ + CLAPPER_QUEUE_ITEM_CHANGE_NORMAL = 1, + CLAPPER_QUEUE_ITEM_CHANGE_INSTANT = 2, + CLAPPER_QUEUE_ITEM_CHANGE_GAPLESS = 3, +} ClapperQueueItemChangeMode; + +G_END_DECLS diff --git a/src/lib/clapper/clapper-enums.h b/src/lib/clapper/clapper-enums.h new file mode 100644 index 00000000..a1576fcd --- /dev/null +++ b/src/lib/clapper/clapper-enums.h @@ -0,0 +1,130 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +/** + * ClapperPlayerState: + * @CLAPPER_PLAYER_STATE_STOPPED: Player is stopped. + * @CLAPPER_PLAYER_STATE_BUFFERING: Player is buffering. + * @CLAPPER_PLAYER_STATE_PAUSED: Player is paused. + * @CLAPPER_PLAYER_STATE_PLAYING: Player is playing. + */ +typedef enum +{ + CLAPPER_PLAYER_STATE_STOPPED = 0, + CLAPPER_PLAYER_STATE_BUFFERING, + CLAPPER_PLAYER_STATE_PAUSED, + CLAPPER_PLAYER_STATE_PLAYING, +} ClapperPlayerState; + +/** + * ClapperPlayerSeekMethod: + * @CLAPPER_PLAYER_SEEK_METHOD_ACCURATE: Seek to exact position (slow). + * @CLAPPER_PLAYER_SEEK_METHOD_NORMAL: Seek to approximated position. + * @CLAPPER_PLAYER_SEEK_METHOD_FAST: Seek to position of nearest keyframe (fast). + */ +typedef enum +{ + CLAPPER_PLAYER_SEEK_METHOD_ACCURATE = 0, + CLAPPER_PLAYER_SEEK_METHOD_NORMAL, + CLAPPER_PLAYER_SEEK_METHOD_FAST, +} ClapperPlayerSeekMethod; + +/** + * ClapperQueueProgressionMode: + * @CLAPPER_QUEUE_PROGRESSION_NONE: Queue will not change current item after playback finishes. + * @CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE: Queue selects items one after another until the end. + * When end of queue is reached, this mode will continue one another item is added to the queue, + * playing it if player autoplay property is set, otherwise current player state is kept. + * @CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM: Queue keeps repeating current media item. + * @CLAPPER_QUEUE_PROGRESSION_CAROUSEL: Queue starts from beginning after last media item. + * @CLAPPER_QUEUE_PROGRESSION_SHUFFLE: Queue selects a random media item after current one. + * Shuffle mode will avoid reselecting previously shuffled items as long as possible. + * After it runs out of unused items, shuffling begins anew. + */ +typedef enum +{ + CLAPPER_QUEUE_PROGRESSION_NONE = 0, + CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE, + CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM, + CLAPPER_QUEUE_PROGRESSION_CAROUSEL, + CLAPPER_QUEUE_PROGRESSION_SHUFFLE, +} ClapperQueueProgressionMode; + +/** + * ClapperMarkerType: + * @CLAPPER_MARKER_TYPE_UNKNOWN: Unknown marker type. + * @CLAPPER_MARKER_TYPE_TITLE: A title marker in timeline. + * @CLAPPER_MARKER_TYPE_CHAPTER: A chapter marker in timeline. + * @CLAPPER_MARKER_TYPE_TRACK: A track marker in timeline. + * @CLAPPER_STREAM_TYPE_CUSTOM_1: A custom marker 1 for free usage by application. + * @CLAPPER_STREAM_TYPE_CUSTOM_2: A custom marker 2 for free usage by application. + * @CLAPPER_STREAM_TYPE_CUSTOM_3: A custom marker 3 for free usage by application. + */ +typedef enum +{ + CLAPPER_MARKER_TYPE_UNKNOWN = 0, + CLAPPER_MARKER_TYPE_TITLE, + CLAPPER_MARKER_TYPE_CHAPTER, + CLAPPER_MARKER_TYPE_TRACK, + CLAPPER_MARKER_TYPE_CUSTOM_1 = 101, + CLAPPER_MARKER_TYPE_CUSTOM_2 = 102, + CLAPPER_MARKER_TYPE_CUSTOM_3 = 103, +} ClapperMarkerType; + +/** + * ClapperStreamType: + * @CLAPPER_STREAM_TYPE_UNKNOWN: Unknown stream type. + * @CLAPPER_STREAM_TYPE_VIDEO: Stream is a #ClapperVideoStream. + * @CLAPPER_STREAM_TYPE_AUDIO: Stream is a #ClapperAudioStream. + * @CLAPPER_STREAM_TYPE_SUBTITLE: Stream is a #ClapperSubtitleStream. + */ +typedef enum +{ + CLAPPER_STREAM_TYPE_UNKNOWN = 0, + CLAPPER_STREAM_TYPE_VIDEO, + CLAPPER_STREAM_TYPE_AUDIO, + CLAPPER_STREAM_TYPE_SUBTITLE, +} ClapperStreamType; + +/** + * ClapperDiscovererDiscoveryMode: + * @CLAPPER_DISCOVERER_DISCOVERY_ALWAYS: Run discovery for every single media item added to [class@Clapper.Queue]. + * This mode is useful when application presents a list of items to select from to the user before playback. + * It will scan every single item in queue, so user can have an updated list of items when selecting what to play. + * @CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT: Only run discovery on an item if it is not a currently selected item in [class@Clapper.Queue]. + * This mode is optimal when application always plays (or at least goes into paused) after selecting item from queue. + * It will skip discovery of such items since they will be discovered by [class@Clapper.Player] anyway. + */ +typedef enum +{ + CLAPPER_DISCOVERER_DISCOVERY_ALWAYS = 0, + CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT, +} ClapperDiscovererDiscoveryMode; + +G_END_DECLS diff --git a/src/lib/clapper/clapper-feature-private.h b/src/lib/clapper/clapper-feature-private.h new file mode 100644 index 00000000..9a398521 --- /dev/null +++ b/src/lib/clapper/clapper-feature-private.h @@ -0,0 +1,72 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +void clapper_feature_call_prepare (ClapperFeature *feature); + +G_GNUC_INTERNAL +void clapper_feature_call_unprepare (ClapperFeature *feature); + +G_GNUC_INTERNAL +void clapper_feature_call_property_changed (ClapperFeature *feature, GParamSpec *pspec); + +G_GNUC_INTERNAL +void clapper_feature_call_state_changed (ClapperFeature *feature, ClapperPlayerState state); + +G_GNUC_INTERNAL +void clapper_feature_call_position_changed (ClapperFeature *feature, gdouble position); + +G_GNUC_INTERNAL +void clapper_feature_call_speed_changed (ClapperFeature *feature, gdouble speed); + +G_GNUC_INTERNAL +void clapper_feature_call_volume_changed (ClapperFeature *feature, gdouble volume); + +G_GNUC_INTERNAL +void clapper_feature_call_mute_changed (ClapperFeature *feature, gboolean mute); + +G_GNUC_INTERNAL +void clapper_feature_call_played_item_changed (ClapperFeature *feature, ClapperMediaItem *item); + +G_GNUC_INTERNAL +void clapper_feature_call_item_updated (ClapperFeature *feature, ClapperMediaItem *item); + +G_GNUC_INTERNAL +void clapper_feature_call_queue_item_added (ClapperFeature *feature, ClapperMediaItem *item, guint index); + +G_GNUC_INTERNAL +void clapper_feature_call_queue_item_removed (ClapperFeature *feature, ClapperMediaItem *item, guint index); + +G_GNUC_INTERNAL +void clapper_feature_call_queue_item_repositioned (ClapperFeature *self, guint before, guint after); + +G_GNUC_INTERNAL +void clapper_feature_call_queue_cleared (ClapperFeature *feature); + +G_GNUC_INTERNAL +void clapper_feature_call_queue_progression_changed (ClapperFeature *feature, ClapperQueueProgressionMode mode); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-feature.c b/src/lib/clapper/clapper-feature.c new file mode 100644 index 00000000..fa0a0d7e --- /dev/null +++ b/src/lib/clapper/clapper-feature.c @@ -0,0 +1,225 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperFeature: + * + * A base class for creating new features for the player. + * + * Feature objects are meant for adding additional functionalities that + * are supposed to either act on playback/properties changes and/or change + * them themselves due to some external signal/event. + * + * For reacting to playback changes subclass should override this class + * virtual functions logic, while for controlling playback implementation + * may call [method@Gst.Object.get_parent] to acquire a weak reference on + * a parent [class@Clapper.Player] object feature was added to. + */ + +#include "clapper-feature.h" +#include "clapper-feature-private.h" +#include "clapper-player-private.h" + +#define GST_CAT_DEFAULT clapper_feature_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +typedef struct _ClapperFeaturePrivate ClapperFeaturePrivate; + +struct _ClapperFeaturePrivate +{ + gboolean prepared; +}; + +#define parent_class clapper_feature_parent_class +G_DEFINE_TYPE_WITH_PRIVATE (ClapperFeature, clapper_feature, GST_TYPE_OBJECT); + +#define CALL_WITH_ARGS(_feature,_vfunc,...) \ + ClapperFeaturePrivate *priv = clapper_feature_get_instance_private (_feature); \ + if (priv->prepared) { \ + ClapperFeatureClass *feature_class = CLAPPER_FEATURE_GET_CLASS (_feature); \ + if (feature_class->_vfunc) \ + feature_class->_vfunc (_feature, __VA_ARGS__); } + +#define CALL_WITHOUT_ARGS(_feature,_vfunc) \ + ClapperFeaturePrivate *priv = clapper_feature_get_instance_private (_feature); \ + if (priv->prepared) { \ + ClapperFeatureClass *feature_class = CLAPPER_FEATURE_GET_CLASS (_feature); \ + if (feature_class->_vfunc) \ + feature_class->_vfunc (_feature); } + +void +clapper_feature_call_prepare (ClapperFeature *self) +{ + ClapperFeaturePrivate *priv = clapper_feature_get_instance_private (self); + + if (!priv->prepared) { + ClapperFeatureClass *feature_class = CLAPPER_FEATURE_GET_CLASS (self); + gboolean prepared = TRUE; // mark subclass without prepare method as prepared + + if (feature_class->prepare) + prepared = feature_class->prepare (self); + + priv->prepared = prepared; + } +} + +void +clapper_feature_call_unprepare (ClapperFeature *self) +{ + ClapperFeaturePrivate *priv = clapper_feature_get_instance_private (self); + + if (priv->prepared) { + ClapperFeatureClass *feature_class = CLAPPER_FEATURE_GET_CLASS (self); + gboolean unprepared = TRUE; // mark subclass without unprepare method as unprepared + + if (feature_class->unprepare) + unprepared = feature_class->unprepare (self); + + priv->prepared = !unprepared; + } +} + +void +clapper_feature_call_property_changed (ClapperFeature *self, GParamSpec *pspec) +{ + CALL_WITH_ARGS (self, property_changed, pspec); +} + +void +clapper_feature_call_state_changed (ClapperFeature *self, ClapperPlayerState state) +{ + CALL_WITH_ARGS (self, state_changed, state); +} + +void +clapper_feature_call_position_changed (ClapperFeature *self, gdouble position) +{ + CALL_WITH_ARGS (self, position_changed, position); +} + +void +clapper_feature_call_speed_changed (ClapperFeature *self, gdouble speed) +{ + CALL_WITH_ARGS (self, speed_changed, speed); +} + +void +clapper_feature_call_volume_changed (ClapperFeature *self, gdouble volume) +{ + CALL_WITH_ARGS (self, volume_changed, volume); +} + +void +clapper_feature_call_mute_changed (ClapperFeature *self, gboolean mute) +{ + CALL_WITH_ARGS (self, mute_changed, mute); +} + +void +clapper_feature_call_played_item_changed (ClapperFeature *self, ClapperMediaItem *item) +{ + CALL_WITH_ARGS (self, played_item_changed, item); +} + +void +clapper_feature_call_item_updated (ClapperFeature *self, ClapperMediaItem *item) +{ + CALL_WITH_ARGS (self, item_updated, item); +} + +void +clapper_feature_call_queue_item_added (ClapperFeature *self, ClapperMediaItem *item, guint index) +{ + CALL_WITH_ARGS (self, queue_item_added, item, index); +} + +void +clapper_feature_call_queue_item_removed (ClapperFeature *self, ClapperMediaItem *item, guint index) +{ + CALL_WITH_ARGS (self, queue_item_removed, item, index); +} + +void +clapper_feature_call_queue_item_repositioned (ClapperFeature *self, guint before, guint after) +{ + CALL_WITH_ARGS (self, queue_item_repositioned, before, after); +} + +void +clapper_feature_call_queue_cleared (ClapperFeature *self) +{ + CALL_WITHOUT_ARGS (self, queue_cleared); +} + +void +clapper_feature_call_queue_progression_changed (ClapperFeature *self, ClapperQueueProgressionMode mode) +{ + CALL_WITH_ARGS (self, queue_progression_changed, mode); +} + +static void +clapper_feature_init (ClapperFeature *self) +{ +} + +static void +clapper_feature_dispatch_properties_changed (GObject *object, + guint n_pspecs, GParamSpec **pspecs) +{ + ClapperPlayer *player; + + if ((player = CLAPPER_PLAYER_CAST (gst_object_get_parent (GST_OBJECT_CAST (object))))) { + ClapperFeaturesManager *features_manager; + + if ((features_manager = clapper_player_get_features_manager (player))) { + guint i; + + for (i = 0; i < n_pspecs; ++i) { + clapper_features_manager_trigger_property_changed (features_manager, + CLAPPER_FEATURE_CAST (object), pspecs[i]); + } + } + + gst_object_unref (player); + } + + G_OBJECT_CLASS (parent_class)->dispatch_properties_changed (object, n_pspecs, pspecs); +} + +static void +clapper_feature_finalize (GObject *object) +{ + ClapperFeature *self = CLAPPER_FEATURE_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_feature_class_init (ClapperFeatureClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperfeature", 0, + "Clapper Feature"); + + gobject_class->dispatch_properties_changed = clapper_feature_dispatch_properties_changed; + gobject_class->finalize = clapper_feature_finalize; +} diff --git a/src/lib/clapper/clapper-feature.h b/src/lib/clapper/clapper-feature.h new file mode 100644 index 00000000..f41f6d72 --- /dev/null +++ b/src/lib/clapper/clapper-feature.h @@ -0,0 +1,219 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_FEATURE (clapper_feature_get_type()) +#define CLAPPER_FEATURE_CAST(obj) ((ClapperFeature *)(obj)) + +G_DECLARE_DERIVABLE_TYPE (ClapperFeature, clapper_feature, CLAPPER, FEATURE, GstObject) + +/** + * ClapperFeatureClass: + * @parent_class: The object class structure. + * @prepare: Prepare feature for operation (optional). + * @unprepare: Revert the changes done in @prepare (optional). + * @property_changed: A property of @feature changed its value. + * @state_changed: Player state was changed. + * @position_changed: Player position was changed. + * @speed_changed: Player speed was changed. + * @volume_changed: Player volume was changed. + * @mute_changed: Player mute state was changed. + * @played_item_changed: Currently playing media item got changed. + * @item_updated: An item in queue got updated. + * @queue_item_added: An item was added to the queue. + * @queue_item_removed: An item was removed from queue. + * @queue_item_reposition: An item changed position within queue. + * @queue_cleared: All items were removed from queue. + * @queue_progression_changed: Progression mode of the queue was changed. + */ +struct _ClapperFeatureClass +{ + GstObjectClass parent_class; + + /** + * ClapperFeatureClass::prepare: + * @feature: a #ClapperFeature + * + * Prepare feature for operation (optional). + * + * This is different from init() as its called from features thread once + * feature is added to the player, so it can already access it parent using + * gst_object_get_parent(). If it fails, no other method will be called. + * + * Returns: %TRUE on success, %FALSE otherwise. + */ + gboolean (* prepare) (ClapperFeature *feature); + + /** + * ClapperFeatureClass::unprepare: + * @feature: a #ClapperFeature + * + * Revert the changes done in @prepare (optional). + * + * Returns: %TRUE on success, %FALSE otherwise. + */ + gboolean (* unprepare) (ClapperFeature *feature); + + /** + * ClapperFeatureClass::property_changed: + * @feature: a #ClapperFeature + * @pspec: a #GParamSpec + * + * A property of @feature changed its value. + * + * Useful for reconfiguring @feature, since unlike "notify" signal + * this is always called from the thread that feature works on and + * only after feature was prepared. + */ + void (* property_changed) (ClapperFeature *feature, GParamSpec *pspec); + + /** + * ClapperFeatureClass::state_changed: + * @feature: a #ClapperFeature + * @state: a #ClapperPlayerState + * + * Player state was changed. + */ + void (* state_changed) (ClapperFeature *feature, ClapperPlayerState state); + + /** + * ClapperFeatureClass::position_changed: + * @feature: a #ClapperFeature + * @position: a decimal number with current position in seconds + * + * Player position was changed. + */ + void (* position_changed) (ClapperFeature *feature, gdouble position); + + /** + * ClapperFeatureClass::speed_changed: + * @feature: a #ClapperFeature + * @speed: the playback speed multiplier + * + * Player speed was changed. + */ + void (* speed_changed) (ClapperFeature *feature, gdouble speed); + + /** + * ClapperFeatureClass::volume_changed: + * @feature: a #ClapperFeature + * @volume: the volume level + * + * Player volume was changed. + */ + void (* volume_changed) (ClapperFeature *feature, gdouble volume); + + /** + * ClapperFeatureClass::mute_changed: + * @feature: a #ClapperFeature + * @mute: %TRUE if player is muted, %FALSE otherwise + * + * Player mute state was changed. + */ + void (* mute_changed) (ClapperFeature *feature, gboolean mute); + + /** + * ClapperFeatureClass::played_item_changed: + * @feature: a #ClapperFeature + * @item: a #ClapperMediaItem that is now playing + * + * New media item started playing. All following events (such as position changes) + * will be related to this @item from now on. + */ + void (* played_item_changed) (ClapperFeature *feature, ClapperMediaItem *item); + + /** + * ClapperFeatureClass::item_updated: + * @feature: a #ClapperFeature + * @item: a #ClapperMediaItem that was updated + * + * An item in queue got updated. This might be (or not) currently + * played item. Implementations can get parent player object + * if they want to check that from its queue. + */ + void (* item_updated) (ClapperFeature *feature, ClapperMediaItem *item); + + /** + * ClapperFeatureClass::queue_item_added: + * @feature: a #ClapperFeature + * @item: a #ClapperMediaItem that was added + * @index: position at which @item was placed in queue + * + * An item was added to the queue. + */ + void (* queue_item_added) (ClapperFeature *feature, ClapperMediaItem *item, guint index); + + /** + * ClapperFeatureClass::queue_item_removed: + * @feature: a #ClapperFeature + * @item: a #ClapperMediaItem that was removed + * @index: position from which @item was removed in queue + * + * An item was removed from queue. + */ + void (* queue_item_removed) (ClapperFeature *feature, ClapperMediaItem *item, guint index); + + /** + * ClapperFeatureClass::queue_item_reposition: + * @feature: a #ClapperFeature + * @before: position from which #ClapperMediaItem was removed + * @after: position at which #ClapperMediaItem was inserted after removal + * + * An item changed position within queue. + */ + void (* queue_item_repositioned) (ClapperFeature *feature, guint before, guint after); + + /** + * ClapperFeatureClass::queue_cleared: + * @feature: a #ClapperFeature + * + * All items were removed from queue. Note that in such event + * @queue_item_removed will NOT be called for each item for performance reasons. + * You probably want to implement this function if you also implemented item removal. + */ + void (* queue_cleared) (ClapperFeature *feature); + + /** + * ClapperFeatureClass::queue_progression_changed: + * @feature: a #ClapperFeature + * @mode: a #ClapperQueueProgressionMode + * + * Progression mode of the queue was changed. + */ + void (* queue_progression_changed) (ClapperFeature *feature, ClapperQueueProgressionMode mode); + + /*< private >*/ + gpointer padding[8]; +}; + +G_END_DECLS diff --git a/src/lib/clapper/clapper-features-bus-private.h b/src/lib/clapper/clapper-features-bus-private.h new file mode 100644 index 00000000..cb7cca9d --- /dev/null +++ b/src/lib/clapper/clapper-features-bus-private.h @@ -0,0 +1,44 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-features-manager-private.h" +#include "clapper-enums-private.h" + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_FEATURES_BUS (clapper_features_bus_get_type()) +#define CLAPPER_FEATURES_BUS_CAST(obj) ((ClapperFeaturesBus *)(obj)) + +/** + * ClapperFeaturesBus: + */ +G_DECLARE_FINAL_TYPE (ClapperFeaturesBus, clapper_features_bus, CLAPPER, FEATURES_BUS, GstBus) + +void clapper_features_bus_initialize (void); + +ClapperFeaturesBus * clapper_features_bus_new (void); + +void clapper_features_bus_post_event (ClapperFeaturesBus *features_bus, ClapperFeaturesManager *src, ClapperFeaturesManagerEvent event, GValue *value, GValue *extra_value); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-features-bus.c b/src/lib/clapper/clapper-features-bus.c new file mode 100644 index 00000000..ad9ee06f --- /dev/null +++ b/src/lib/clapper/clapper-features-bus.c @@ -0,0 +1,168 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "clapper-bus-private.h" +#include "clapper-features-manager-private.h" +#include "clapper-features-bus-private.h" + +#define GST_CAT_DEFAULT clapper_features_bus_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperFeaturesBus +{ + GstBus parent; +}; + +#define parent_class clapper_features_bus_parent_class +G_DEFINE_TYPE (ClapperFeaturesBus, clapper_features_bus, GST_TYPE_BUS); + +enum +{ + CLAPPER_FEATURES_BUS_STRUCTURE_UNKNOWN = 0, + CLAPPER_FEATURES_BUS_STRUCTURE_EVENT +}; + +static ClapperBusQuark _structure_quarks[] = { + {"unknown", 0}, + {"event", 0}, + {NULL, 0} +}; + +enum +{ + CLAPPER_FEATURES_BUS_FIELD_UNKNOWN = 0, + CLAPPER_FEATURES_BUS_FIELD_EVENT, + CLAPPER_FEATURES_BUS_FIELD_VALUE, + CLAPPER_FEATURES_BUS_FIELD_EXTRA_VALUE +}; + +static ClapperBusQuark _field_quarks[] = { + {"unknown", 0}, + {"event", 0}, + {"value", 0}, + {"extra-value", 0}, + {NULL, 0} +}; + +#define _STRUCTURE_QUARK(q) (_structure_quarks[CLAPPER_FEATURES_BUS_STRUCTURE_##q].quark) +#define _FIELD_QUARK(q) (_field_quarks[CLAPPER_FEATURES_BUS_FIELD_##q].quark) +#define _MESSAGE_SRC_CLAPPER_FEATURES_MANAGER(msg) ((ClapperFeaturesManager *) GST_MESSAGE_SRC (msg)) + +void +clapper_features_bus_initialize (void) +{ + gint i; + + for (i = 0; _structure_quarks[i].name; ++i) + _structure_quarks[i].quark = g_quark_from_static_string (_structure_quarks[i].name); + for (i = 0; _field_quarks[i].name; ++i) + _field_quarks[i].quark = g_quark_from_static_string (_field_quarks[i].name); +} + +void +clapper_features_bus_post_event (ClapperFeaturesBus *self, + ClapperFeaturesManager *src, ClapperFeaturesManagerEvent event, + GValue *value, GValue *extra_value) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (EVENT), + _FIELD_QUARK (EVENT), G_TYPE_ENUM, event, + NULL); + + if (value) + gst_structure_id_take_value (structure, _FIELD_QUARK (VALUE), value); + if (extra_value) + gst_structure_id_take_value (structure, _FIELD_QUARK (EXTRA_VALUE), extra_value); + + gst_bus_post (GST_BUS_CAST (self), gst_message_new_application ( + GST_OBJECT_CAST (src), structure)); +} + +static inline void +_handle_event_msg (GstMessage *msg, const GstStructure *structure, + ClapperFeaturesManager *features_manager) +{ + ClapperFeaturesManagerEvent event = CLAPPER_FEATURES_MANAGER_EVENT_UNKNOWN; + const GValue *value = gst_structure_id_get_value (structure, _FIELD_QUARK (VALUE)); + const GValue *extra_value = gst_structure_id_get_value (structure, _FIELD_QUARK (EXTRA_VALUE)); + + gst_structure_id_get (structure, + _FIELD_QUARK (EVENT), G_TYPE_ENUM, &event, + NULL); + + clapper_features_manager_handle_event (features_manager, event, value, extra_value); +} + +static gboolean +clapper_features_bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G_GNUC_UNUSED) +{ + if (G_LIKELY (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_APPLICATION)) { + ClapperFeaturesManager *features_manager = _MESSAGE_SRC_CLAPPER_FEATURES_MANAGER (msg); + const GstStructure *structure = gst_message_get_structure (msg); + GQuark quark = gst_structure_get_name_id (structure); + + if (quark == _STRUCTURE_QUARK (EVENT)) + _handle_event_msg (msg, structure, features_manager); + } + + return G_SOURCE_CONTINUE; +} + +/* + * clapper_features_bus_new: + * + * Returns: (transfer full): a new #ClapperFeaturesBus instance. + */ +ClapperFeaturesBus * +clapper_features_bus_new (void) +{ + GstBus *features_bus; + + features_bus = GST_BUS_CAST (g_object_new (CLAPPER_TYPE_FEATURES_BUS, NULL)); + gst_object_ref_sink (features_bus); + + gst_bus_add_watch (features_bus, (GstBusFunc) clapper_features_bus_message_func, NULL); + + return CLAPPER_FEATURES_BUS_CAST (features_bus); +} + +static void +clapper_features_bus_init (ClapperFeaturesBus *self) +{ +} + +static void +clapper_features_bus_finalize (GObject *object) +{ + ClapperFeaturesBus *self = CLAPPER_FEATURES_BUS_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_features_bus_class_init (ClapperFeaturesBusClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperfeaturesbus", 0, + "Clapper Features Bus"); + + gobject_class->finalize = clapper_features_bus_finalize; +} diff --git a/src/lib/clapper/clapper-features-manager-private.h b/src/lib/clapper/clapper-features-manager-private.h new file mode 100644 index 00000000..3e566cb5 --- /dev/null +++ b/src/lib/clapper/clapper-features-manager-private.h @@ -0,0 +1,81 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "clapper-enums-private.h" +#include "clapper-threaded-object.h" +#include "clapper-feature.h" + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_FEATURES_MANAGER (clapper_features_manager_get_type()) +#define CLAPPER_FEATURES_MANAGER_CAST(obj) ((ClapperFeaturesManager *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperFeaturesManager, clapper_features_manager, CLAPPER, FEATURES_MANAGER, ClapperThreadedObject) + +G_GNUC_INTERNAL +ClapperFeaturesManager * clapper_features_manager_new (void); + +G_GNUC_INTERNAL +void clapper_features_manager_add_feature (ClapperFeaturesManager *features, ClapperFeature *feature, GstObject *parent); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_property_changed (ClapperFeaturesManager *self, ClapperFeature *feature, GParamSpec *pspec); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_state_changed (ClapperFeaturesManager *features, ClapperPlayerState state); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_position_changed (ClapperFeaturesManager *features, gdouble position); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_speed_changed (ClapperFeaturesManager *features, gdouble speed); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_volume_changed (ClapperFeaturesManager *features, gdouble volume); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_mute_changed (ClapperFeaturesManager *features, gboolean mute); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_played_item_changed (ClapperFeaturesManager *features, ClapperMediaItem *item); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_item_updated (ClapperFeaturesManager *features, ClapperMediaItem *item); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_queue_item_added (ClapperFeaturesManager *features, ClapperMediaItem *item, guint index); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_queue_item_removed (ClapperFeaturesManager *features, ClapperMediaItem *item, guint index); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_queue_item_repositioned (ClapperFeaturesManager *features, guint before, guint after); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_queue_cleared (ClapperFeaturesManager *features); + +G_GNUC_INTERNAL +void clapper_features_manager_trigger_queue_progression_changed (ClapperFeaturesManager *features, ClapperQueueProgressionMode mode); + +G_GNUC_INTERNAL +void clapper_features_manager_handle_event (ClapperFeaturesManager *features, ClapperFeaturesManagerEvent event, const GValue *value, const GValue *extra_value); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-features-manager.c b/src/lib/clapper/clapper-features-manager.c new file mode 100644 index 00000000..d92f2892 --- /dev/null +++ b/src/lib/clapper/clapper-features-manager.c @@ -0,0 +1,371 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "clapper-features-manager-private.h" +#include "clapper-features-bus-private.h" +#include "clapper-feature-private.h" + +#define GST_CAT_DEFAULT clapper_features_manager_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperFeaturesManager +{ + ClapperThreadedObject parent; + + GPtrArray *features; + ClapperFeaturesBus *bus; +}; + +#define parent_class clapper_features_manager_parent_class +G_DEFINE_TYPE (ClapperFeaturesManager, clapper_features_manager, CLAPPER_TYPE_THREADED_OBJECT); + +static inline void +_post_object (ClapperFeaturesManager *self, ClapperFeaturesManagerEvent event, GObject *data) +{ + GValue value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_OBJECT); + g_value_set_object (&value, data); + + clapper_features_bus_post_event (self->bus, self, event, &value, NULL); +} + +static inline void +_post_int (ClapperFeaturesManager *self, ClapperFeaturesManagerEvent event, gint data) +{ + GValue value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_INT); + g_value_set_int (&value, data); + + clapper_features_bus_post_event (self->bus, self, event, &value, NULL); +} + +static inline void +_post_double (ClapperFeaturesManager *self, ClapperFeaturesManagerEvent event, gdouble data) +{ + GValue value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_DOUBLE); + g_value_set_double (&value, data); + + clapper_features_bus_post_event (self->bus, self, event, &value, NULL); +} + +static inline void +_post_boolean (ClapperFeaturesManager *self, ClapperFeaturesManagerEvent event, gboolean data) +{ + GValue value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_BOOLEAN); + g_value_set_boolean (&value, data); + + clapper_features_bus_post_event (self->bus, self, event, &value, NULL); +} + +static inline void +_post_item_added_or_removed (ClapperFeaturesManager *self, ClapperFeaturesManagerEvent event, + ClapperMediaItem *item, guint index) +{ + GValue value = G_VALUE_INIT; + GValue extra_value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_OBJECT); + g_value_set_object (&value, (GObject *) item); + + g_value_init (&extra_value, G_TYPE_UINT); + g_value_set_uint (&extra_value, index); + + clapper_features_bus_post_event (self->bus, self, event, &value, &extra_value); +} + +static inline void +_post_item_reposition (ClapperFeaturesManager *self, guint data_1, guint data_2) +{ + GValue value = G_VALUE_INIT; + GValue extra_value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_UINT); + g_value_set_uint (&value, data_1); + + g_value_init (&extra_value, G_TYPE_UINT); + g_value_set_uint (&extra_value, data_2); + + clapper_features_bus_post_event (self->bus, self, + CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_REPOSITIONED, &value, &extra_value); +} + +/* + * clapper_features_manager_new: + * + * Returns: (transfer full): a new #ClapperFeaturesManager instance. + */ +ClapperFeaturesManager * +clapper_features_manager_new (void) +{ + ClapperFeaturesManager *features_manager; + + features_manager = g_object_new (CLAPPER_TYPE_FEATURES_MANAGER, NULL); + gst_object_ref_sink (features_manager); + + return features_manager; +} + +void +clapper_features_manager_add_feature (ClapperFeaturesManager *self, ClapperFeature *feature, GstObject *parent) +{ + GValue value = G_VALUE_INIT; + GValue extra_value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_OBJECT); + g_value_set_object (&value, G_OBJECT (feature)); + + g_value_init (&extra_value, G_TYPE_OBJECT); + g_value_set_object (&extra_value, G_OBJECT (parent)); + + clapper_features_bus_post_event (self->bus, self, + CLAPPER_FEATURES_MANAGER_EVENT_FEATURE_ADDED, &value, &extra_value); +} + +void +clapper_features_manager_trigger_property_changed (ClapperFeaturesManager *self, ClapperFeature *feature, GParamSpec *pspec) +{ + GValue value = G_VALUE_INIT; + GValue extra_value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_OBJECT); + g_value_set_object (&value, G_OBJECT (feature)); + + g_value_init (&extra_value, G_TYPE_PARAM); + g_value_set_param (&extra_value, pspec); + + clapper_features_bus_post_event (self->bus, self, + CLAPPER_FEATURES_MANAGER_EVENT_FEATURE_PROPERTY_CHANGED, &value, &extra_value); +} + +void +clapper_features_manager_trigger_state_changed (ClapperFeaturesManager *self, ClapperPlayerState state) +{ + _post_int (self, CLAPPER_FEATURES_MANAGER_EVENT_STATE_CHANGED, state); +} + +void +clapper_features_manager_trigger_position_changed (ClapperFeaturesManager *self, gdouble position) +{ + _post_double (self, CLAPPER_FEATURES_MANAGER_EVENT_POSITION_CHANGED, position); +} + +void +clapper_features_manager_trigger_speed_changed (ClapperFeaturesManager *self, gdouble speed) +{ + _post_double (self, CLAPPER_FEATURES_MANAGER_EVENT_SPEED_CHANGED, speed); +} + +void +clapper_features_manager_trigger_volume_changed (ClapperFeaturesManager *self, gdouble volume) +{ + _post_double (self, CLAPPER_FEATURES_MANAGER_EVENT_VOLUME_CHANGED, volume); +} + +void +clapper_features_manager_trigger_mute_changed (ClapperFeaturesManager *self, gboolean mute) +{ + _post_boolean (self, CLAPPER_FEATURES_MANAGER_EVENT_MUTE_CHANGED, mute); +} + +void +clapper_features_manager_trigger_played_item_changed (ClapperFeaturesManager *self, ClapperMediaItem *item) +{ + _post_object (self, CLAPPER_FEATURES_MANAGER_EVENT_PLAYED_ITEM_CHANGED, (GObject *) item); +} + +void +clapper_features_manager_trigger_item_updated (ClapperFeaturesManager *self, ClapperMediaItem *item) +{ + _post_object (self, CLAPPER_FEATURES_MANAGER_EVENT_ITEM_UPDATED, (GObject *) item); +} + +void +clapper_features_manager_trigger_queue_item_added (ClapperFeaturesManager *self, ClapperMediaItem *item, guint index) +{ + _post_item_added_or_removed (self, CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_ADDED, item, index); +} + +void +clapper_features_manager_trigger_queue_item_removed (ClapperFeaturesManager *self, ClapperMediaItem *item, guint index) +{ + _post_item_added_or_removed (self, CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_REMOVED, item, index); +} + +void +clapper_features_manager_trigger_queue_item_repositioned (ClapperFeaturesManager *self, guint before, guint after) +{ + _post_item_reposition (self, before, after); +} + +void +clapper_features_manager_trigger_queue_cleared (ClapperFeaturesManager *self) +{ + clapper_features_bus_post_event (self->bus, self, CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_CLEARED, NULL, NULL); +} + +void +clapper_features_manager_trigger_queue_progression_changed (ClapperFeaturesManager *self, ClapperQueueProgressionMode mode) +{ + _post_int (self, CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_PROGRESSION_CHANGED, mode); +} + +void +clapper_features_manager_handle_event (ClapperFeaturesManager *self, ClapperFeaturesManagerEvent event, + const GValue *value, const GValue *extra_value) +{ + guint i; + + switch (event) { + case CLAPPER_FEATURES_MANAGER_EVENT_FEATURE_ADDED:{ + ClapperFeature *feature = g_value_get_object (value); + GstObject *parent = g_value_get_object (extra_value); + + if (!g_ptr_array_find (self->features, feature, NULL)) { + g_ptr_array_add (self->features, gst_object_ref (feature)); + gst_object_set_parent (GST_OBJECT_CAST (feature), parent); + + clapper_feature_call_prepare (feature); + } + + /* Nothing more to do */ + return; + } + default: + break; + } + + for (i = 0; i < self->features->len; ++i) { + ClapperFeature *feature = g_ptr_array_index (self->features, i); + + switch (event) { + case CLAPPER_FEATURES_MANAGER_EVENT_FEATURE_PROPERTY_CHANGED:{ + ClapperFeature *event_feature = g_value_get_object (value); + + if (feature == event_feature) { + clapper_feature_call_property_changed (feature, + g_value_get_param (extra_value)); + } + break; + } + case CLAPPER_FEATURES_MANAGER_EVENT_STATE_CHANGED: + clapper_feature_call_state_changed (feature, g_value_get_int (value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_POSITION_CHANGED: + clapper_feature_call_position_changed (feature, g_value_get_double (value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_SPEED_CHANGED: + clapper_feature_call_speed_changed (feature, g_value_get_double (value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_VOLUME_CHANGED: + clapper_feature_call_volume_changed (feature, g_value_get_double (value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_MUTE_CHANGED: + clapper_feature_call_mute_changed (feature, g_value_get_boolean (value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_PLAYED_ITEM_CHANGED: + clapper_feature_call_played_item_changed (feature, + CLAPPER_MEDIA_ITEM_CAST (g_value_get_object (value))); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_ITEM_UPDATED: + clapper_feature_call_item_updated (feature, + CLAPPER_MEDIA_ITEM_CAST (g_value_get_object (value))); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_ADDED: + clapper_feature_call_queue_item_added (feature, + CLAPPER_MEDIA_ITEM_CAST (g_value_get_object (value)), + g_value_get_uint (extra_value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_REMOVED: + clapper_feature_call_queue_item_removed (feature, + CLAPPER_MEDIA_ITEM_CAST (g_value_get_object (value)), + g_value_get_uint (extra_value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_ITEM_REPOSITIONED: + clapper_feature_call_queue_item_repositioned (feature, + g_value_get_uint (value), + g_value_get_uint (extra_value)); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_CLEARED: + clapper_feature_call_queue_cleared (feature); + break; + case CLAPPER_FEATURES_MANAGER_EVENT_QUEUE_PROGRESSION_CHANGED: + clapper_feature_call_queue_progression_changed (feature, g_value_get_int (value)); + break; + default: + break; + } + } +} + +static void +clapper_features_manager_thread_start (ClapperThreadedObject *threaded_object) +{ + ClapperFeaturesManager *self = CLAPPER_FEATURES_MANAGER_CAST (threaded_object); + + GST_TRACE_OBJECT (threaded_object, "Features manager thread start"); + + self->features = g_ptr_array_new_with_free_func ( + (GDestroyNotify) gst_object_unref); + self->bus = clapper_features_bus_new (); +} + +static void +clapper_features_manager_thread_stop (ClapperThreadedObject *threaded_object) +{ + ClapperFeaturesManager *self = CLAPPER_FEATURES_MANAGER_CAST (threaded_object); + guint i; + + GST_TRACE_OBJECT (threaded_object, "Features manager thread stop"); + + gst_bus_set_flushing (GST_BUS_CAST (self->bus), TRUE); + gst_bus_remove_watch (GST_BUS_CAST (self->bus)); + gst_clear_object (&self->bus); + + for (i = 0; i < self->features->len; ++i) { + ClapperFeature *feature = g_ptr_array_index (self->features, i); + + clapper_feature_call_unprepare (feature); + gst_object_unparent (GST_OBJECT_CAST (feature)); + } + + g_ptr_array_unref (self->features); +} + +static void +clapper_features_manager_init (ClapperFeaturesManager *self) +{ +} + +static void +clapper_features_manager_class_init (ClapperFeaturesManagerClass *klass) +{ + ClapperThreadedObjectClass *threaded_object = (ClapperThreadedObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperfeaturesmanager", 0, + "Clapper Features Manager"); + + threaded_object->thread_start = clapper_features_manager_thread_start; + threaded_object->thread_stop = clapper_features_manager_thread_stop; +} diff --git a/src/lib/clapper/clapper-marker-private.h b/src/lib/clapper/clapper-marker-private.h new file mode 100644 index 00000000..14e24bb8 --- /dev/null +++ b/src/lib/clapper/clapper-marker-private.h @@ -0,0 +1,35 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "clapper-enums.h" +#include "clapper-marker.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperMarker * clapper_marker_new_internal (ClapperMarkerType marker_type, const gchar *name, gdouble start, gdouble end); + +G_GNUC_INTERNAL +gboolean clapper_marker_is_internal (ClapperMarker *marker); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-marker.c b/src/lib/clapper/clapper-marker.c new file mode 100644 index 00000000..4620bcf0 --- /dev/null +++ b/src/lib/clapper/clapper-marker.c @@ -0,0 +1,338 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperMarker: + * + * Represents a point in timeline. + * + * Markers are a convienient way of marking points of interest within a + * [class@Clapper.Timeline] of [class@Clapper.MediaItem]. Use them + * to indicate certain areas on the timeline. + * + * Markers are reference counted immutable objects. Once a marker is created + * it can only be inserted into a single [class@Clapper.Timeline] at a time. + * + * Please note that markers are independent of [property@Clapper.MediaItem:duration] + * and applications should not assume that all markers must have start/end times + * lower or equal the item duration. This is not the case in e.g. live streams + * where duration is unknown, but markers are still allowed to mark entries + * (like EPG titles for example). + * + * Remember that [class@Clapper.Player] will also automatically insert certain + * markers extracted from media such as video chapters. Clapper will never + * "touch" the ones created by the application. If you want to differentiate + * your own markers, applications can define and create markers with one of + * the custom types from [enum@Clapper.MarkerType] enum. + * + * Example: + * + * ```c + * #define MY_APP_MARKER (CLAPPER_MARKER_TYPE_CUSTOM_1) + * + * ClapperMarker *marker = clapper_marker_new (MY_APP_MARKER, title, start, end); + * ``` + * + * ```c + * ClapperMarkerType marker_type = clapper_marker_get_marker_type (marker); + * + * if (marker_type == MY_APP_MARKER) { + * // Do something with your custom marker + * } + * ``` + */ + +#include "clapper-marker-private.h" + +#define GST_CAT_DEFAULT clapper_marker_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperMarker +{ + GstObject parent; + + ClapperMarkerType marker_type; + gchar *title; + gdouble start; + gdouble end; + + gboolean is_internal; +}; + +enum +{ + PROP_0, + PROP_MARKER_TYPE, + PROP_TITLE, + PROP_START, + PROP_END, + PROP_LAST +}; + +#define parent_class clapper_marker_parent_class +G_DEFINE_TYPE (ClapperMarker, clapper_marker, GST_TYPE_OBJECT); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +/** + * clapper_marker_new: + * @marker_type: a #ClapperMarkerType + * @title: (nullable): title of the marker + * @start: a start position of the marker + * @end: an end position of the marker or [const@Clapper.MARKER_NO_END] if none + * + * Creates a new #ClapperMarker with given params. + * + * It is considered a programmer error trying to set an ending + * point that is before the starting one. If end is unknown or + * not defined a special [const@Clapper.MARKER_NO_END] value + * should be used. + * + * Returns: (transfer full): a new #ClapperMarker. + */ +ClapperMarker * +clapper_marker_new (ClapperMarkerType marker_type, const gchar *title, + gdouble start, gdouble end) +{ + ClapperMarker *marker; + + marker = g_object_new (CLAPPER_TYPE_MARKER, + "marker-type", marker_type, + "title", title, + "start", start, + "end", end, NULL); + gst_object_ref_sink (marker); + + return marker; +} + +ClapperMarker * +clapper_marker_new_internal (ClapperMarkerType marker_type, const gchar *title, + gdouble start, gdouble end) +{ + ClapperMarker *marker; + + marker = clapper_marker_new (marker_type, title, start, end); + marker->is_internal = TRUE; + + return marker; +} + +/** + * clapper_marker_get_marker_type: + * @marker: a #ClapperMarker + * + * Get the #ClapperMarkerType of @marker. + * + * Returns: type of marker. + */ +ClapperMarkerType +clapper_marker_get_marker_type (ClapperMarker *self) +{ + g_return_val_if_fail (CLAPPER_IS_MARKER (self), CLAPPER_MARKER_TYPE_UNKNOWN); + + return self->marker_type; +} + +/** + * clapper_marker_get_title: + * @marker: a #ClapperMarker + * + * Get the title of @marker. + * + * Returns: the marker title. + */ +const gchar * +clapper_marker_get_title (ClapperMarker *self) +{ + g_return_val_if_fail (CLAPPER_IS_MARKER (self), NULL); + + return self->title; +} + +/** + * clapper_marker_get_start: + * @marker: a #ClapperMarker + * + * Get the start position (in seconds) of @marker. + * + * Returns: marker start. + */ +gdouble +clapper_marker_get_start (ClapperMarker *self) +{ + g_return_val_if_fail (CLAPPER_IS_MARKER (self), 0); + + return self->start; +} + +/** + * clapper_marker_get_end: + * @marker: a #ClapperMarker + * + * Get the end position (in seconds) of @marker. + * + * Returns: marker end. + */ +gdouble +clapper_marker_get_end (ClapperMarker *self) +{ + g_return_val_if_fail (CLAPPER_IS_MARKER (self), CLAPPER_MARKER_NO_END); + + return self->end; +} + +gboolean +clapper_marker_is_internal (ClapperMarker *self) +{ + return self->is_internal; +} + +static void +clapper_marker_init (ClapperMarker *self) +{ + self->marker_type = CLAPPER_MARKER_TYPE_UNKNOWN; + self->end = CLAPPER_MARKER_NO_END; +} + +static void +clapper_marker_constructed (GObject *object) +{ + ClapperMarker *self = CLAPPER_MARKER_CAST (object); + + G_OBJECT_CLASS (parent_class)->constructed (object); + + GST_TRACE_OBJECT (self, "Created new marker" + ", type: %i, title: \"%s\", start: %lf, end: %lf", + self->marker_type, GST_STR_NULL (self->title), self->start, self->end); +} + +static void +clapper_marker_finalize (GObject *object) +{ + ClapperMarker *self = CLAPPER_MARKER_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + g_free (self->title); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_marker_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperMarker *self = CLAPPER_MARKER_CAST (object); + + switch (prop_id) { + case PROP_MARKER_TYPE: + g_value_set_enum (value, clapper_marker_get_marker_type (self)); + break; + case PROP_TITLE: + g_value_set_string (value, clapper_marker_get_title (self)); + break; + case PROP_START: + g_value_set_double (value, clapper_marker_get_start (self)); + break; + case PROP_END: + g_value_set_double (value, clapper_marker_get_end (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_marker_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperMarker *self = CLAPPER_MARKER_CAST (object); + + switch (prop_id) { + case PROP_MARKER_TYPE: + self->marker_type = g_value_get_enum (value); + break; + case PROP_TITLE: + self->title = g_value_dup_string (value); + break; + case PROP_START: + self->start = g_value_get_double (value); + break; + case PROP_END: + self->end = g_value_get_double (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_marker_class_init (ClapperMarkerClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappermarker", 0, + "Clapper Marker"); + + gobject_class->constructed = clapper_marker_constructed; + gobject_class->get_property = clapper_marker_get_property; + gobject_class->set_property = clapper_marker_set_property; + gobject_class->finalize = clapper_marker_finalize; + + /** + * ClapperMarker:marker-type: + * + * Type of stream. + */ + param_specs[PROP_MARKER_TYPE] = g_param_spec_enum ("marker-type", + NULL, NULL, CLAPPER_TYPE_MARKER_TYPE, CLAPPER_MARKER_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMarker:title: + * + * Title of marker. + */ + param_specs[PROP_TITLE] = g_param_spec_string ("title", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMarker:start: + * + * Starting time of marker. + */ + param_specs[PROP_START] = g_param_spec_double ("start", + NULL, NULL, 0, G_MAXDOUBLE, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMarker:end: + * + * Ending time of marker. + */ + param_specs[PROP_END] = g_param_spec_double ("end", + NULL, NULL, -1, G_MAXDOUBLE, CLAPPER_MARKER_NO_END, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-marker.h b/src/lib/clapper/clapper-marker.h new file mode 100644 index 00000000..8f10bc42 --- /dev/null +++ b/src/lib/clapper/clapper-marker.h @@ -0,0 +1,58 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_MARKER (clapper_marker_get_type()) +#define CLAPPER_MARKER_CAST(obj) ((ClapperMarker *)(obj)) + +/* NOTE: #ClapperMarker are immutable objects that cannot be derived, + * otherwise #ClapperFeaturesManager would not be able to announce media + * item changed caused by changes within them */ +G_DECLARE_FINAL_TYPE (ClapperMarker, clapper_marker, CLAPPER, MARKER, GstObject) + +/** + * CLAPPER_MARKER_NO_END: + * + * The value used to indicate that marker does not have an ending time specified + */ +#define CLAPPER_MARKER_NO_END (-1.0) + +ClapperMarker * clapper_marker_new (ClapperMarkerType marker_type, const gchar *title, gdouble start, gdouble end); + +ClapperMarkerType clapper_marker_get_marker_type (ClapperMarker *marker); + +const gchar * clapper_marker_get_title (ClapperMarker *marker); + +gdouble clapper_marker_get_start (ClapperMarker *marker); + +gdouble clapper_marker_get_end (ClapperMarker *marker); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-media-item-private.h b/src/lib/clapper/clapper-media-item-private.h new file mode 100644 index 00000000..ca1faf55 --- /dev/null +++ b/src/lib/clapper/clapper-media-item-private.h @@ -0,0 +1,46 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-media-item.h" +#include "clapper-player-private.h" +#include "clapper-app-bus-private.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +void clapper_media_item_update_from_tag_list (ClapperMediaItem *item, const GstTagList *tags, ClapperPlayer *player); + +G_GNUC_INTERNAL +void clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDiscovererInfo *info); + +G_GNUC_INTERNAL +gboolean clapper_media_item_set_duration (ClapperMediaItem *item, gdouble duration, ClapperAppBus *app_bus); + +G_GNUC_INTERNAL +void clapper_media_item_set_used (ClapperMediaItem *item, gboolean used); + +G_GNUC_INTERNAL +gboolean clapper_media_item_get_used (ClapperMediaItem *item); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c new file mode 100644 index 00000000..be378f41 --- /dev/null +++ b/src/lib/clapper/clapper-media-item.c @@ -0,0 +1,641 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperMediaItem: + * + * Represents a media item. + * + * A newly created media item must be added to player [class@Clapper.Queue] + * first in order to be played. + */ + +#include "clapper-media-item-private.h" +#include "clapper-timeline-private.h" +#include "clapper-player-private.h" +#include "clapper-playbin-bus-private.h" +#include "clapper-features-manager-private.h" +#include "clapper-utils-private.h" + +#define GST_CAT_DEFAULT clapper_media_item_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperMediaItem +{ + GstObject parent; + + gchar *uri; + gchar *suburi; + + ClapperTimeline *timeline; + + guint id; + gchar *title; + gchar *container_format; + gdouble duration; + + /* For shuffle */ + gboolean used; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_URI, + PROP_SUBURI, + PROP_TITLE, + PROP_CONTAINER_FORMAT, + PROP_DURATION, + PROP_TIMELINE, + PROP_LAST +}; + +#define parent_class clapper_media_item_parent_class +G_DEFINE_TYPE (ClapperMediaItem, clapper_media_item, GST_TYPE_OBJECT); + +static guint _item_id = 0; +static GMutex id_lock; +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +/** + * clapper_media_item_new: + * @uri: a media URI + * + * Creates new #ClapperMediaItem from URI. + * + * Use one of the URI protocols supported by plugins in #GStreamer + * installation. For local files you can use either "file" protocol + * or [ctor@Clapper.MediaItem.new_from_file] method. + * + * It is considered a programmer error trying to create new media item from + * invalid URI. If URI is valid, but unsupported by installed plugins on user + * system, [class@Clapper.Player] will emit a [signal@Clapper.Player::missing-plugin] + * signal upon playback. + * + * Returns: (transfer full): a new #ClapperMediaItem. + */ +ClapperMediaItem * +clapper_media_item_new (const gchar *uri) +{ + ClapperMediaItem *item; + + g_return_val_if_fail (uri != NULL, NULL); + + item = g_object_new (CLAPPER_TYPE_MEDIA_ITEM, "uri", uri, NULL); + gst_object_ref_sink (item); + + g_mutex_lock (&id_lock); + item->id = _item_id; + _item_id++; + g_mutex_unlock (&id_lock); + + /* FIXME: Set initial container format from file extension parsing */ + + GST_TRACE_OBJECT (item, "New media item, ID: %u, URI: %s, title: %s", + item->id, item->uri, item->title); + + return item; +} + +/** + * clapper_media_item_new_from_file: + * @file: a #GFile + * + * Creates new #ClapperMediaItem from #GFile. + * + * Same as [ctor@Clapper.MediaItem.new], but uses a [iface@Gio.File] + * for convenience in some situations instead of an URI. + * + * Returns: (transfer full): a new #ClapperMediaItem. + */ +ClapperMediaItem * +clapper_media_item_new_from_file (GFile *file) +{ + ClapperMediaItem *item; + gchar *uri; + + g_return_val_if_fail (G_IS_FILE (file), NULL); + + uri = clapper_utils_uri_from_file (file); + item = clapper_media_item_new (uri); + + g_free (uri); + + return item; +} + +/** + * clapper_media_item_get_id: + * @item: a #ClapperMediaItem + * + * Get the unique ID of #ClapperMediaItem. + * + * Returns: an ID of #ClapperMediaItem. + */ +guint +clapper_media_item_get_id (ClapperMediaItem *self) +{ + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), G_MAXUINT); + + return self->id; +} + +/** + * clapper_media_item_get_uri: + * @item: a #ClapperMediaItem + * + * Get the URI of #ClapperMediaItem. + * + * Returns: an URI of #ClapperMediaItem. + */ +const gchar * +clapper_media_item_get_uri (ClapperMediaItem *self) +{ + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + return self->uri; +} + +/** + * clapper_media_item_set_suburi: + * @item: a #ClapperMediaItem + * + * Set the additional URI of #ClapperMediaItem. + * + * This is typically used to add an external subtitles URI + * to the @item. + */ +void +clapper_media_item_set_suburi (ClapperMediaItem *self, const gchar *suburi) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + changed = g_set_str (&self->suburi, suburi); + GST_OBJECT_UNLOCK (self); + + if (changed) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + if (player) { + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_SUBURI]); + clapper_playbin_bus_post_item_suburi_change (player->bus, self); + + gst_object_unref (player); + } + } +} + +/** + * clapper_media_item_get_suburi: + * @item: a #ClapperMediaItem + * + * Get the additional URI of #ClapperMediaItem. + * + * Returns: (transfer full) (nullable): an additional URI of #ClapperMediaItem. + */ +gchar * +clapper_media_item_get_suburi (ClapperMediaItem *self) +{ + gchar *suburi; + + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + GST_OBJECT_LOCK (self); + suburi = g_strdup (self->suburi); + GST_OBJECT_UNLOCK (self); + + return suburi; +} + +static gboolean +clapper_media_item_take_title (ClapperMediaItem *self, gchar *title, + ClapperAppBus *app_bus) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = g_strcmp0 (self->title, title) != 0)) { + g_free (self->title); + self->title = title; + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_app_bus_post_prop_notify (app_bus, GST_OBJECT_CAST (self), param_specs[PROP_TITLE]); + else + g_free (title); + + return changed; +} + +/** + * clapper_media_item_get_title: + * @item: a #ClapperMediaItem + * + * Get media item title. + * + * The title can be either text detected by media discovery once it + * completes. Otherwise whenever possible this will try to return a title + * extracted from media URI e.g. basename without extension for local files. + * + * Returns: (transfer full) (nullable): media title. + */ +gchar * +clapper_media_item_get_title (ClapperMediaItem *self) +{ + gchar *title; + + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + GST_OBJECT_LOCK (self); + title = g_strdup (self->title); + GST_OBJECT_UNLOCK (self); + + return title; +} + +static gboolean +clapper_media_item_take_container_format (ClapperMediaItem *self, gchar *container_format, + ClapperAppBus *app_bus) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = g_strcmp0 (self->container_format, container_format) != 0)) { + g_free (self->container_format); + self->container_format = container_format; + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_app_bus_post_prop_notify (app_bus, GST_OBJECT_CAST (self), param_specs[PROP_CONTAINER_FORMAT]); + else + g_free (container_format); + + return changed; +} + +/** + * clapper_media_item_get_container_format: + * @item: a #ClapperMediaItem + * + * Get media item container format. + * + * Returns: (transfer full) (nullable): media container format. + */ +gchar * +clapper_media_item_get_container_format (ClapperMediaItem *self) +{ + gchar *container_format; + + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + GST_OBJECT_LOCK (self); + container_format = g_strdup (self->container_format); + GST_OBJECT_UNLOCK (self); + + return container_format; +} + +gboolean +clapper_media_item_set_duration (ClapperMediaItem *self, gdouble duration, + ClapperAppBus *app_bus) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (self->duration, duration, FLT_EPSILON))) + self->duration = duration; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_DEBUG_OBJECT (self, "Duration: %" GST_TIME_FORMAT, GST_TIME_ARGS (duration * GST_SECOND)); + clapper_app_bus_post_prop_notify (app_bus, GST_OBJECT_CAST (self), param_specs[PROP_DURATION]); + } + + return changed; +} + +/** + * clapper_media_item_get_duration: + * @item: a #ClapperMediaItem + * + * Get media item duration as decimal number in seconds. + * + * Returns: media duration. + */ +gdouble +clapper_media_item_get_duration (ClapperMediaItem *self) +{ + gdouble duration; + + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), 0); + + GST_OBJECT_LOCK (self); + duration = self->duration; + GST_OBJECT_UNLOCK (self); + + return duration; +} + +/** + * clapper_media_item_get_timeline: + * @item: a #ClapperMediaItem + * + * Get the [class@Clapper.Timeline] assosiated with @item. + * + * Returns: (transfer none): a #ClapperTimeline of item. + */ +ClapperTimeline * +clapper_media_item_get_timeline (ClapperMediaItem *self) +{ + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + return self->timeline; +} + +static gboolean +clapper_media_item_update_from_container_tags (ClapperMediaItem *self, const GstTagList *tags, + ClapperAppBus *app_bus) +{ + gchar *string = NULL; + gboolean changed = FALSE; + + if (gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &string)) + changed |= clapper_media_item_take_container_format (self, string, app_bus); + if (gst_tag_list_get_string (tags, GST_TAG_TITLE, &string)) + changed |= clapper_media_item_take_title (self, string, app_bus); + + return changed; +} + +void +clapper_media_item_update_from_tag_list (ClapperMediaItem *self, const GstTagList *tags, + ClapperPlayer *player) +{ + GstTagScope scope = gst_tag_list_get_scope (tags); + + if (scope == GST_TAG_SCOPE_GLOBAL) { + gboolean changed = clapper_media_item_update_from_container_tags (self, tags, player->app_bus); + + if (changed) { + ClapperFeaturesManager *features_manager; + + if ((features_manager = clapper_player_get_features_manager (player))) + clapper_features_manager_trigger_item_updated (features_manager, self); + } + } +} + +void +clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDiscovererInfo *info) +{ + ClapperPlayer *player; + GstDiscovererStreamInfo *sinfo; + GstClockTime duration; + gdouble val_dbl; + gboolean changed = FALSE; + + if (!(player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)))) + return; + + for (sinfo = gst_discoverer_info_get_stream_info (info); + sinfo != NULL; + sinfo = gst_discoverer_stream_info_get_next (sinfo)) { + const GstTagList *tags; + + if (GST_IS_DISCOVERER_CONTAINER_INFO (sinfo)) { + GstDiscovererContainerInfo *cinfo = (GstDiscovererContainerInfo *) sinfo; + + if ((tags = gst_discoverer_container_info_get_tags (cinfo))) + changed |= clapper_media_item_update_from_container_tags (self, tags, player->app_bus); + } + gst_discoverer_stream_info_unref (sinfo); + } + + duration = gst_discoverer_info_get_duration (info); + + if (G_UNLIKELY (duration == GST_CLOCK_TIME_NONE)) + duration = 0; + + val_dbl = (gdouble) duration / GST_SECOND; + changed |= clapper_media_item_set_duration (self, val_dbl, player->app_bus); + + if (changed) { + ClapperFeaturesManager *features_manager; + + if ((features_manager = clapper_player_get_features_manager (player))) + clapper_features_manager_trigger_item_updated (features_manager, self); + } + + gst_object_unref (player); +} + +void +clapper_media_item_set_used (ClapperMediaItem *self, gboolean used) +{ + GST_OBJECT_LOCK (self); + self->used = used; + GST_OBJECT_UNLOCK (self); +} + +gboolean +clapper_media_item_get_used (ClapperMediaItem *self) +{ + gboolean used; + + GST_OBJECT_LOCK (self); + used = self->used; + GST_OBJECT_UNLOCK (self); + + return used; +} + +static void +clapper_media_item_init (ClapperMediaItem *self) +{ + self->timeline = clapper_timeline_new (); + gst_object_set_parent (GST_OBJECT_CAST (self->timeline), GST_OBJECT_CAST (self)); +} + +static void +clapper_media_item_constructed (GObject *object) +{ + ClapperMediaItem *self = CLAPPER_MEDIA_ITEM_CAST (object); + + /* Be safe when someone incorrectly constructs item without URI */ + if (G_UNLIKELY (self->uri == NULL)) + self->uri = g_strdup ("file://"); + + self->title = clapper_utils_title_from_uri (self->uri); + + G_OBJECT_CLASS (parent_class)->constructed (object); +} + +static void +clapper_media_item_finalize (GObject *object) +{ + ClapperMediaItem *self = CLAPPER_MEDIA_ITEM_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + g_free (self->uri); + g_free (self->title); + g_free (self->container_format); + + gst_object_unparent (GST_OBJECT_CAST (self->timeline)); + gst_object_unref (self->timeline); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_media_item_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperMediaItem *self = CLAPPER_MEDIA_ITEM_CAST (object); + + switch (prop_id) { + case PROP_URI: + self->uri = g_value_dup_string (value); + break; + case PROP_SUBURI: + clapper_media_item_set_suburi (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_media_item_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperMediaItem *self = CLAPPER_MEDIA_ITEM_CAST (object); + + switch (prop_id) { + case PROP_ID: + g_value_set_uint (value, clapper_media_item_get_id (self)); + break; + case PROP_URI: + g_value_set_string (value, clapper_media_item_get_uri (self)); + break; + case PROP_SUBURI: + g_value_take_string (value, clapper_media_item_get_suburi (self)); + break; + case PROP_TITLE: + g_value_take_string (value, clapper_media_item_get_title (self)); + break; + case PROP_CONTAINER_FORMAT: + g_value_take_string (value, clapper_media_item_get_container_format (self)); + break; + case PROP_DURATION: + g_value_set_double (value, clapper_media_item_get_duration (self)); + break; + case PROP_TIMELINE: + g_value_set_object (value, clapper_media_item_get_timeline (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_media_item_class_init (ClapperMediaItemClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappermediaitem", 0, + "Clapper Media Item"); + + gobject_class->constructed = clapper_media_item_constructed; + gobject_class->set_property = clapper_media_item_set_property; + gobject_class->get_property = clapper_media_item_get_property; + gobject_class->finalize = clapper_media_item_finalize; + + /** + * ClapperMediaItem:id: + * + * Media Item ID. + */ + param_specs[PROP_ID] = g_param_spec_uint ("id", + NULL, NULL, 0, G_MAXUINT, G_MAXUINT, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMediaItem:uri: + * + * Media URI. + */ + param_specs[PROP_URI] = g_param_spec_string ("uri", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMediaItem:suburi: + * + * Media additional URI. + */ + param_specs[PROP_SUBURI] = g_param_spec_string ("suburi", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMediaItem:title: + * + * Media title. + */ + param_specs[PROP_TITLE] = g_param_spec_string ("title", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMediaItem:container-format: + * + * Media container format. + */ + param_specs[PROP_CONTAINER_FORMAT] = g_param_spec_string ("container-format", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMediaItem:duration: + * + * Media duration as a decimal number in seconds. + */ + param_specs[PROP_DURATION] = g_param_spec_double ("duration", + NULL, NULL, 0, G_MAXDOUBLE, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:timeline: + * + * Media timeline. + */ + param_specs[PROP_TIMELINE] = g_param_spec_object ("timeline", + NULL, NULL, CLAPPER_TYPE_TIMELINE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-media-item.h b/src/lib/clapper/clapper-media-item.h new file mode 100644 index 00000000..16b6441a --- /dev/null +++ b/src/lib/clapper/clapper-media-item.h @@ -0,0 +1,60 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include +#include + +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_MEDIA_ITEM (clapper_media_item_get_type()) +#define CLAPPER_MEDIA_ITEM_CAST(obj) ((ClapperMediaItem *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperMediaItem, clapper_media_item, CLAPPER, MEDIA_ITEM, GstObject) + +ClapperMediaItem * clapper_media_item_new (const gchar *uri); + +ClapperMediaItem * clapper_media_item_new_from_file (GFile *file); + +guint clapper_media_item_get_id (ClapperMediaItem *item); + +const gchar * clapper_media_item_get_uri (ClapperMediaItem *item); + +void clapper_media_item_set_suburi (ClapperMediaItem *item, const gchar *suburi); + +gchar * clapper_media_item_get_suburi (ClapperMediaItem *item); + +gchar * clapper_media_item_get_title (ClapperMediaItem *item); + +gchar * clapper_media_item_get_container_format (ClapperMediaItem *item); + +gdouble clapper_media_item_get_duration (ClapperMediaItem *item); + +ClapperTimeline * clapper_media_item_get_timeline (ClapperMediaItem *item); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-playbin-bus-private.h b/src/lib/clapper/clapper-playbin-bus-private.h new file mode 100644 index 00000000..260b27cb --- /dev/null +++ b/src/lib/clapper/clapper-playbin-bus-private.h @@ -0,0 +1,53 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-enums-private.h" +#include "clapper-player.h" +#include "clapper-media-item.h" + +G_BEGIN_DECLS + +void clapper_playbin_bus_initialize (void); + +gboolean clapper_playbin_bus_message_func (GstBus *bus, GstMessage *msg, ClapperPlayer *player); + +void clapper_playbin_bus_post_set_volume (GstBus *bus, GstElement *playbin, gdouble volume); + +void clapper_playbin_bus_post_set_prop (GstBus *bus, GstObject *src, const gchar *name, GValue *value); + +void clapper_playbin_bus_post_set_play_flag (GstBus *bus, ClapperPlayerPlayFlags flag, gboolean enabled); + +void clapper_playbin_bus_post_request_state (GstBus *bus, ClapperPlayer *player, GstState state); + +void clapper_playbin_bus_post_seek (GstBus *bus, gdouble position, ClapperPlayerSeekMethod flags); + +void clapper_playbin_bus_post_rate_change (GstBus *bus, gdouble rate); + +void clapper_playbin_bus_post_stream_change (GstBus *bus); + +void clapper_playbin_bus_post_current_item_change (GstBus *bus, ClapperMediaItem *current_item, ClapperQueueItemChangeMode mode); + +void clapper_playbin_bus_post_item_suburi_change (GstBus *bus, ClapperMediaItem *item); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c new file mode 100644 index 00000000..7243c596 --- /dev/null +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -0,0 +1,1147 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "clapper-bus-private.h" +#include "clapper-playbin-bus-private.h" +#include "clapper-app-bus-private.h" +#include "clapper-player-private.h" +#include "clapper-queue-private.h" +#include "clapper-media-item-private.h" +#include "clapper-timeline-private.h" +#include "clapper-stream-private.h" +#include "clapper-stream-list-private.h" + +#define GST_CAT_DEFAULT clapper_playbin_bus_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +enum +{ + CLAPPER_PLAYBIN_BUS_STRUCTURE_UNKNOWN = 0, + CLAPPER_PLAYBIN_BUS_STRUCTURE_SET_PROP, + CLAPPER_PLAYBIN_BUS_STRUCTURE_SET_PLAY_FLAG, + CLAPPER_PLAYBIN_BUS_STRUCTURE_SEEK, + CLAPPER_PLAYBIN_BUS_STRUCTURE_RATE_CHANGE, + CLAPPER_PLAYBIN_BUS_STRUCTURE_STREAM_CHANGE, + CLAPPER_PLAYBIN_BUS_STRUCTURE_CURRENT_ITEM_CHANGE, + CLAPPER_PLAYBIN_BUS_STRUCTURE_ITEM_SUBURI_CHANGE +}; + +static ClapperBusQuark _structure_quarks[] = { + {"unknown", 0}, + {"set-prop", 0}, + {"set-play-flag", 0}, + {"seek", 0}, + {"rate-change", 0}, + {"stream-change", 0}, + {"current-item-change", 0}, + {"item-suburi-change", 0}, + {NULL, 0} +}; + +enum +{ + CLAPPER_PLAYBIN_BUS_FIELD_UNKNOWN = 0, + CLAPPER_PLAYBIN_BUS_FIELD_NAME, + CLAPPER_PLAYBIN_BUS_FIELD_VALUE, + CLAPPER_PLAYBIN_BUS_FIELD_FLAG, + CLAPPER_PLAYBIN_BUS_FIELD_POSITION, + CLAPPER_PLAYBIN_BUS_FIELD_RATE, + CLAPPER_PLAYBIN_BUS_FIELD_SEEK_METHOD, + CLAPPER_PLAYBIN_BUS_FIELD_MEDIA_ITEM, + CLAPPER_PLAYBIN_BUS_FIELD_ITEM_CHANGE_MODE +}; + +static ClapperBusQuark _field_quarks[] = { + {"unknown", 0}, + {"name", 0}, + {"value", 0}, + {"flag", 0}, + {"position", 0}, + {"rate", 0}, + {"seek-method", 0}, + {"media-item", 0}, + {"item-change-mode", 0}, + {NULL, 0} +}; + +#define _STRUCTURE_QUARK(q) (_structure_quarks[CLAPPER_PLAYBIN_BUS_STRUCTURE_##q].quark) +#define _FIELD_QUARK(q) (_field_quarks[CLAPPER_PLAYBIN_BUS_FIELD_##q].quark) +#define _FIELD_NAME(q) (_field_quarks[CLAPPER_PLAYBIN_BUS_FIELD_##q].name) +#define _MESSAGE_SRC_GOBJECT(msg) ((GObject *) GST_MESSAGE_SRC (msg)) + +void +clapper_playbin_bus_initialize (void) +{ + guint i; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperplaybinbus", 0, + "Clapper Playbin Bus"); + + for (i = 0; _structure_quarks[i].name; ++i) + _structure_quarks[i].quark = g_quark_from_static_string (_structure_quarks[i].name); + for (i = 0; _field_quarks[i].name; ++i) + _field_quarks[i].quark = g_quark_from_static_string (_field_quarks[i].name); +} +/* +static gboolean +_set_object_prop (GQuark field_id, const GValue *value, GstObject *object) +{ + const gchar *prop_name = g_quark_to_string (field_id); + + GST_DEBUG ("Setting %s property: %s", GST_OBJECT_NAME (object), prop_name); + g_object_set_property (G_OBJECT (object), prop_name, value); + + return G_SOURCE_CONTINUE; +} +*/ + +static void +_perform_flush_seek (ClapperPlayer *player) +{ + GstEvent *seek_event; + GstSeekFlags flags = GST_SEEK_FLAG_FLUSH; + gint64 position = GST_CLOCK_TIME_NONE; + gdouble rate = clapper_player_get_speed (player); + + if (rate != 1.0) + flags |= GST_SEEK_FLAG_TRICKMODE; + + if (gst_element_query (player->playbin, player->position_query)) + gst_query_parse_position (player->position_query, NULL, &position); + + if (rate >= 0) { + seek_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, + GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE); + } else { + seek_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, + GST_SEEK_TYPE_SET, G_GINT64_CONSTANT (0), GST_SEEK_TYPE_SET, position); + } + clapper_player_remove_tick_source (player); + + GST_DEBUG_OBJECT (player, "Flush seeking with rate %.2lf to: %" GST_TIME_FORMAT, + rate, GST_TIME_ARGS (position)); + + if (!gst_element_send_event (player->playbin, seek_event)) + GST_WARNING_OBJECT (player, "Could not perform a flush seek"); +} + +static void +_update_current_duration (ClapperPlayer *player) +{ + gint64 duration; + + if (!gst_element_query_duration (player->playbin, GST_FORMAT_TIME, &duration)) + return; + + if (G_UNLIKELY (duration < 0)) + duration = 0; + + if (G_LIKELY (player->played_item != NULL)) { + gdouble duration_dbl = (gdouble) duration / GST_SECOND; + + if (clapper_media_item_set_duration (player->played_item, duration_dbl, player->app_bus)) { + ClapperFeaturesManager *features_manager; + + if ((features_manager = clapper_player_get_features_manager (player))) + clapper_features_manager_trigger_item_updated (features_manager, player->played_item); + } + } +} + +static inline void +_handle_warning_msg (GstMessage *msg, ClapperPlayer *player) +{ + GError *error = NULL; + gchar *debug_info = NULL; + guint signal_id; + + gst_message_parse_warning (msg, &error, &debug_info); + GST_WARNING_OBJECT (player, "Warning: %s", error->message); + + signal_id = g_signal_lookup ("warning", CLAPPER_TYPE_PLAYER); + + clapper_app_bus_post_error_signal (player->app_bus, + GST_OBJECT_CAST (player), signal_id, error, debug_info); + + g_clear_error (&error); + g_free (debug_info); +} + +static inline void +_handle_error_msg (GstMessage *msg, ClapperPlayer *player) +{ + GError *error = NULL; + gchar *debug_info = NULL; + guint signal_id; + + gst_message_parse_error (msg, &error, &debug_info); + GST_ERROR_OBJECT (player, "Error: %s", error->message); + + GST_OBJECT_LOCK (player); + player->had_error = TRUE; + GST_OBJECT_UNLOCK (player); + + /* Remove position query, since there was an error */ + clapper_player_remove_tick_source (player); + + /* After error we should go to READY, so all elements will stop processing buffers */ + gst_element_set_state (player->playbin, GST_STATE_READY); + + signal_id = g_signal_lookup ("error", CLAPPER_TYPE_PLAYER); + + clapper_app_bus_post_error_signal (player->app_bus, + GST_OBJECT_CAST (player), signal_id, error, debug_info); + + g_clear_error (&error); + g_free (debug_info); +} + +static inline void +_handle_buffering_msg (GstMessage *msg, ClapperPlayer *player) +{ + gint percent; + gboolean is_buffering; + + gst_message_parse_buffering (msg, &percent); + GST_LOG_OBJECT (player, "Buffering: %i%%", percent); + + is_buffering = (percent < 100); + + /* If no change return */ + if (player->is_buffering == is_buffering) + return; + + player->is_buffering = is_buffering; + + /* When buffering we need to manually refresh to enter buffering state + * while later playbin PLAYING state message will trigger leave */ + if (player->is_buffering || player->target_state < GST_STATE_PLAYING) + clapper_player_handle_playbin_state_changed (player); + + /* TODO: Review this code later */ + if (player->target_state > GST_STATE_PAUSED) { + GstStateChangeReturn ret; + + ret = gst_element_set_state (player->playbin, + (is_buffering) ? GST_STATE_PAUSED : GST_STATE_PLAYING); + + if (ret == GST_STATE_CHANGE_FAILURE) + GST_FIXME_OBJECT (player, "HANDLE BUFFERING STATE CHANGE ERROR"); + } +} + +void +clapper_playbin_bus_post_set_volume (GstBus *bus, GstElement *playbin, gdouble volume) +{ + GValue value = G_VALUE_INIT; + gdouble volume_linear; + + volume_linear = gst_stream_volume_convert_volume ( + GST_STREAM_VOLUME_FORMAT_CUBIC, + GST_STREAM_VOLUME_FORMAT_LINEAR, + volume); + + g_value_init (&value, G_TYPE_DOUBLE); + g_value_set_double (&value, volume_linear); + + clapper_playbin_bus_post_set_prop (bus, GST_OBJECT_CAST (playbin), "volume", &value); +} + +/* GValue is transfer-full!!! */ +void +clapper_playbin_bus_post_set_prop (GstBus *bus, GstObject *src, + const gchar *name, GValue *value) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SET_PROP), + _FIELD_QUARK (NAME), G_TYPE_STRING, name, + NULL); + gst_structure_id_take_value (structure, _FIELD_QUARK (VALUE), value); + gst_bus_post (bus, gst_message_new_application (src, structure)); +} + +static inline void +_handle_set_prop_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + const gchar *prop_name = gst_structure_get_string (structure, _FIELD_NAME (NAME)); + const GValue *value = gst_structure_id_get_value (structure, _FIELD_QUARK (VALUE)); + + /* We cannot change some playbin properties, until pipeline is running. + * Notify user about change immediatelly and we will apply value on preroll. */ + if (GST_MESSAGE_SRC (msg) == GST_OBJECT_CAST (player->playbin) + && player->current_state <= GST_STATE_READY) { + if (strcmp (prop_name, "volume") == 0) { + clapper_player_handle_playbin_volume_changed (player, value); + return; + } else if (strcmp (prop_name, "mute") == 0) { + clapper_player_handle_playbin_mute_changed (player, value); + return; + } + } + + GST_DEBUG ("Setting %s property: %s", GST_OBJECT_NAME (GST_MESSAGE_SRC (msg)), prop_name); + g_object_set_property (_MESSAGE_SRC_GOBJECT (msg), prop_name, value); +} + +void +clapper_playbin_bus_post_set_play_flag (GstBus *bus, + ClapperPlayerPlayFlags flag, gboolean enabled) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SET_PLAY_FLAG), + _FIELD_QUARK (FLAG), G_TYPE_FLAGS, flag, + _FIELD_QUARK (VALUE), G_TYPE_BOOLEAN, enabled, + NULL); + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_handle_set_play_flag_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + ClapperPlayerPlayFlags flag = 0; + gboolean enabled, enable = FALSE; + gint flags = 0; + + gst_structure_id_get (structure, + _FIELD_QUARK (FLAG), G_TYPE_FLAGS, &flag, + _FIELD_QUARK (VALUE), G_TYPE_BOOLEAN, &enable, + NULL); + + g_object_get (player->playbin, "flags", &flags, NULL); + enabled = ((flags & flag) == flag); + + if (enabled != enable) { + if (enable) + flags |= flag; + else + flags &= ~flag; + + GST_DEBUG_OBJECT (player, "%sabling play flag: %i", (enable) ? "En" : "Dis", flag); + g_object_set (player->playbin, "flags", flags, NULL); + } +} + +void +clapper_playbin_bus_post_request_state (GstBus *bus, ClapperPlayer *player, GstState state) +{ + gst_bus_post (bus, gst_message_new_request_state (GST_OBJECT_CAST (player), state)); +} + +static inline void +_handle_request_state_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstState state; + + gst_message_parse_request_state (msg, &state); + + if (state > GST_STATE_READY) { + gboolean has_item; + + GST_OBJECT_LOCK (player); + has_item = (player->played_item || player->pending_item); + GST_OBJECT_UNLOCK (player); + + if (!has_item) + return; + } + + /* If message came from player, update user requested target state */ + if (GST_MESSAGE_SRC (msg) == GST_OBJECT_CAST (player)) + player->target_state = state; + + /* FIXME: Also ignore play/pause call for live content */ + + /* Ignore play/pause state requests if we are buffering, + * just update target state for later */ + if (player->is_buffering && state > GST_STATE_READY) + return; + + GST_DEBUG_OBJECT (player, "Changing state to: %s", + gst_element_state_get_name (state)); + gst_element_set_state (player->playbin, state); +} + +void +clapper_playbin_bus_post_seek (GstBus *bus, gdouble position, ClapperPlayerSeekMethod seek_method) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SEEK), + _FIELD_QUARK (POSITION), G_TYPE_INT64, (gint64) (position * GST_SECOND), + _FIELD_QUARK (SEEK_METHOD), G_TYPE_ENUM, seek_method, + NULL); + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_handle_seek_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + GstEvent *seek_event; + gint64 position = 0; + gdouble rate; + ClapperPlayerSeekMethod seek_method = CLAPPER_PLAYER_SEEK_METHOD_NORMAL; + GstSeekFlags flags = GST_SEEK_FLAG_FLUSH; + + /* We should ignore seek if pipeline is going to be stopped */ + if (player->target_state < GST_STATE_PAUSED) + return; + + gst_structure_id_get (structure, + _FIELD_QUARK (POSITION), G_TYPE_INT64, &position, + _FIELD_QUARK (SEEK_METHOD), G_TYPE_ENUM, &seek_method, + NULL); + + /* If we are starting playback, do a seek after preroll */ + if (player->current_state < GST_STATE_PAUSED) { + player->pending_position = (gdouble) position / GST_SECOND; + return; + } + + switch (seek_method) { + case CLAPPER_PLAYER_SEEK_METHOD_FAST: + flags |= (GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_SNAP_NEAREST); + break; + case CLAPPER_PLAYER_SEEK_METHOD_NORMAL: + break; + case CLAPPER_PLAYER_SEEK_METHOD_ACCURATE: + flags |= GST_SEEK_FLAG_ACCURATE; + break; + default: + g_assert_not_reached (); + break; + } + + rate = clapper_player_get_speed (player); + + if (rate != 1.0) + flags |= GST_SEEK_FLAG_TRICKMODE; + + if (rate >= 0) { + seek_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, + GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE); + } else { + seek_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, + GST_SEEK_TYPE_SET, G_GINT64_CONSTANT (0), GST_SEEK_TYPE_SET, position); + } + + GST_DEBUG ("Seeking with rate %.2lf to: %" GST_TIME_FORMAT, + rate, GST_TIME_ARGS (position)); + + clapper_player_remove_tick_source (player); + + if (!(player->seeking = gst_element_send_event (player->playbin, seek_event))) { + /* FIXME: Should we maybe call _handle_error_msg with + * some error here? Or will playbin post such message for us? */ + GST_ERROR ("Could not seek"); + } +} + +void +clapper_playbin_bus_post_rate_change (GstBus *bus, gdouble rate) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (RATE_CHANGE), + _FIELD_QUARK (RATE), G_TYPE_DOUBLE, rate, + NULL); + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_handle_rate_change_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + GstEvent *seek_event; + gint64 position = GST_CLOCK_TIME_NONE; + GstSeekType seek_type = GST_SEEK_TYPE_NONE; + GstSeekFlags flags = GST_SEEK_FLAG_NONE; + gdouble /* current_rate,*/ rate = 1.0; + + gst_structure_id_get (structure, + _FIELD_QUARK (RATE), G_TYPE_DOUBLE, &rate, + NULL); + + if (player->speed_changing && player->requested_speed != 0) { + player->pending_speed = rate; + return; + } + + /* We cannot perform playback rate changes until pipeline is running. + * Notify user about change immediatelly and we will apply value on preroll. */ + if (player->current_state < GST_STATE_PAUSED + || player->target_state < GST_STATE_PAUSED) { + clapper_player_handle_playbin_rate_changed (player, rate); + return; + } + + /* FIXME: Using GST_SEEK_FLAG_INSTANT_RATE_CHANGE, audio-filter stops + * working with playbin2 and seek event fails with playbin3 :-( */ +#if 0 + /* We can only do instant rate changes without flushing when + * playback direction stays the same. Otherwise get most current + * position, so we can seek back as close to it as possible. */ + current_rate = clapper_player_get_speed (player); + + if ((rate < 0 && current_rate < 0) || (rate > 0 && current_rate > 0)) { +#else + if (FALSE) { +#endif + flags |= GST_SEEK_FLAG_INSTANT_RATE_CHANGE; + } else { + seek_type = GST_SEEK_TYPE_SET; + flags |= GST_SEEK_FLAG_FLUSH; + + if (gst_element_query (player->playbin, player->position_query)) + gst_query_parse_position (player->position_query, NULL, &position); + } + + /* Round playback rate to 1.0 */ + if (G_APPROX_VALUE (rate, 1.0, FLT_EPSILON)) + rate = 1.0; + + if (rate != 1.0) + flags |= GST_SEEK_FLAG_TRICKMODE; + + if (rate >= 0) { + seek_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, + seek_type, position, seek_type, GST_CLOCK_TIME_NONE); + } else { + seek_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, + seek_type, (position < 0) ? GST_CLOCK_TIME_NONE : G_GINT64_CONSTANT (0), + seek_type, position); + } + + GST_DEBUG_OBJECT (player, "Changing rate to: %.2lf", rate); + + /* Similarly as in normal seek */ + if ((flags & GST_SEEK_FLAG_INSTANT_RATE_CHANGE) == 0) + clapper_player_remove_tick_source (player); + + if (gst_element_send_event (player->playbin, seek_event)) { + if ((flags & GST_SEEK_FLAG_INSTANT_RATE_CHANGE) == 0) { + player->requested_speed = rate; + player->speed_changing = TRUE; + } else { + player->requested_speed = 0; + player->pending_speed = 0; + player->speed_changing = FALSE; + clapper_player_handle_playbin_rate_changed (player, rate); + } + } else { + /* FIXME: Should we maybe call _handle_error_msg with + * some error here? Or will playbin post such message for us? */ + GST_ERROR ("Could not change rate"); + } +} + +static inline void +_handle_state_changed_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstState old_state, pending_state; + gboolean preroll, eos; + + /* We only care about our parent bin state changes */ + if (GST_MESSAGE_SRC (msg) != GST_OBJECT_CAST (player->playbin)) + return; + + gst_message_parse_state_changed (msg, &old_state, &player->current_state, &pending_state); + GST_LOG_OBJECT (player, "State changed, old: %i, current: %i, pending: %i", + old_state, player->current_state, pending_state); + + /* Seek operation is progressing as expected. Return as we do not + * want to change ClapperPlayerState when seeking or rate changing. */ + if ((player->seeking || player->speed_changing) + && player->current_state > GST_STATE_READY) + return; + + if ((eos = (player->pending_eos && player->current_state == GST_STATE_PAUSED))) + player->pending_eos = FALSE; + + g_atomic_int_set (&player->eos, (gint) eos); + + if (player->current_state <= GST_STATE_READY) + clapper_player_reset (player, FALSE); + + if (player->current_state == GST_STATE_PLAYING) + clapper_player_add_tick_source (player); + else + clapper_player_remove_tick_source (player); + + /* Notify user about current position either right before or after + * changed playback (so it does not look like seek after paused) */ + if (player->current_state < old_state) + clapper_player_refresh_position (player); + + clapper_player_handle_playbin_state_changed (player); + + if (player->current_state > old_state) + clapper_player_refresh_position (player); + + preroll = (old_state == GST_STATE_READY + && player->current_state == GST_STATE_PAUSED + && (pending_state == GST_STATE_VOID_PENDING || pending_state == GST_STATE_PLAYING)); + + if (preroll) { + gdouble speed; + + GST_DEBUG ("Setting cached playbin props after preroll"); + + clapper_player_set_volume (player, clapper_player_get_volume (player)); + clapper_player_set_mute (player, clapper_player_get_mute (player)); + + speed = clapper_player_get_speed (player); + + /* Playback always starts with normal speed and from zero. + * When not changed do not post seek event. */ + if (!G_APPROX_VALUE (speed, 1.0, FLT_EPSILON)) + clapper_player_set_speed (player, speed); + if (!G_APPROX_VALUE (player->pending_position, 0, FLT_EPSILON)) { + clapper_player_seek (player, player->pending_position); + player->pending_position = 0; + } + + _update_current_duration (player); + } +} + +void +clapper_playbin_bus_post_current_item_change (GstBus *bus, ClapperMediaItem *current_item, + ClapperQueueItemChangeMode mode) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (CURRENT_ITEM_CHANGE), + _FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, current_item, + _FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_ENUM, mode, + NULL); + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_handle_current_item_change_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + ClapperMediaItem *current_item = NULL; + ClapperQueueItemChangeMode mode = CLAPPER_QUEUE_ITEM_CHANGE_NORMAL; + + gst_structure_id_get (structure, + _FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, ¤t_item, + _FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_ENUM, &mode, + NULL); + + player->pending_position = 0; // We store pending position for played item, so reset + + if (player->current_state < GST_STATE_READY || mode == CLAPPER_QUEUE_ITEM_CHANGE_NORMAL) + gst_element_set_state (player->playbin, GST_STATE_READY); + + clapper_player_set_pending_item (player, current_item, mode); + + if (!current_item) { + player->target_state = GST_STATE_READY; + } else { + GST_OBJECT_LOCK (player); + if (player->autoplay) + player->target_state = GST_STATE_PLAYING; + GST_OBJECT_UNLOCK (player); + } + + if ((mode == CLAPPER_QUEUE_ITEM_CHANGE_NORMAL && player->target_state > GST_STATE_READY) + || player->current_state != player->target_state) + gst_element_set_state (player->playbin, player->target_state); + + gst_clear_object (¤t_item); +} + +void +clapper_playbin_bus_post_item_suburi_change (GstBus *bus, ClapperMediaItem *item) +{ + GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (ITEM_SUBURI_CHANGE), + _FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, item, + NULL); + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_handle_item_suburi_change_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + ClapperMediaItem *item = NULL; + + gst_structure_id_get (structure, + _FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, &item, + NULL); + + if (item == player->played_item) { + gst_element_set_state (player->playbin, GST_STATE_READY); + clapper_player_set_pending_item (player, item, CLAPPER_QUEUE_ITEM_CHANGE_NORMAL); + gst_element_set_state (player->playbin, player->target_state); + } + + gst_object_unref (item); +} + +void +clapper_playbin_bus_post_stream_change (GstBus *bus) +{ + GstStructure *structure = gst_structure_new_id_empty (_STRUCTURE_QUARK (STREAM_CHANGE)); + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_handle_stream_change_msg (GstMessage *msg, + const GstStructure *structure G_GNUC_UNUSED, ClapperPlayer *player) +{ + GST_DEBUG_OBJECT (player, "Requested stream change"); + + if (player->use_playbin3) { + GList *list = NULL; + ClapperStreamList *vstream_list, *astream_list, *sstream_list; + ClapperStream *vstream = NULL, *astream = NULL, *sstream = NULL; + + vstream_list = clapper_player_get_video_streams (player); + if ((vstream = clapper_stream_list_get_current_stream (vstream_list))) { + GstStream *gst_stream = clapper_stream_get_gst_stream (vstream); + list = g_list_append (list, (gpointer) gst_stream_get_stream_id (gst_stream)); + } + + astream_list = clapper_player_get_audio_streams (player); + if ((astream = clapper_stream_list_get_current_stream (astream_list))) { + GstStream *gst_stream = clapper_stream_get_gst_stream (astream); + list = g_list_append (list, (gpointer) gst_stream_get_stream_id (gst_stream)); + } + + sstream_list = clapper_player_get_subtitle_streams (player); + if ((sstream = clapper_stream_list_get_current_stream (sstream_list))) { + GstStream *gst_stream = clapper_stream_get_gst_stream (sstream); + list = g_list_append (list, (gpointer) gst_stream_get_stream_id (gst_stream)); + } + + if (list) { + if (gst_element_send_event (player->playbin, gst_event_new_select_streams (list)) + && player->current_state >= GST_STATE_PAUSED) { + /* XXX: I am not sure if we "officially" need to flush seek after select + * streams, but as of GStreamer 1.22 it doesn't work otherwise. */ + _perform_flush_seek (player); + } + g_list_free (list); + } + + /* Need to hold ref until after event is + * sent to ensure ID pointer lifespan */ + gst_clear_object (&vstream); + gst_clear_object (&astream); + gst_clear_object (&sstream); + } else { + ClapperStreamList *stream_list; + gint current_video = -1, current_audio = -1, current_text = -1; + guint index; + + g_object_get (player->playbin, + "current-video", ¤t_video, + "current-audio", ¤t_audio, + "current-text", ¤t_text, NULL); + + stream_list = clapper_player_get_video_streams (player); + index = clapper_stream_list_get_current_index (stream_list); + + if (index != (guint) current_video) + g_object_set (player->playbin, "current-video", index, NULL); + + stream_list = clapper_player_get_audio_streams (player); + index = clapper_stream_list_get_current_index (stream_list); + + if (index != (guint) current_audio) + g_object_set (player->playbin, "current-audio", index, NULL); + + stream_list = clapper_player_get_subtitle_streams (player); + index = clapper_stream_list_get_current_index (stream_list); + + if (index != (guint) current_text) + g_object_set (player->playbin, "current-text", index, NULL); + } +} + +static inline void +_handle_app_msg (GstMessage *msg, ClapperPlayer *player) +{ + const GstStructure *structure = gst_message_get_structure (msg); + GQuark quark = gst_structure_get_name_id (structure); + + if (quark == _STRUCTURE_QUARK (SET_PROP)) + _handle_set_prop_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (SET_PLAY_FLAG)) + _handle_set_play_flag_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (SEEK)) + _handle_seek_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (RATE_CHANGE)) + _handle_rate_change_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (STREAM_CHANGE)) + _handle_stream_change_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (CURRENT_ITEM_CHANGE)) + _handle_current_item_change_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (ITEM_SUBURI_CHANGE)) + _handle_item_suburi_change_msg (msg, structure, player); +} + +static inline void +_handle_element_msg (GstMessage *msg, ClapperPlayer *player) +{ + if (gst_is_missing_plugin_message (msg)) { + gchar *name, *details; + guint signal_id; + + name = gst_missing_plugin_message_get_description (msg); + details = gst_missing_plugin_message_get_installer_detail (msg); + signal_id = g_signal_lookup ("missing-plugin", CLAPPER_TYPE_PLAYER); + + clapper_app_bus_post_desc_with_details_signal (player->app_bus, + GST_OBJECT_CAST (player), signal_id, name, details); + + g_free (name); + g_free (details); + } +} + +static inline void +_handle_tag_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstObject *src = GST_MESSAGE_SRC (msg); + GstTagList *tags = NULL; + + /* Tag messages should only be posted by sink elements */ + if (G_UNLIKELY (!src)) + return; + + gst_message_parse_tag (msg, &tags); + + GST_LOG_OBJECT (player, "Got tags from element: %s: %" GST_PTR_FORMAT, + GST_OBJECT_NAME (src), tags); + + if (G_LIKELY (player->played_item != NULL)) + clapper_media_item_update_from_tag_list (player->played_item, tags, player); + + gst_tag_list_unref (tags); +} + +static inline void +_handle_toc_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstObject *src = GST_MESSAGE_SRC (msg); + GstToc *toc = NULL; + ClapperTimeline *timeline; + gboolean updated = FALSE; + + /* TOC messages should only be posted by sink elements after start */ + if (G_UNLIKELY (!src || !player->played_item)) + return; + + /* Either new TOC was found or previous one was updated */ + gst_message_parse_toc (msg, &toc, &updated); + + GST_DEBUG_OBJECT (player, "Got TOC (%" GST_PTR_FORMAT ")" + " from element: %s, updated: %s", + toc, GST_OBJECT_NAME (src), (updated) ? "yes" : "no"); + + timeline = clapper_media_item_get_timeline (player->played_item); + + if (clapper_timeline_set_toc (timeline, toc, updated)) { + clapper_app_bus_post_refresh_timeline (player->app_bus, + GST_OBJECT_CAST (player->played_item)); + } + + gst_toc_unref (toc); +} + +static inline void +_handle_property_notify_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstObject *src = NULL; + const gchar *prop_name = NULL; + const GValue *value = NULL; + + gst_message_parse_property_notify (msg, &src, &prop_name, &value); + GST_DEBUG ("Received info about changed %s property: %s", + GST_OBJECT_NAME (src), prop_name); + + /* Since we manually need to request elements to post this message, + * any other element posting this is unlikely */ + if (G_UNLIKELY (src != GST_OBJECT_CAST (player->playbin))) + return; + + if (strcmp (prop_name, "volume") == 0) + clapper_player_handle_playbin_volume_changed (player, value); + else if (strcmp (prop_name, "mute") == 0) + clapper_player_handle_playbin_mute_changed (player, value); + else if (strcmp (prop_name, "flags") == 0) + clapper_player_handle_playbin_flags_changed (player, value); + else if (strcmp (prop_name, "av-offset") == 0) + clapper_player_handle_playbin_av_offset_changed (player, value); + else if (strcmp (prop_name, "text-offset") == 0) + clapper_player_handle_playbin_text_offset_changed (player, value); + else + clapper_player_handle_playbin_common_prop_changed (player, prop_name); +} + +static inline void +_handle_stream_collection_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstStreamCollection *collection = NULL; + + GST_INFO_OBJECT (player, "Stream collection"); + + gst_message_parse_stream_collection (msg, &collection); + clapper_player_take_stream_collection (player, collection); +} + +static inline void +_handle_streams_selected_msg (GstMessage *msg, ClapperPlayer *player) +{ + /* NOTE: Streams selected message carries whole collection + * and allows reading actually selected streams from it + * via gst_message_streams_selected_* methods */ + + GST_INFO_OBJECT (player, "Streams selected"); + + if (player->use_playbin3) { + guint i, n_streams = gst_message_streams_selected_get_size (msg); + + for (i = 0; i < n_streams; ++i) { + GstStream *stream = gst_message_streams_selected_get_stream (msg, i); + GstStreamType stream_type = gst_stream_get_stream_type (stream); + + if ((stream_type & GST_STREAM_TYPE_VIDEO) == GST_STREAM_TYPE_VIDEO) { + if (!clapper_player_find_active_decoder_with_stream_id (player, + GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO, gst_stream_get_stream_id (stream))) + GST_DEBUG_OBJECT (player, "Active video decoder not found"); + } else if ((stream_type & GST_STREAM_TYPE_AUDIO) == GST_STREAM_TYPE_AUDIO) { + if (!clapper_player_find_active_decoder_with_stream_id (player, + GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, gst_stream_get_stream_id (stream))) + GST_DEBUG_OBJECT (player, "Active audio decoder not found"); + } + } + } else { + /* In playbin2 we do not know real stream IDs, so + * we iterate in search for all active ones */ + clapper_player_playbin_update_current_decoders (player); + } +} + +static inline void +_handle_stream_start_msg (GstMessage *msg, ClapperPlayer *player) +{ + guint group = 0; + gboolean changed; + + /* We only care about our parent bin start which + * happens after all sinks have started */ + if (GST_MESSAGE_SRC (msg) != GST_OBJECT_CAST (player->playbin)) + return; + + if (!gst_message_parse_group_id (msg, &group)) + return; + + GST_INFO_OBJECT (player, "Stream start, group: %u", group); + + GST_OBJECT_LOCK (player); + + /* This should never happen, but better be safe */ + if (G_UNLIKELY (player->pending_item == NULL)) { + GST_ERROR_OBJECT (player, "Starting some stream, but there was no pending one!"); + GST_OBJECT_UNLOCK (player); + + return; + } + + changed = gst_object_replace ((GstObject **) &player->played_item, GST_OBJECT_CAST (player->pending_item)); + gst_clear_object (&player->pending_item); + + GST_OBJECT_UNLOCK (player); + + if (G_LIKELY (changed)) { + clapper_queue_handle_played_item_changed (player->queue, player->played_item, player->app_bus); + + if (clapper_player_get_have_features (player)) + clapper_features_manager_trigger_played_item_changed (player->features_manager, player->played_item); + } + + clapper_app_bus_post_refresh_streams (player->app_bus, GST_OBJECT_CAST (player)); + + /* Update position on start after announcing item change, + * since we will not do this on state change when gapless */ + clapper_player_refresh_position (player); + + /* With playbin2 we update all decoders at once after stream start */ + if (!player->use_playbin3) + clapper_player_playbin_update_current_decoders (player); +} + +static inline void +_handle_duration_changed_msg (GstMessage *msg G_GNUC_UNUSED, ClapperPlayer *player) +{ + _update_current_duration (player); +} + +static inline void +_handle_async_done_msg (GstMessage *msg G_GNUC_UNUSED, ClapperPlayer *player) +{ + if (player->seeking) { + guint signal_id; + + player->seeking = FALSE; + + GST_DEBUG_OBJECT (player, "Seek done"); + signal_id = g_signal_lookup ("seek-done", CLAPPER_TYPE_PLAYER); + + /* Update current position first, then announce seek done */ + clapper_player_refresh_position (player); + clapper_app_bus_post_simple_signal (player->app_bus, + GST_OBJECT_CAST (player), signal_id); + } + if (player->speed_changing) { + if (player->pending_speed != 0) { + GST_DEBUG_OBJECT (player, "Changing rate to pending value: %.2lf -> %.2lf", + player->speed, player->pending_speed); + clapper_player_set_speed (player, player->pending_speed); + player->pending_speed = 0; + } else { + clapper_player_handle_playbin_rate_changed (player, player->requested_speed); + player->speed_changing = FALSE; + } + player->requested_speed = 0; + } +} + +static inline void +_handle_latency_msg (GstMessage *msg G_GNUC_UNUSED, ClapperPlayer *player) +{ + GST_LOG_OBJECT (player, "Latency changed"); + gst_bin_recalculate_latency (GST_BIN_CAST (player->playbin)); +} + +static inline void +_handle_clock_lost_msg (GstMessage *msg, ClapperPlayer *player) +{ + GstStateChangeReturn ret; + + if (player->target_state != GST_STATE_PLAYING) + return; + + GST_DEBUG_OBJECT (player, "Clock lost"); + + ret = gst_element_set_state (player->playbin, GST_STATE_PAUSED); + if (ret != GST_STATE_CHANGE_FAILURE) + ret = gst_element_set_state (player->playbin, GST_STATE_PLAYING); + + if (ret == GST_STATE_CHANGE_FAILURE) { + GstMessage *msg; + GError *error; + + error = g_error_new (GST_CORE_ERROR, GST_CORE_ERROR_STATE_CHANGE, + "Could not recover with changing state after clock was lost"); + msg = gst_message_new_error (GST_OBJECT (player), error, NULL); + + _handle_error_msg (msg, player); + + g_error_free (error); + gst_message_unref (msg); + } +} + +static inline void +_handle_eos_msg (GstMessage *msg G_GNUC_UNUSED, ClapperPlayer *player) +{ + gboolean had_error; + + /* EOS happens after "about-to-finish" if URI did not change. + * Changing items should be done in former one while pausing + * after playback here. */ + + GST_INFO_OBJECT (player, "EOS"); + + /* This is also used in another thread */ + GST_OBJECT_LOCK (player); + had_error = player->had_error; + GST_OBJECT_UNLOCK (player); + + /* Error handling already changes state to READY */ + if (G_UNLIKELY (had_error)) + return; + + if (!clapper_queue_handle_eos (player->queue, player)) { + player->pending_eos = TRUE; + gst_element_set_state (player->playbin, GST_STATE_PAUSED); + } +} + +gboolean +clapper_playbin_bus_message_func (GstBus *bus, GstMessage *msg, ClapperPlayer *player) +{ + switch (GST_MESSAGE_TYPE (msg)) { + case GST_MESSAGE_BUFFERING: + _handle_buffering_msg (msg, player); + break; + case GST_MESSAGE_REQUEST_STATE: + _handle_request_state_msg (msg, player); + break; + case GST_MESSAGE_STATE_CHANGED: + _handle_state_changed_msg (msg, player); + break; + case GST_MESSAGE_APPLICATION: + _handle_app_msg (msg, player); + break; + case GST_MESSAGE_ELEMENT: + _handle_element_msg (msg, player); + break; + case GST_MESSAGE_TAG: + _handle_tag_msg (msg, player); + break; + case GST_MESSAGE_TOC: + _handle_toc_msg (msg, player); + break; + case GST_MESSAGE_PROPERTY_NOTIFY: + _handle_property_notify_msg (msg, player); + break; + case GST_MESSAGE_STREAM_COLLECTION: + _handle_stream_collection_msg (msg, player); + break; + case GST_MESSAGE_STREAMS_SELECTED: + _handle_streams_selected_msg (msg, player); + break; + case GST_MESSAGE_STREAM_START: + _handle_stream_start_msg (msg, player); + break; + case GST_MESSAGE_DURATION_CHANGED: + _handle_duration_changed_msg (msg, player); + break; + case GST_MESSAGE_ASYNC_DONE: + _handle_async_done_msg (msg, player); + break; + case GST_MESSAGE_LATENCY: + _handle_latency_msg (msg, player); + break; + case GST_MESSAGE_CLOCK_LOST: + _handle_clock_lost_msg (msg, player); + break; + case GST_MESSAGE_EOS: + _handle_eos_msg (msg, player); + break; + case GST_MESSAGE_WARNING: + _handle_warning_msg (msg, player); + break; + case GST_MESSAGE_ERROR: + _handle_error_msg (msg, player); + break; + default: + break; + } + + return G_SOURCE_CONTINUE; +} diff --git a/src/lib/clapper/clapper-player-private.h b/src/lib/clapper/clapper-player-private.h new file mode 100644 index 00000000..0512cbbe --- /dev/null +++ b/src/lib/clapper/clapper-player-private.h @@ -0,0 +1,138 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "clapper-player.h" +#include "clapper-queue.h" +#include "clapper-enums.h" + +#include "clapper-app-bus-private.h" +#include "clapper-features-manager-private.h" + +G_BEGIN_DECLS + +#define clapper_player_set_have_features(player,have) (g_atomic_int_set (&player->have_features, (gint) have)) +#define clapper_player_get_have_features(player) (g_atomic_int_get (&player->have_features) == 1) +#define clapper_player_get_features_manager(player) (clapper_player_get_have_features(player) ? player->features_manager : NULL) + +struct _ClapperPlayer +{ + ClapperThreadedObject parent; + + ClapperQueue *queue; + + ClapperStreamList *video_streams; + ClapperStreamList *audio_streams; + ClapperStreamList *subtitle_streams; + + ClapperFeaturesManager *features_manager; + gint have_features; // atomic integer + + /* This is different from queue current item as it is used/changed only + * on player thread, so we can always update correct item without lock */ + ClapperMediaItem *played_item; + + /* Will eventually become our "played_item", can be set from + * different thread, thus needs a lock */ + ClapperMediaItem *pending_item; + + GstElement *playbin; + + GstBus *bus; + ClapperAppBus *app_bus; + + GSource *tick_source; + GstQuery *position_query; + + /* Must only be used from player thread */ + GstState current_state; // reported from playbin + GstState target_state; // state requested by user + gboolean is_buffering; + gdouble pending_position; // store seek before playback + gdouble requested_speed, pending_speed; // store speed for consecutive rate changes + + /* Stream collection */ + GstStreamCollection *collection; + gulong stream_notify_id; + + /* Extra params */ + gboolean use_playbin3; // when using playbin3 + gboolean had_error; // so we do not do stuff after error + gboolean seeking; // during seek operation + gboolean speed_changing; // during rate change operation + gboolean pending_eos; // when pausing due to EOS + gint eos; // atomic integer + + /* Playbin2 compat */ + gint n_video, n_audio, n_text; + + /* Props */ + gboolean autoplay; + gboolean mute; + gdouble volume; + gdouble speed; + gdouble position; + ClapperPlayerState state; + GstElement *video_decoder; + GstElement *audio_decoder; + gboolean video_enabled; + gboolean audio_enabled; + gboolean subtitles_enabled; + gdouble audio_offset; + gdouble subtitle_offset; +}; + +ClapperPlayer * clapper_player_get_from_ancestor (GstObject *object); + +gboolean clapper_player_refresh_position (ClapperPlayer *player); + +void clapper_player_add_tick_source (ClapperPlayer *player); + +void clapper_player_remove_tick_source (ClapperPlayer *player); + +void clapper_player_handle_playbin_state_changed (ClapperPlayer *player); + +void clapper_player_handle_playbin_volume_changed (ClapperPlayer *player, const GValue *value); + +void clapper_player_handle_playbin_mute_changed (ClapperPlayer *player, const GValue *value); + +void clapper_player_handle_playbin_flags_changed (ClapperPlayer *player, const GValue *value); + +void clapper_player_handle_playbin_av_offset_changed (ClapperPlayer *player, const GValue *value); + +void clapper_player_handle_playbin_text_offset_changed (ClapperPlayer *player, const GValue *value); + +void clapper_player_handle_playbin_common_prop_changed (ClapperPlayer *player, const gchar *prop_name); + +void clapper_player_handle_playbin_rate_changed (ClapperPlayer *player, gdouble speed); + +void clapper_player_set_pending_item (ClapperPlayer *player, ClapperMediaItem *pending_item, ClapperQueueItemChangeMode mode); + +void clapper_player_take_stream_collection (ClapperPlayer *player, GstStreamCollection *collection); + +void clapper_player_refresh_streams (ClapperPlayer *player); + +gboolean clapper_player_find_active_decoder_with_stream_id (ClapperPlayer *player, GstElementFactoryListType type, const gchar *stream_id); + +void clapper_player_playbin_update_current_decoders (ClapperPlayer *player); + +void clapper_player_reset (ClapperPlayer *player, gboolean pending_dispose); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-player.c b/src/lib/clapper/clapper-player.c new file mode 100644 index 00000000..611f2f5d --- /dev/null +++ b/src/lib/clapper/clapper-player.c @@ -0,0 +1,2340 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperPlayer: + * + * The media player object used for playback. + * + * #ClapperPlayer was written in an easy to use way, so no special GStreamer + * experience is needed to get started with making various playback applications. + * + * Scheduling media for playback is done using a [class@Clapper.Queue] upon which + * player operates. + * + * Player uses `GStreamer` internally and handles playback on a separate thread, while + * serializing all events/commands between player and the thread it was created upon + * (usually main app thread). This makes it very easy to integrate with UI toolkits + * that operate on a single thread like (but not limited to) GTK. + * + * To listen for property changes, you can connect to property "notify" signal. + */ + +#include + +#include "clapper-player.h" +#include "clapper-player-private.h" +#include "clapper-playbin-bus-private.h" +#include "clapper-app-bus-private.h" +#include "clapper-queue-private.h" +#include "clapper-media-item.h" +#include "clapper-stream-list-private.h" +#include "clapper-stream-private.h" +#include "clapper-video-stream-private.h" +#include "clapper-audio-stream-private.h" +#include "clapper-subtitle-stream-private.h" +#include "clapper-enums-private.h" +#include "clapper-utils-private.h" +#include "../shared/clapper-shared-utils-private.h" + +#define DEFAULT_AUTOPLAY FALSE +#define DEFAULT_MUTE FALSE +#define DEFAULT_VOLUME 1.0 +#define DEFAULT_SPEED 1.0 +#define DEFAULT_STATE CLAPPER_PLAYER_STATE_STOPPED +#define DEFAULT_VIDEO_ENABLED TRUE +#define DEFAULT_AUDIO_ENABLED TRUE +#define DEFAULT_SUBTITLES_ENABLED TRUE + +#define GST_CAT_DEFAULT clapper_player_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +#define parent_class clapper_player_parent_class +G_DEFINE_TYPE (ClapperPlayer, clapper_player, CLAPPER_TYPE_THREADED_OBJECT); + +enum +{ + PROP_0, + PROP_QUEUE, + PROP_VIDEO_STREAMS, + PROP_AUDIO_STREAMS, + PROP_SUBTITLE_STREAMS, + PROP_AUTOPLAY, + PROP_POSITION, + PROP_SPEED, + PROP_STATE, + PROP_MUTE, + PROP_VOLUME, + PROP_VIDEO_SINK, + PROP_AUDIO_SINK, + PROP_VIDEO_FILTER, + PROP_AUDIO_FILTER, + PROP_CURRENT_VIDEO_DECODER, + PROP_CURRENT_AUDIO_DECODER, + PROP_VIDEO_ENABLED, + PROP_AUDIO_ENABLED, + PROP_SUBTITLES_ENABLED, + PROP_AUDIO_OFFSET, + PROP_SUBTITLE_OFFSET, + PROP_SUBTITLE_FONT_DESC, + PROP_LAST +}; + +enum +{ + SIGNAL_SEEK_DONE, + SIGNAL_MISSING_PLUGIN, + SIGNAL_WARNING, + SIGNAL_ERROR, + SIGNAL_LAST +}; + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; +static guint signals[SIGNAL_LAST] = { 0, }; + +/* Properties we expose through API, thus we want to emit notify signals for them */ +static const gchar *playbin_watchlist[] = { + "volume", + "mute", + "flags", + "audio-sink", + "video-sink", + "audio-filter", + "video-filter", + "av-offset", + "text-offset", + NULL +}; + +gboolean +clapper_player_refresh_position (ClapperPlayer *self) +{ + gint64 position = GST_CLOCK_TIME_NONE; + gdouble position_dbl; + gboolean changed; + + if (gst_element_query (self->playbin, self->position_query)) + gst_query_parse_position (self->position_query, NULL, &position); + + if (position < 0) + position = 0; + + position_dbl = (gdouble) position / GST_SECOND; + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (self->position, position_dbl, FLT_EPSILON))) + self->position = position_dbl; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_LOG_OBJECT (self, "Position: %" CLAPPER_TIME_MS_FORMAT, + CLAPPER_TIME_MS_ARGS (position_dbl)); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_POSITION]); + if (clapper_player_get_have_features (self)) + clapper_features_manager_trigger_position_changed (self->features_manager, position_dbl); + } + + return G_SOURCE_CONTINUE; +} + +void +clapper_player_add_tick_source (ClapperPlayer *self) +{ + GST_OBJECT_LOCK (self); + if (!self->tick_source) { + self->tick_source = clapper_shared_utils_context_timeout_add_full ( + clapper_threaded_object_get_context (CLAPPER_THREADED_OBJECT_CAST (self)), + G_PRIORITY_DEFAULT_IDLE, 100, + (GSourceFunc) clapper_player_refresh_position, + self, NULL); + GST_TRACE_OBJECT (self, "Added tick source"); + } + GST_OBJECT_UNLOCK (self); +} + +void +clapper_player_remove_tick_source (ClapperPlayer *self) +{ + GST_OBJECT_LOCK (self); + if (self->tick_source) { + g_source_destroy (self->tick_source); + g_clear_pointer (&self->tick_source, g_source_unref); + GST_TRACE_OBJECT (self, "Removed tick source"); + } + GST_OBJECT_UNLOCK (self); +} + +void +clapper_player_handle_playbin_state_changed (ClapperPlayer *self) +{ + ClapperPlayerState state; + gboolean changed; + + if (self->is_buffering) { + state = CLAPPER_PLAYER_STATE_BUFFERING; + } else { + switch (self->current_state) { + case GST_STATE_PLAYING: + state = CLAPPER_PLAYER_STATE_PLAYING; + break; + case GST_STATE_PAUSED: + state = CLAPPER_PLAYER_STATE_PAUSED; + break; + default: + state = CLAPPER_PLAYER_STATE_STOPPED; + break; + } + } + + GST_OBJECT_LOCK (self); + if ((changed = self->state != state)) + self->state = state; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "State changed, now: %i", state); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_STATE]); + if (clapper_player_get_have_features (self)) + clapper_features_manager_trigger_state_changed (self->features_manager, state); + } +} + +/* Not using common_prop_changed() because needs linear -> cubic conversion + * before applying and can only be applied during playback */ +void +clapper_player_handle_playbin_volume_changed (ClapperPlayer *self, const GValue *value) +{ + gdouble volume, volume_linear; + gboolean changed; + + volume_linear = g_value_get_double (value); + GST_DEBUG_OBJECT (self, "Playbin volume changed, linear: %lf", volume_linear); + + volume = gst_stream_volume_convert_volume ( + GST_STREAM_VOLUME_FORMAT_LINEAR, + GST_STREAM_VOLUME_FORMAT_CUBIC, + volume_linear); + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (self->volume, volume, FLT_EPSILON))) + self->volume = volume; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Volume: %.2lf", volume); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_VOLUME]); + if (clapper_player_get_have_features (self)) + clapper_features_manager_trigger_volume_changed (self->features_manager, volume); + } +} + +/* Not using common_prop_changed() because can only be applied during playback */ +void +clapper_player_handle_playbin_mute_changed (ClapperPlayer *self, const GValue *value) +{ + gboolean mute, changed; + + mute = g_value_get_boolean (value); + GST_DEBUG_OBJECT (self, "Playbin mute changed"); + + GST_OBJECT_LOCK (self); + if ((changed = self->mute != mute)) + self->mute = mute; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Mute: %s", (mute) ? "yes" : "no"); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_MUTE]); + if (clapper_player_get_have_features (self)) + clapper_features_manager_trigger_mute_changed (self->features_manager, mute); + } +} + +void +clapper_player_handle_playbin_flags_changed (ClapperPlayer *self, const GValue *value) +{ + gint flags; + gboolean video_enabled, audio_enabled, subtitles_enabled; + gboolean video_changed, audio_changed, subtitles_changed; + + flags = g_value_get_flags (value); + + video_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_VIDEO) == CLAPPER_PLAYER_PLAY_FLAG_VIDEO); + audio_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_AUDIO) == CLAPPER_PLAYER_PLAY_FLAG_AUDIO); + subtitles_enabled = ((flags & CLAPPER_PLAYER_PLAY_FLAG_TEXT) == CLAPPER_PLAYER_PLAY_FLAG_TEXT); + + GST_OBJECT_LOCK (self); + + if ((video_changed = self->video_enabled != video_enabled)) + self->video_enabled = video_enabled; + if ((audio_changed = self->audio_enabled != audio_enabled)) + self->audio_enabled = audio_enabled; + if ((subtitles_changed = self->subtitles_enabled != subtitles_enabled)) + self->subtitles_enabled = subtitles_enabled; + + GST_OBJECT_UNLOCK (self); + + if (video_changed) { + GST_INFO_OBJECT (self, "Video enabled: %s", (video_enabled) ? "yes" : "no"); + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_VIDEO_ENABLED]); + } + if (audio_changed) { + GST_INFO_OBJECT (self, "Audio enabled: %s", (audio_enabled) ? "yes" : "no"); + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_AUDIO_ENABLED]); + } + if (subtitles_changed) { + GST_INFO_OBJECT (self, "Subtitles enabled: %s", (subtitles_enabled) ? "yes" : "no"); + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_SUBTITLES_ENABLED]); + } +} + +void +clapper_player_handle_playbin_av_offset_changed (ClapperPlayer *self, const GValue *value) +{ + gdouble offset = (gdouble) g_value_get_int64 (value) / GST_SECOND; + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (self->audio_offset, offset, FLT_EPSILON))) + self->audio_offset = offset; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Audio offset: %.2lf", offset); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_AUDIO_OFFSET]); + } +} + +void +clapper_player_handle_playbin_text_offset_changed (ClapperPlayer *self, const GValue *value) +{ + gdouble offset = (gdouble) g_value_get_int64 (value) / GST_SECOND; + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (self->subtitle_offset, offset, FLT_EPSILON))) + self->subtitle_offset = offset; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Subtitles offset: %.2lf", offset); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_SUBTITLE_OFFSET]); + } +} + +void +clapper_player_handle_playbin_common_prop_changed (ClapperPlayer *self, const gchar *prop_name) +{ + GObjectClass *gobject_class = G_OBJECT_GET_CLASS (self); + GParamSpec *pspec = g_object_class_find_property (gobject_class, prop_name); + + if (G_LIKELY (pspec != NULL)) { + GST_DEBUG_OBJECT (self, "Playbin %s changed", prop_name); + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), pspec); + } +} + +void +clapper_player_handle_playbin_rate_changed (ClapperPlayer *self, gdouble speed) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (self->speed, speed, FLT_EPSILON))) + self->speed = speed; + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Speed: %.2lf", speed); + + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_SPEED]); + if (clapper_player_get_have_features (self)) + clapper_features_manager_trigger_speed_changed (self->features_manager, speed); + } +} + +static void +clapper_player_set_current_video_decoder (ClapperPlayer *self, GstElement *element) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + changed = gst_object_replace ((GstObject **) &self->video_decoder, GST_OBJECT_CAST (element)); + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Current video decoder: %" GST_PTR_FORMAT, element); + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_VIDEO_DECODER]); + } +} + +static void +clapper_player_set_current_audio_decoder (ClapperPlayer *self, GstElement *element) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + changed = gst_object_replace ((GstObject **) &self->audio_decoder, GST_OBJECT_CAST (element)); + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Current audio decoder: %" GST_PTR_FORMAT, element); + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_AUDIO_DECODER]); + } +} + +void +clapper_player_set_pending_item (ClapperPlayer *self, ClapperMediaItem *pending_item, + ClapperQueueItemChangeMode mode) +{ + const gchar *uri = NULL; + gchar *suburi = NULL; + + /* We cannot do gapless/instant with pending suburi in place, + * do a check and if necessary use normal mode instead */ + if (mode > CLAPPER_QUEUE_ITEM_CHANGE_NORMAL) { + g_object_get (self->playbin, "suburi", &suburi, NULL); + + if (suburi) { + mode = CLAPPER_QUEUE_ITEM_CHANGE_NORMAL; + g_clear_pointer (&suburi, g_free); + } + } + + /* Might be NULL (e.g. after queue is cleared) */ + if (pending_item) { + uri = clapper_media_item_get_uri (pending_item); + suburi = clapper_media_item_get_suburi (pending_item); + } + + GST_INFO_OBJECT (self, "Changing item with mode %u, URI: \"%s\", SUBURI: \"%s\"", + mode, GST_STR_NULL (uri), GST_STR_NULL (suburi)); + + /* We need to lock here, as this function is also called from "about-to-finish" + * signal which comes from different thread and we need to change URIs in it ASAP, + * so we cannot schedule an invoke of another thread there */ + GST_OBJECT_LOCK (self); + gst_object_replace ((GstObject **) &self->pending_item, GST_OBJECT_CAST (pending_item)); + GST_OBJECT_UNLOCK (self); + + /* GStreamer does not support changing suburi in gapless/instant mode */ + if (mode == CLAPPER_QUEUE_ITEM_CHANGE_NORMAL) + g_object_set (self->playbin, "suburi", suburi, NULL); + + if (uri) { + if (mode == CLAPPER_QUEUE_ITEM_CHANGE_INSTANT) + g_object_set (self->playbin, "instant-uri", TRUE, NULL); + + g_object_set (self->playbin, "uri", uri, NULL); + + if (mode == CLAPPER_QUEUE_ITEM_CHANGE_INSTANT) + g_object_set (self->playbin, "instant-uri", FALSE, NULL); + } + + g_free (suburi); +} + +static void +_stream_notify_cb (GstStreamCollection *collection, + GstStream *gst_stream, GParamSpec *pspec, ClapperPlayer *self) +{ + GstStreamType stream_type; + ClapperStream *stream = NULL; + const gchar *pspec_name = g_param_spec_get_name (pspec); + GstCaps *caps = NULL; + GstTagList *tags = NULL; + + if (pspec_name == g_intern_string ("caps")) + caps = gst_stream_get_caps (gst_stream); + else if (pspec_name == g_intern_string ("tags")) + tags = gst_stream_get_tags (gst_stream); + else + return; + + stream_type = gst_stream_get_stream_type (gst_stream); + + if ((stream_type & GST_STREAM_TYPE_VIDEO) == GST_STREAM_TYPE_VIDEO) { + stream = clapper_stream_list_get_stream_for_gst_stream (self->video_streams, gst_stream); + } else if ((stream_type & GST_STREAM_TYPE_AUDIO) == GST_STREAM_TYPE_AUDIO) { + stream = clapper_stream_list_get_stream_for_gst_stream (self->audio_streams, gst_stream); + } else if ((stream_type & GST_STREAM_TYPE_TEXT) == GST_STREAM_TYPE_TEXT) { + stream = clapper_stream_list_get_stream_for_gst_stream (self->subtitle_streams, gst_stream); + } + + if (G_LIKELY (stream != NULL)) { + ClapperStreamClass *stream_class = CLAPPER_STREAM_GET_CLASS (stream); + + stream_class->internal_stream_updated (stream, caps, tags); + gst_object_unref (stream); + } + + gst_clear_caps (&caps); + gst_clear_tag_list (&tags); +} + +void +clapper_player_take_stream_collection (ClapperPlayer *self, GstStreamCollection *collection) +{ + GST_OBJECT_LOCK (self); + + if (self->stream_notify_id != 0) { + g_signal_handler_disconnect (self->collection, self->stream_notify_id); + self->stream_notify_id = 0; + } + gst_clear_object (&self->collection); + self->collection = collection; + + GST_OBJECT_UNLOCK (self); +} + +/* + * Must be called from main thread! + */ +void +clapper_player_refresh_streams (ClapperPlayer *self) +{ + GList *vstreams = NULL, *astreams = NULL, *sstreams = NULL; + guint i, n_streams; + + GST_TRACE_OBJECT (self, "Removing all obsolete streams"); + + GST_OBJECT_LOCK (self); + + /* We should not be connected here anymore, but better be safe */ + if (G_LIKELY (self->stream_notify_id == 0)) { + /* Initial update is done upon stream construction, thus + * we do not have to call this callback here after connecting + * (also why we connect it before constructing our streams). */ + self->stream_notify_id = g_signal_connect (self->collection, "stream-notify", + G_CALLBACK (_stream_notify_cb), self); + } + + n_streams = gst_stream_collection_get_size (self->collection); + + for (i = 0; i < n_streams; ++i) { + GstStream *gst_stream = gst_stream_collection_get_stream (self->collection, i); + GstStreamType stream_type = gst_stream_get_stream_type (gst_stream); + + GST_LOG_OBJECT (self, "Found %" GST_PTR_FORMAT, gst_stream); + + if ((stream_type & GST_STREAM_TYPE_VIDEO) == GST_STREAM_TYPE_VIDEO) { + vstreams = g_list_append (vstreams, clapper_video_stream_new (gst_stream)); + } else if ((stream_type & GST_STREAM_TYPE_AUDIO) == GST_STREAM_TYPE_AUDIO) { + astreams = g_list_append (astreams, clapper_audio_stream_new (gst_stream)); + } else if ((stream_type & GST_STREAM_TYPE_TEXT) == GST_STREAM_TYPE_TEXT) { + sstreams = g_list_append (sstreams, clapper_subtitle_stream_new (gst_stream)); + } else { + GST_WARNING_OBJECT (self, "Unhandled stream type: %s", + gst_stream_type_get_name (stream_type)); + } + } + + GST_OBJECT_UNLOCK (self); + + clapper_stream_list_replace_streams (self->video_streams, vstreams); + clapper_stream_list_replace_streams (self->audio_streams, astreams); + clapper_stream_list_replace_streams (self->subtitle_streams, sstreams); + + /* We only want to do this once for all stream lists, so + * playbin will select the same streams as we initially did */ + clapper_playbin_bus_post_stream_change (self->bus); + + if (vstreams) + g_list_free (vstreams); + if (astreams) + g_list_free (astreams); + if (sstreams) + g_list_free (sstreams); +} + +static gboolean +_iterate_decoder_pads (ClapperPlayer *self, GstElement *element, + const gchar *stream_id, GstElementFactoryListType type) +{ + GstIterator *iter; + GValue value = G_VALUE_INIT; + gboolean found = FALSE; + + iter = gst_element_iterate_src_pads (element); + + while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) { + GstPad *decoder_pad = g_value_get_object (&value); + gchar *decoder_sid = gst_pad_get_stream_id (decoder_pad); + + GST_DEBUG_OBJECT (self, "Decoder stream: %s", decoder_sid); + + if ((found = (g_strcmp0 (decoder_sid, stream_id) == 0))) { + GST_DEBUG_OBJECT (self, "Found decoder for stream: %s", stream_id); + + if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) == GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO) + clapper_player_set_current_video_decoder (self, element); + else if ((type & GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) == GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) + clapper_player_set_current_audio_decoder (self, element); + } + + g_free (decoder_sid); + g_value_unset (&value); + + if (found) + break; + } + + gst_iterator_free (iter); + + return found; +} + +gboolean +clapper_player_find_active_decoder_with_stream_id (ClapperPlayer *self, + GstElementFactoryListType type, const gchar *stream_id) +{ + GstIterator *iter; + GValue value = G_VALUE_INIT; + gboolean found = FALSE; + + GST_DEBUG_OBJECT (self, "Searching for decoder with stream: %s", stream_id); + + type |= GST_ELEMENT_FACTORY_TYPE_DECODER; + iter = gst_bin_iterate_recurse (GST_BIN_CAST (self->playbin)); + + while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) { + GstElement *element = g_value_get_object (&value); + GstElementFactory *factory = gst_element_get_factory (element); + + if (factory && gst_element_factory_list_is_type (factory, type)) + found = _iterate_decoder_pads (self, element, stream_id, type); + + g_value_unset (&value); + + if (found) + break; + } + + gst_iterator_free (iter); + + return found; +} + +/* For playbin2 only */ +void +clapper_player_playbin_update_current_decoders (ClapperPlayer *self) +{ + GstIterator *iter; + GValue value = G_VALUE_INIT; + gboolean found_video = FALSE, found_audio = FALSE; + + iter = gst_bin_iterate_all_by_element_factory_name ( + GST_BIN_CAST (self->playbin), "input-selector"); + + while (gst_iterator_next (iter, &value) == GST_ITERATOR_OK) { + GstElement *element = g_value_get_object (&value); + GstPad *active_pad; + + g_object_get (element, "active-pad", &active_pad, NULL); + + if (active_pad) { + gchar *stream_id; + + stream_id = gst_pad_get_stream_id (active_pad); + gst_object_unref (active_pad); + + if (stream_id) { + if (!found_video) { + found_video = clapper_player_find_active_decoder_with_stream_id (self, + GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO, stream_id); + } + if (!found_audio) { + found_audio = clapper_player_find_active_decoder_with_stream_id (self, + GST_ELEMENT_FACTORY_TYPE_MEDIA_AUDIO, stream_id); + } + g_free (stream_id); + } + } + + g_value_unset (&value); + + if (found_video && found_audio) + break; + } + + gst_iterator_free (iter); + + if (!found_video) + GST_DEBUG_OBJECT (self, "Active video decoder not found"); + if (!found_audio) + GST_DEBUG_OBJECT (self, "Active audio decoder not found"); +} + +void +clapper_player_reset (ClapperPlayer *self, gboolean pending_dispose) +{ + GST_OBJECT_LOCK (self); + + GST_DEBUG_OBJECT (self, "Reset"); + + self->had_error = FALSE; + gst_clear_object (&self->played_item); + + if (pending_dispose) { + gst_clear_object (&self->video_decoder); + gst_clear_object (&self->audio_decoder); + } + + GST_OBJECT_UNLOCK (self); + + /* Emit notify when we are not going to be disposed */ + if (!pending_dispose) { + /* Clear current decoders (next item might not have video/audio track) */ + clapper_player_set_current_video_decoder (self, NULL); + clapper_player_set_current_audio_decoder (self, NULL); + } +} + +static void +_about_to_finish_cb (GstElement *playbin, ClapperPlayer *self) +{ + gboolean had_error; + + GST_INFO_OBJECT (self, "About to finish"); + + /* This signal comes from different thread */ + GST_OBJECT_LOCK (self); + had_error = self->had_error; + GST_OBJECT_UNLOCK (self); + + /* We do not want to progress playlist after error */ + if (G_UNLIKELY (had_error)) + return; + + clapper_queue_handle_about_to_finish (self->queue, self); +} + +static void +_playbin_streams_changed_cb (GstElement *playbin, ClapperPlayer *self) +{ + GstStreamCollection *collection = gst_stream_collection_new (NULL); + gint i; + + GST_DEBUG_OBJECT (self, "Playbin streams changed"); + + g_object_get (playbin, "n-video", &self->n_video, NULL); + for (i = 0; i < self->n_video; ++i) { + gst_stream_collection_add_stream (collection, + gst_stream_new (NULL, NULL, GST_STREAM_TYPE_VIDEO, GST_STREAM_FLAG_NONE)); + } + + g_object_get (playbin, "n-audio", &self->n_audio, NULL); + for (i = 0; i < self->n_audio; ++i) { + gst_stream_collection_add_stream (collection, + gst_stream_new (NULL, NULL, GST_STREAM_TYPE_AUDIO, GST_STREAM_FLAG_NONE)); + } + + g_object_get (playbin, "n-text", &self->n_text, NULL); + for (i = 0; i < self->n_text; ++i) { + gst_stream_collection_add_stream (collection, + gst_stream_new (NULL, NULL, GST_STREAM_TYPE_TEXT, GST_STREAM_FLAG_NONE)); + } + + clapper_player_take_stream_collection (self, collection); +} + +static void +_playbin_tags_changed (ClapperPlayer *self, gint index, gint global_index) +{ + GstStream *gst_stream; + GstStreamType stream_type; + GstTagList *tags = NULL; + GstPad *pad = NULL; + GstCaps *caps = NULL; + + gst_stream = gst_stream_collection_get_stream (self->collection, global_index); + stream_type = gst_stream_get_stream_type (gst_stream); + + if ((stream_type & GST_STREAM_TYPE_VIDEO) == GST_STREAM_TYPE_VIDEO) { + g_signal_emit_by_name (self->playbin, "get-video-tags", index, &tags); + g_signal_emit_by_name (self->playbin, "get-video-pad", index, &pad); + } else if ((stream_type & GST_STREAM_TYPE_AUDIO) == GST_STREAM_TYPE_AUDIO) { + g_signal_emit_by_name (self->playbin, "get-audio-tags", index, &tags); + g_signal_emit_by_name (self->playbin, "get-audio-pad", index, &pad); + } else if ((stream_type & GST_STREAM_TYPE_TEXT) == GST_STREAM_TYPE_TEXT) { + g_signal_emit_by_name (self->playbin, "get-text-tags", index, &tags); + g_signal_emit_by_name (self->playbin, "get-text-pad", index, &pad); + } + + gst_stream_set_tags (gst_stream, tags); + gst_clear_tag_list (&tags); + + if (G_LIKELY (pad != NULL)) { + caps = gst_pad_get_current_caps (pad); + gst_object_unref (pad); + } + + gst_stream_set_caps (gst_stream, caps); + gst_clear_caps (&caps); +} + +static void +_playbin_video_tags_changed_cb (GstElement *playbin, gint index, ClapperPlayer *self) +{ + GST_DEBUG_OBJECT (self, "Video stream %i tags changed", index); + _playbin_tags_changed (self, index, index); +} + +static void +_playbin_audio_tags_changed_cb (GstElement *playbin, gint index, ClapperPlayer *self) +{ + GST_DEBUG_OBJECT (self, "Audio stream %i tags changed", index); + _playbin_tags_changed (self, index, self->n_video + index); +} + +static void +_playbin_text_tags_changed_cb (GstElement *playbin, gint index, ClapperPlayer *self) +{ + GST_DEBUG_OBJECT (self, "Subtitle stream %i tags changed", index); + _playbin_tags_changed (self, index, self->n_video + self->n_audio + index); +} + +static void +_playbin_selected_streams_changed_cb (GstElement *playbin, + GParamSpec *pspec G_GNUC_UNUSED, ClapperPlayer *self) +{ + GstMessage *msg; + gint current_video = 0, current_audio = 0, current_text = 0; + gboolean success = TRUE; + + msg = gst_message_new_streams_selected ( + GST_OBJECT_CAST (playbin), self->collection); + + g_object_get (playbin, + "current-video", ¤t_video, + "current-audio", ¤t_audio, + "current-text", ¤t_text, NULL); + + GST_DEBUG_OBJECT (self, "Selected streams changed, video: %i, audio: %i, text: %i", + current_video, current_audio, current_text); + + /* We cannot play text stream only, skip streams selected for now */ + if (current_video < 0 && current_audio < 0) { + success = FALSE; + goto finish; + } + + if (current_video >= 0) { + GstStream *gst_stream = gst_stream_collection_get_stream (self->collection, + current_video); + + if (gst_stream) + gst_message_streams_selected_add (msg, gst_stream); + else + success = FALSE; + } + if (current_audio >= 0) { + GstStream *gst_stream = gst_stream_collection_get_stream (self->collection, + self->n_video + current_audio); + + if (gst_stream) + gst_message_streams_selected_add (msg, gst_stream); + else + success = FALSE; + } + if (current_text >= 0) { + GstStream *gst_stream = gst_stream_collection_get_stream (self->collection, + self->n_video + self->n_audio + current_text); + + if (gst_stream) + gst_message_streams_selected_add (msg, gst_stream); + else + success = FALSE; + } + +finish: + /* Since "current-*" is changed one at a time from signal emissions, + * we might fail here to assemble everything until last signal */ + if (success) + gst_bus_post (self->bus, msg); + else + gst_message_unref (msg); +} + +ClapperPlayer * +clapper_player_get_from_ancestor (GstObject *object) +{ + GstObject *parent = gst_object_get_parent (object); + + while (parent) { + GstObject *tmp; + + if (CLAPPER_IS_PLAYER (parent)) + return CLAPPER_PLAYER_CAST (parent); + + tmp = gst_object_get_parent (parent); + gst_object_unref (parent); + parent = tmp; + } + + return NULL; +} + +/** + * clapper_player_new: + * + * Creates a new #ClapperPlayer instance. + * + * Returns: (transfer full): a new #ClapperPlayer instance. + */ +ClapperPlayer * +clapper_player_new (void) +{ + ClapperPlayer *player; + + player = g_object_new (CLAPPER_TYPE_PLAYER, NULL); + gst_object_ref_sink (player); + + return player; +} + +/** + * clapper_player_get_queue: + * @player: a #ClapperPlayer + * + * Get the #ClapperQueue of the player. + * + * The queue belongs to the player and can be accessed for as long + * as #ClapperPlayer object instance it belongs to is alive. + * + * Returns: (transfer none): the #ClapperQueue of the player. + */ +ClapperQueue * +clapper_player_get_queue (ClapperPlayer *self) +{ + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + return self->queue; +} + +/** + * clapper_player_get_video_streams: + * @player: a #ClapperPlayer + * + * Get a list of video streams within media item. + * + * Returns: (transfer none): a #ClapperStreamList of video #ClapperStream. + */ +ClapperStreamList * +clapper_player_get_video_streams (ClapperPlayer *self) +{ + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + return self->video_streams; +} + +/** + * clapper_player_get_audio_streams: + * @player: a #ClapperPlayer + * + * Get a list of audio streams within media item. + * + * Returns: (transfer none): a #ClapperStreamList of audio #ClapperStream. + */ +ClapperStreamList * +clapper_player_get_audio_streams (ClapperPlayer *self) +{ + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + return self->audio_streams; +} + +/** + * clapper_player_get_subtitle_streams: + * @player: a #ClapperPlayer + * + * Get a list of subtitle streams within media item. + * + * Returns: (transfer none): a #ClapperStreamList of subtitle #ClapperStream. + */ +ClapperStreamList * +clapper_player_get_subtitle_streams (ClapperPlayer *self) +{ + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + return self->subtitle_streams; +} + +/** + * clapper_player_set_autoplay: + * @player: a #ClapperPlayer + * @enabled: %TRUE to enable autoplay, %FALSE otherwise. + * + * Set the autoplay state of the player. + * + * When autoplay is enabled, player will always try to start + * playback after current media item changes. When disabled + * current playback state is preserved when changing items. + */ +void +clapper_player_set_autoplay (ClapperPlayer *self, gboolean autoplay) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + GST_OBJECT_LOCK (self); + if ((changed = self->autoplay != autoplay)) + self->autoplay = autoplay; + GST_OBJECT_UNLOCK (self); + + if (changed) { + clapper_app_bus_post_prop_notify (self->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_AUTOPLAY]); + } +} + +/** + * clapper_player_get_autoplay: + * @player: a #ClapperPlayer + * + * Get the autoplay value. + * + * Returns: %TRUE if autoplay is enabled, %FALSE otherwise. + */ +gboolean +clapper_player_get_autoplay (ClapperPlayer *self) +{ + gboolean autoplay; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), DEFAULT_AUTOPLAY); + + GST_OBJECT_LOCK (self); + autoplay = self->autoplay; + GST_OBJECT_UNLOCK (self); + + return autoplay; +} + +/** + * clapper_player_get_position: + * @player: a #ClapperPlayer + * + * Get the current player playback position. + * + * The returned value is in seconds as a decimal number. + * + * Returns: the position of the player. + */ +gdouble +clapper_player_get_position (ClapperPlayer *self) +{ + gdouble position; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), 0); + + GST_OBJECT_LOCK (self); + position = self->position; + GST_OBJECT_UNLOCK (self); + + return position; +} + +/** + * clapper_player_get_state: + * @player: a #ClapperPlayer + * + * Get the current #ClapperPlayerState. + * + * Returns: the #ClapperPlayerState of the player. + */ +ClapperPlayerState +clapper_player_get_state (ClapperPlayer *self) +{ + ClapperPlayerState state; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), DEFAULT_STATE); + + GST_OBJECT_LOCK (self); + state = self->state; + GST_OBJECT_UNLOCK (self); + + return state; +} + +/** + * clapper_player_set_mute: + * @player: a #ClapperPlayer + * @mute: %TRUE if player should be muted, %FALSE otherwise. + * + * Set the mute state of the player. + */ +void +clapper_player_set_mute (ClapperPlayer *self, gboolean mute) +{ + GValue value = G_VALUE_INIT; + + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + g_value_init (&value, G_TYPE_BOOLEAN); + g_value_set_boolean (&value, mute); + + clapper_playbin_bus_post_set_prop (self->bus, GST_OBJECT_CAST (self->playbin), "mute", &value); +} + +/** + * clapper_player_get_mute: + * @player: a #ClapperPlayer + * + * Get the mute state of the player. + * + * Returns: %TRUE if player is muted, %FALSE otherwise. + */ +gboolean +clapper_player_get_mute (ClapperPlayer *self) +{ + gboolean mute; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), DEFAULT_MUTE); + + GST_OBJECT_LOCK (self); + mute = self->mute; + GST_OBJECT_UNLOCK (self); + + return mute; +} + +/** + * clapper_player_set_volume: + * @player: a #ClapperPlayer + * @volume: the volume level. + * + * Set the volume of the player. + * + * The value should be within 0 - 2.0 range, where 1.0 is 100% + * volume and anything above results with an overamplification. + */ +void +clapper_player_set_volume (ClapperPlayer *self, gdouble volume) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (volume >= 0 && volume <= 2.0); + + clapper_playbin_bus_post_set_volume (self->bus, self->playbin, volume); +} + +/** + * clapper_player_get_volume: + * @player: a #ClapperPlayer + * + * Get the volume of the player. + */ +gdouble +clapper_player_get_volume (ClapperPlayer *self) +{ + gdouble volume; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), DEFAULT_VOLUME); + + GST_OBJECT_LOCK (self); + volume = self->volume; + GST_OBJECT_UNLOCK (self); + + return volume; +} + +/** + * clapper_player_set_speed: + * @player: a #ClapperPlayer + * @speed: the playback speed multiplier. + * + * Set the speed multiplier of the player. + */ +void +clapper_player_set_speed (ClapperPlayer *self, gdouble speed) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (speed != 0); + + clapper_playbin_bus_post_rate_change (self->bus, speed); +} + +/** + * clapper_player_get_speed: + * @player: a #ClapperPlayer + * + * Get the speed of the player used for playback. + * + * Returns: the playback speed multiplier. + */ +gdouble +clapper_player_get_speed (ClapperPlayer *self) +{ + gdouble speed; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), DEFAULT_SPEED); + + GST_OBJECT_LOCK (self); + speed = self->speed; + GST_OBJECT_UNLOCK (self); + + return speed; +} + +/* XXX: Also serialized into player thread, so action order like stop() -> set_sink() -> play() is not racy */ +static void +clapper_player_set_playbin_prop_element (ClapperPlayer *self, const gchar *prop_name, GstElement *element) +{ + GValue value = G_VALUE_INIT; + + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (GST_IS_ELEMENT (element)); + + g_value_init (&value, GST_TYPE_ELEMENT); + g_value_set_object (&value, element); + + clapper_playbin_bus_post_set_prop (self->bus, GST_OBJECT_CAST (self->playbin), prop_name, &value); +} + +static GstElement * +clapper_player_get_playbin_prop_element (ClapperPlayer *self, const gchar *prop_name) +{ + GstElement *element = NULL; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + g_object_get (self->playbin, prop_name, &element, NULL); + + return element; +} + +/** + * clapper_player_set_video_sink: + * @player: a #ClapperPlayer + * @element: a #GstElement. + * + * Set #GstElement to be used as video sink. + */ +void +clapper_player_set_video_sink (ClapperPlayer *self, GstElement *element) +{ + clapper_player_set_playbin_prop_element (self, "video-sink", element); +} + +/** + * clapper_player_get_video_sink: + * @player: a #ClapperPlayer + * + * Get #GstElement used as video sink. + * + * Returns: (transfer full): #GstElement set as video sink. + */ +GstElement * +clapper_player_get_video_sink (ClapperPlayer *self) +{ + return clapper_player_get_playbin_prop_element (self, "video-sink"); +} + +/** + * clapper_player_set_audio_sink: + * @player: a #ClapperPlayer + * @element: a #GstElement. + * + * Set #GstElement to be used as audio sink. + */ +void +clapper_player_set_audio_sink (ClapperPlayer *self, GstElement *element) +{ + clapper_player_set_playbin_prop_element (self, "audio-sink", element); +} + +/** + * clapper_player_get_audio_sink: + * @player: a #ClapperPlayer + * + * Get #GstElement used as audio sink. + * + * Returns: (transfer full): #GstElement set as audio sink. + */ +GstElement * +clapper_player_get_audio_sink (ClapperPlayer *self) +{ + return clapper_player_get_playbin_prop_element (self, "audio-sink"); +} + +/** + * clapper_player_set_video_filter: + * @player: a #ClapperPlayer + * @element: a #GstElement. + * + * Set #GstElement to be used as video filter. + */ +void +clapper_player_set_video_filter (ClapperPlayer *self, GstElement *element) +{ + clapper_player_set_playbin_prop_element (self, "video-filter", element); +} + +/** + * clapper_player_get_video_filter: + * @player: a #ClapperPlayer + * + * Get #GstElement used as video filter. + * + * Returns: (transfer full): #GstElement set as video filter. + */ +GstElement * +clapper_player_get_video_filter (ClapperPlayer *self) +{ + return clapper_player_get_playbin_prop_element (self, "video-filter"); +} + +/** + * clapper_player_set_audio_filter: + * @player: a #ClapperPlayer + * @element: a #GstElement. + * + * Set #GstElement to be used as audio filter. + */ +void +clapper_player_set_audio_filter (ClapperPlayer *self, GstElement *element) +{ + clapper_player_set_playbin_prop_element (self, "audio-filter", element); +} + +/** + * clapper_player_get_audio_filter: + * @player: a #ClapperPlayer + * + * Get #GstElement used as audio filter. + * + * Returns: (transfer full): #GstElement set as audio filter. + */ +GstElement * +clapper_player_get_audio_filter (ClapperPlayer *self) +{ + return clapper_player_get_playbin_prop_element (self, "audio-filter"); +} + +/** + * clapper_player_get_current_video_decoder: + * @player: a #ClapperPlayer + * + * Get #GstElement currently used as video decoder. + * + * Returns: (transfer full): #GstElement currently used as video decoder. + */ +GstElement * +clapper_player_get_current_video_decoder (ClapperPlayer *self) +{ + GstElement *element = NULL; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + GST_OBJECT_LOCK (self); + if (self->video_decoder) + element = gst_object_ref (self->video_decoder); + GST_OBJECT_UNLOCK (self); + + return element; +} + +/** + * clapper_player_get_current_audio_decoder: + * @player: a #ClapperPlayer + * + * Get #GstElement currently used as audio decoder. + * + * Returns: (transfer full): #GstElement currently used as audio decoder. + */ +GstElement * +clapper_player_get_current_audio_decoder (ClapperPlayer *self) +{ + GstElement *element = NULL; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + GST_OBJECT_LOCK (self); + if (self->audio_decoder) + element = gst_object_ref (self->audio_decoder); + GST_OBJECT_UNLOCK (self); + + return element; +} + +/** + * clapper_player_set_video_enabled: + * @player: a #ClapperPlayer + * @enabled: whether enabled + * + * Set whether enable video stream. + */ +void +clapper_player_set_video_enabled (ClapperPlayer *self, gboolean enabled) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + clapper_playbin_bus_post_set_play_flag (self->bus, CLAPPER_PLAYER_PLAY_FLAG_VIDEO, enabled); +} + +/** + * clapper_player_get_video_enabled: + * @player: a #ClapperPlayer + * + * Get whether video stream is enabled. + * + * Returns: %TRUE if enabled, %FALSE otherwise. + */ +gboolean +clapper_player_get_video_enabled (ClapperPlayer *self) +{ + gboolean enabled; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), FALSE); + + GST_OBJECT_LOCK (self); + enabled = self->video_enabled; + GST_OBJECT_UNLOCK (self); + + return enabled; +} + +/** + * clapper_player_set_audio_enabled: + * @player: a #ClapperPlayer + * @enabled: whether enabled + * + * Set whether enable audio stream. + */ +void +clapper_player_set_audio_enabled (ClapperPlayer *self, gboolean enabled) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + clapper_playbin_bus_post_set_play_flag (self->bus, CLAPPER_PLAYER_PLAY_FLAG_AUDIO, enabled); +} + +/** + * clapper_player_get_audio_enabled: + * @player: a #ClapperPlayer + * + * Get whether audio stream is enabled. + * + * Returns: %TRUE if enabled, %FALSE otherwise. + */ +gboolean +clapper_player_get_audio_enabled (ClapperPlayer *self) +{ + gboolean enabled; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), FALSE); + + GST_OBJECT_LOCK (self); + enabled = self->audio_enabled; + GST_OBJECT_UNLOCK (self); + + return enabled; +} + +/** + * clapper_player_set_subtitles_enabled: + * @player: a #ClapperPlayer + * @enabled: whether enabled + * + * Set whether subtitles should be shown if any. + */ +void +clapper_player_set_subtitles_enabled (ClapperPlayer *self, gboolean enabled) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + clapper_playbin_bus_post_set_play_flag (self->bus, CLAPPER_PLAYER_PLAY_FLAG_TEXT, enabled); +} + +/** + * clapper_player_get_subtitles_enabled: + * @player: a #ClapperPlayer + * + * Get whether subtitles are to be shown when available. + * + * Returns: %TRUE if enabled, %FALSE otherwise. + */ +gboolean +clapper_player_get_subtitles_enabled (ClapperPlayer *self) +{ + gboolean enabled; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), FALSE); + + GST_OBJECT_LOCK (self); + enabled = self->subtitles_enabled; + GST_OBJECT_UNLOCK (self); + + return enabled; +} + +/** + * clapper_player_set_audio_offset: + * @player: a #ClapperPlayer + * @offset: a decimal audio offset (in seconds) + * + * Set synchronisation offset between the audio stream and video. + * + * Positive values make the audio ahead of the video and negative + * values make the audio go behind the video. + */ +void +clapper_player_set_audio_offset (ClapperPlayer *self, gdouble offset) +{ + GValue value = G_VALUE_INIT; + + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (offset >= G_MININT64 && offset <= G_MAXINT64); + + g_value_init (&value, G_TYPE_INT64); + g_value_set_int64 (&value, (gint64) (offset * GST_SECOND)); + + clapper_playbin_bus_post_set_prop (self->bus, + GST_OBJECT_CAST (self->playbin), "av-offset", &value); +} + +/** + * clapper_player_get_audio_offset: + * @player: a #ClapperPlayer + * + * Get the currently set audio stream offset. + * + * The returned value is in seconds as a decimal number. + * + * Returns: the audio stream offset. + */ +gdouble +clapper_player_get_audio_offset (ClapperPlayer *self) +{ + gdouble offset; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), 0); + + GST_OBJECT_LOCK (self); + offset = self->audio_offset; + GST_OBJECT_UNLOCK (self); + + return offset; +} + +/** + * clapper_player_set_subtitle_offset: + * @player: a #ClapperPlayer + * @offset: a decimal subtitle stream offset (in seconds) + * + * Set synchronisation offset between the subtitle stream and video. + * + * Positive values make the subtitles ahead of the video and negative + * values make the subtitles go behind the video. + */ +void +clapper_player_set_subtitle_offset (ClapperPlayer *self, gdouble offset) +{ + GValue value = G_VALUE_INIT; + + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (offset >= G_MININT64 && offset <= G_MAXINT64); + + g_value_init (&value, G_TYPE_INT64); + g_value_set_int64 (&value, (gint64) (offset * GST_SECOND)); + + clapper_playbin_bus_post_set_prop (self->bus, + GST_OBJECT_CAST (self->playbin), "text-offset", &value); +} + +/** + * clapper_player_get_subtitle_offset: + * @player: a #ClapperPlayer + * + * Get the currently set subtitle stream offset. + * + * The returned value is in seconds as a decimal number. + * + * Returns: the subtitle stream offset. + */ +gdouble +clapper_player_get_subtitle_offset (ClapperPlayer *self) +{ + gdouble offset; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), 0); + + GST_OBJECT_LOCK (self); + offset = self->subtitle_offset; + GST_OBJECT_UNLOCK (self); + + return offset; +} + +/** + * clapper_player_set_subtitle_font_desc: + * @player: a #ClapperPlayer + * @font_desc: Font description + * + * Set Pango font description to be used for subtitle stream rendering. + */ +void +clapper_player_set_subtitle_font_desc (ClapperPlayer *self, const gchar *font_desc) +{ + GValue value = G_VALUE_INIT; + + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + g_value_init (&value, G_TYPE_STRING); + g_value_set_string (&value, font_desc); + + clapper_playbin_bus_post_set_prop (self->bus, + GST_OBJECT_CAST (self->playbin), "subtitle-font-desc", &value); +} + +/** + * clapper_player_get_subtitle_font_desc: + * @player: a #ClapperPlayer + * + * Get the currently set font description used for subtitle stream rendering. + * + * Returns: (transfer full): the subtitle font description. + */ +gchar * +clapper_player_get_subtitle_font_desc (ClapperPlayer *self) +{ + gchar *font_desc = NULL; + + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + g_object_get (self->playbin, "subtitle-font-desc", &font_desc, NULL); + + return font_desc; +} + +/** + * clapper_player_play: + * @player: a #ClapperPlayer + * + * Either start or resume the playback of current media item. + * + * This function will queue a request for the underlaying #GStreamer + * pipeline to go into PLAYING state. + */ +void +clapper_player_play (ClapperPlayer *self) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + clapper_playbin_bus_post_request_state (self->bus, self, GST_STATE_PLAYING); +} + +/** + * clapper_player_pause: + * @player: a #ClapperPlayer + * + * Pause the playback of current media item. + * + * This function will queue a request for the underlaying #GStreamer + * pipeline to go into PAUSED state, thus can also be used on a not + * yet started video to go into PAUSED state first. + */ +void +clapper_player_pause (ClapperPlayer *self) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + clapper_playbin_bus_post_request_state (self->bus, self, GST_STATE_PAUSED); +} + +/** + * clapper_player_stop: + * @player: a #ClapperPlayer + * + * Stop the playback of current media item. + * + * This function will queue a request for the underlaying #GStreamer + * pipeline to go into READY state. + */ +void +clapper_player_stop (ClapperPlayer *self) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + + clapper_playbin_bus_post_request_state (self->bus, self, GST_STATE_READY); +} + +/** + * clapper_player_seek: + * @player: a #ClapperPlayer + * @position: a decimal number with position to seek to (in seconds) + * + * Request the player to perform a seek operation. + * + * This function will use [enum@Clapper.PlayerSeekMethod.NORMAL] as a + * seeking method. If you wish to specify what method to use per seeking + * request, use [method@Clapper.Player.seek_custom] instead. + * + * Note that seeking requests are per selected media item. Seeking + * requests will be ignored if player is stopped. You need to at least + * call [method@Clapper.Player.pause] before seeking and then your requested + * seek will be handled if item could be played. + */ +void +clapper_player_seek (ClapperPlayer *self, gdouble position) +{ + clapper_player_seek_custom (self, position, CLAPPER_PLAYER_SEEK_METHOD_NORMAL); +} + +/** + * clapper_player_seek_custom: + * @player: a #ClapperPlayer + * @position: a decimal number with position to seek to (in seconds) + * @method: a #ClapperPlayerSeekMethod + * + * Request the player to perform a seek operation. + * + * Same as [method@Clapper.Player.seek], but also allows to specify + * [enum@Clapper.PlayerSeekMethod] to use for seek. + */ +void +clapper_player_seek_custom (ClapperPlayer *self, gdouble position, ClapperPlayerSeekMethod method) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (position >= 0); + + clapper_playbin_bus_post_seek (self->bus, position, method); +} + +/** + * clapper_player_add_feature: + * @player: a #ClapperPlayer + * @feature: a #ClapperFeature + * + * Add another #ClapperFeature to the player. + * + * If feature is already added, this function will do nothing, + * so it is safe to call multiple times if unsure. + */ +void +clapper_player_add_feature (ClapperPlayer *self, ClapperFeature *feature) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (CLAPPER_IS_FEATURE (feature)); + + GST_OBJECT_LOCK (self); + + if (!self->features_manager) + self->features_manager = clapper_features_manager_new (); + + GST_OBJECT_UNLOCK (self); + + /* Once a feature is added, we always have features manager object + * and we can avoid player object locking to check that by using + * clapper_player_get_have_features() which is atomic */ + clapper_player_set_have_features (self, TRUE); + + clapper_features_manager_add_feature (self->features_manager, feature, GST_OBJECT (self)); +} + +static void +clapper_player_thread_start (ClapperThreadedObject *threaded_object) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (threaded_object); + const gchar *env, *playbin_str; + gint i; + + GST_TRACE_OBJECT (threaded_object, "Player thread start"); + + if (!(env = g_getenv ("USE_PLAYBIN3"))) // global GStreamer override + if (!(env = g_getenv ("CLAPPER_USE_PLAYBIN3"))) // Clapper override + env = g_getenv ("GST_CLAPPER_USE_PLAYBIN3"); // compat + + self->use_playbin3 = (env && g_str_has_prefix (env, "1")); + playbin_str = (self->use_playbin3) ? "playbin3" : "playbin"; + + if (!(self->playbin = gst_element_factory_make (playbin_str, NULL))) { + g_error ("Clapper: \"%s\" element not found, please check your setup", playbin_str); + g_assert_not_reached (); + + return; + } + gst_object_ref_sink (self->playbin); + + for (i = 0; playbin_watchlist[i]; ++i) + gst_element_add_property_notify_watch (self->playbin, playbin_watchlist[i], TRUE); + + g_signal_connect (self->playbin, "about-to-finish", G_CALLBACK (_about_to_finish_cb), self); + + if (!self->use_playbin3) { + g_signal_connect (self->playbin, "video-changed", G_CALLBACK (_playbin_streams_changed_cb), self); + g_signal_connect (self->playbin, "audio-changed", G_CALLBACK (_playbin_streams_changed_cb), self); + g_signal_connect (self->playbin, "text-changed", G_CALLBACK (_playbin_streams_changed_cb), self); + + g_signal_connect (self->playbin, "video-tags-changed", G_CALLBACK (_playbin_video_tags_changed_cb), self); + g_signal_connect (self->playbin, "audio-tags-changed", G_CALLBACK (_playbin_audio_tags_changed_cb), self); + g_signal_connect (self->playbin, "text-tags-changed", G_CALLBACK (_playbin_text_tags_changed_cb), self); + + g_signal_connect (self->playbin, "notify::current-video", G_CALLBACK (_playbin_selected_streams_changed_cb), self); + g_signal_connect (self->playbin, "notify::current-audio", G_CALLBACK (_playbin_selected_streams_changed_cb), self); + g_signal_connect (self->playbin, "notify::current-text", G_CALLBACK (_playbin_selected_streams_changed_cb), self); + } + + self->bus = gst_element_get_bus (self->playbin); + gst_bus_add_watch (self->bus, (GstBusFunc) clapper_playbin_bus_message_func, self); +} + +static void +clapper_player_thread_stop (ClapperThreadedObject *threaded_object) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (threaded_object); + + GST_TRACE_OBJECT (threaded_object, "Player thread stop"); + + clapper_player_remove_tick_source (self); + + gst_bus_set_flushing (self->bus, TRUE); + gst_bus_remove_watch (self->bus); + + gst_bus_set_flushing (GST_BUS_CAST (self->app_bus), TRUE); + gst_bus_remove_watch (GST_BUS_CAST (self->app_bus)); + + clapper_player_reset (self, TRUE); + + gst_element_set_state (self->playbin, GST_STATE_NULL); + + gst_clear_object (&self->bus); + gst_clear_object (&self->app_bus); + gst_clear_object (&self->playbin); + gst_clear_object (&self->collection); +} + +static void +clapper_player_init (ClapperPlayer *self) +{ + self->queue = clapper_queue_new (); + gst_object_set_parent (GST_OBJECT_CAST (self->queue), GST_OBJECT_CAST (self)); + + self->video_streams = clapper_stream_list_new (); + gst_object_set_parent (GST_OBJECT_CAST (self->video_streams), GST_OBJECT_CAST (self)); + + self->audio_streams = clapper_stream_list_new (); + gst_object_set_parent (GST_OBJECT_CAST (self->audio_streams), GST_OBJECT_CAST (self)); + + self->subtitle_streams = clapper_stream_list_new (); + gst_object_set_parent (GST_OBJECT_CAST (self->subtitle_streams), GST_OBJECT_CAST (self)); + + self->position_query = gst_query_new_position (GST_FORMAT_TIME); + + self->current_state = GST_STATE_NULL; + self->target_state = GST_STATE_READY; + + self->autoplay = DEFAULT_AUTOPLAY; + self->mute = DEFAULT_MUTE; + self->volume = DEFAULT_VOLUME; + self->speed = DEFAULT_SPEED; + self->state = DEFAULT_STATE; + self->video_enabled = DEFAULT_VIDEO_ENABLED; + self->audio_enabled = DEFAULT_AUDIO_ENABLED; + self->subtitles_enabled = DEFAULT_SUBTITLES_ENABLED; +} + +static void +clapper_player_constructed (GObject *object) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (object); + + GST_OBJECT_LOCK (self); + self->app_bus = clapper_app_bus_new (); + GST_OBJECT_UNLOCK (self); + + G_OBJECT_CLASS (parent_class)->constructed (object); +} + +static void +clapper_player_dispose (GObject *object) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (object); + + GST_OBJECT_LOCK (self); + + if (self->stream_notify_id != 0) { + g_signal_handler_disconnect (self->collection, self->stream_notify_id); + self->stream_notify_id = 0; + } + + GST_OBJECT_UNLOCK (self); + + /* Parent class will wait for player thread to stop running */ + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +clapper_player_finalize (GObject *object) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + gst_object_unparent (GST_OBJECT_CAST (self->queue)); + gst_object_unref (self->queue); + + gst_object_unparent (GST_OBJECT_CAST (self->video_streams)); + gst_object_unref (self->video_streams); + + gst_object_unparent (GST_OBJECT_CAST (self->audio_streams)); + gst_object_unref (self->audio_streams); + + gst_object_unparent (GST_OBJECT_CAST (self->subtitle_streams)); + gst_object_unref (self->subtitle_streams); + + gst_query_unref (self->position_query); + + gst_clear_object (&self->collection); + gst_clear_object (&self->features_manager); + gst_clear_object (&self->pending_item); + gst_clear_object (&self->played_item); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_player_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (object); + + switch (prop_id) { + case PROP_QUEUE: + g_value_set_object (value, clapper_player_get_queue (self)); + break; + case PROP_VIDEO_STREAMS: + g_value_set_object (value, clapper_player_get_video_streams (self)); + break; + case PROP_AUDIO_STREAMS: + g_value_set_object (value, clapper_player_get_audio_streams (self)); + break; + case PROP_SUBTITLE_STREAMS: + g_value_set_object (value, clapper_player_get_subtitle_streams (self)); + break; + case PROP_AUTOPLAY: + g_value_set_boolean (value, clapper_player_get_autoplay (self)); + break; + case PROP_POSITION: + g_value_set_double (value, clapper_player_get_position (self)); + break; + case PROP_SPEED: + g_value_set_double (value, clapper_player_get_speed (self)); + break; + case PROP_STATE: + g_value_set_enum (value, clapper_player_get_state (self)); + break; + case PROP_MUTE: + g_value_set_boolean (value, clapper_player_get_mute (self)); + break; + case PROP_VOLUME: + g_value_set_double (value, clapper_player_get_volume (self)); + break; + case PROP_AUDIO_SINK: + g_value_take_object (value, clapper_player_get_audio_sink (self)); + break; + case PROP_VIDEO_SINK: + g_value_take_object (value, clapper_player_get_video_sink (self)); + break; + case PROP_AUDIO_FILTER: + g_value_take_object (value, clapper_player_get_audio_filter (self)); + break; + case PROP_VIDEO_FILTER: + g_value_take_object (value, clapper_player_get_video_filter (self)); + break; + case PROP_CURRENT_AUDIO_DECODER: + g_value_take_object (value, clapper_player_get_current_audio_decoder (self)); + break; + case PROP_CURRENT_VIDEO_DECODER: + g_value_take_object (value, clapper_player_get_current_video_decoder (self)); + break; + case PROP_VIDEO_ENABLED: + g_value_set_boolean (value, clapper_player_get_video_enabled (self)); + break; + case PROP_AUDIO_ENABLED: + g_value_set_boolean (value, clapper_player_get_audio_enabled (self)); + break; + case PROP_SUBTITLES_ENABLED: + g_value_set_boolean (value, clapper_player_get_subtitles_enabled (self)); + break; + case PROP_AUDIO_OFFSET: + g_value_set_double (value, clapper_player_get_audio_offset (self)); + break; + case PROP_SUBTITLE_OFFSET: + g_value_set_double (value, clapper_player_get_subtitle_offset (self)); + break; + case PROP_SUBTITLE_FONT_DESC: + g_value_take_string (value, clapper_player_get_subtitle_font_desc (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_player_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperPlayer *self = CLAPPER_PLAYER_CAST (object); + + switch (prop_id) { + case PROP_AUTOPLAY: + clapper_player_set_autoplay (self, g_value_get_boolean (value)); + break; + case PROP_SPEED: + clapper_player_set_speed (self, g_value_get_double (value)); + break; + case PROP_MUTE: + clapper_player_set_mute (self, g_value_get_boolean (value)); + break; + case PROP_VOLUME: + clapper_player_set_volume (self, g_value_get_double (value)); + break; + case PROP_AUDIO_SINK: + clapper_player_set_audio_sink (self, g_value_get_object (value)); + break; + case PROP_VIDEO_SINK: + clapper_player_set_video_sink (self, g_value_get_object (value)); + break; + case PROP_AUDIO_FILTER: + clapper_player_set_audio_filter (self, g_value_get_object (value)); + break; + case PROP_VIDEO_FILTER: + clapper_player_set_video_filter (self, g_value_get_object (value)); + break; + case PROP_VIDEO_ENABLED: + clapper_player_set_video_enabled (self, g_value_get_boolean (value)); + break; + case PROP_AUDIO_ENABLED: + clapper_player_set_audio_enabled (self, g_value_get_boolean (value)); + break; + case PROP_SUBTITLES_ENABLED: + clapper_player_set_subtitles_enabled (self, g_value_get_boolean (value)); + break; + case PROP_AUDIO_OFFSET: + clapper_player_set_audio_offset (self, g_value_get_double (value)); + break; + case PROP_SUBTITLE_OFFSET: + clapper_player_set_subtitle_offset (self, g_value_get_double (value)); + break; + case PROP_SUBTITLE_FONT_DESC: + clapper_player_set_subtitle_font_desc (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_player_class_init (ClapperPlayerClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperThreadedObjectClass *threaded_object = (ClapperThreadedObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperplayer", 0, + "Clapper Player"); + + gobject_class->constructed = clapper_player_constructed; + gobject_class->get_property = clapper_player_get_property; + gobject_class->set_property = clapper_player_set_property; + gobject_class->dispose = clapper_player_dispose; + gobject_class->finalize = clapper_player_finalize; + + /** + * ClapperPlayer:queue: + * + * Clapper playback queue. + */ + param_specs[PROP_QUEUE] = g_param_spec_object ("queue", + NULL, NULL, CLAPPER_TYPE_QUEUE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:video-streams: + * + * List of currently available video streams. + */ + param_specs[PROP_VIDEO_STREAMS] = g_param_spec_object ("video-streams", + NULL, NULL, CLAPPER_TYPE_STREAM_LIST, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:audio-streams: + * + * List of currently available audio streams. + */ + param_specs[PROP_AUDIO_STREAMS] = g_param_spec_object ("audio-streams", + NULL, NULL, CLAPPER_TYPE_STREAM_LIST, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:subtitle-streams: + * + * List of currently available subtitle streams. + */ + param_specs[PROP_SUBTITLE_STREAMS] = g_param_spec_object ("subtitle-streams", + NULL, NULL, CLAPPER_TYPE_STREAM_LIST, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:autoplay: + * + * Always try to start playback after media item changes. + */ + param_specs[PROP_AUTOPLAY] = g_param_spec_boolean ("autoplay", + NULL, NULL, DEFAULT_AUTOPLAY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:position: + * + * Current playback position as a decimal number in seconds. + */ + param_specs[PROP_POSITION] = g_param_spec_double ("position", + NULL, NULL, 0, G_MAXDOUBLE, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:speed: + * + * Current playback speed. + */ + param_specs[PROP_SPEED] = g_param_spec_double ("speed", + NULL, NULL, G_MINDOUBLE, G_MAXDOUBLE, DEFAULT_SPEED, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:state: + * + * Current playback state. + */ + param_specs[PROP_STATE] = g_param_spec_enum ("state", + NULL, NULL, CLAPPER_TYPE_PLAYER_STATE, DEFAULT_STATE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:mute: + * + * Mute audio without changing volume. + */ + param_specs[PROP_MUTE] = g_param_spec_boolean ("mute", + NULL, NULL, DEFAULT_MUTE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:volume: + * + * Current volume as a decimal number (1.0 = 100%). + * + * Note that #ClapperPlayer uses a CUBIC volume scale, meaning + * that this property value reflects human hearing level and can + * be easily bound to volume sliders as-is. + */ + param_specs[PROP_VOLUME] = g_param_spec_double ("volume", + NULL, NULL, 0, 2.0, DEFAULT_VOLUME, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:video-sink: + * + * Video sink to use (autovideosink by default). + */ + param_specs[PROP_VIDEO_SINK] = g_param_spec_object ("video-sink", + NULL, NULL, GST_TYPE_ELEMENT, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:audio-sink: + * + * Audio sink to use (autoaudiosink by default). + */ + param_specs[PROP_AUDIO_SINK] = g_param_spec_object ("audio-sink", + NULL, NULL, GST_TYPE_ELEMENT, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:video-filter: + * + * Optional video filter to use (none by default). + */ + param_specs[PROP_VIDEO_FILTER] = g_param_spec_object ("video-filter", + NULL, NULL, GST_TYPE_ELEMENT, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:audio-filter: + * + * Optional audio filter to use (none by default). + */ + param_specs[PROP_AUDIO_FILTER] = g_param_spec_object ("audio-filter", + NULL, NULL, GST_TYPE_ELEMENT, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:current-video-decoder: + * + * Currently used video decoder. + */ + param_specs[PROP_CURRENT_VIDEO_DECODER] = g_param_spec_object ("current-video-decoder", + NULL, NULL, GST_TYPE_ELEMENT, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:current-audio-decoder: + * + * Currently used audio decoder. + */ + param_specs[PROP_CURRENT_AUDIO_DECODER] = g_param_spec_object ("current-audio-decoder", + NULL, NULL, GST_TYPE_ELEMENT, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:video-enabled: + * + * Whether video stream is enabled. + */ + param_specs[PROP_VIDEO_ENABLED] = g_param_spec_boolean ("video-enabled", + NULL, NULL, DEFAULT_VIDEO_ENABLED, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:audio-enabled: + * + * Whether audio stream is enabled. + */ + param_specs[PROP_AUDIO_ENABLED] = g_param_spec_boolean ("audio-enabled", + NULL, NULL, DEFAULT_AUDIO_ENABLED, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:subtitles-enabled: + * + * Whether subtitles stream is enabled. + */ + param_specs[PROP_SUBTITLES_ENABLED] = g_param_spec_boolean ("subtitles-enabled", + NULL, NULL, DEFAULT_SUBTITLES_ENABLED, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:audio-offset: + * + * Audio stream offset relative to video. + */ + param_specs[PROP_AUDIO_OFFSET] = g_param_spec_double ("audio-offset", + NULL, NULL, G_MININT64, G_MAXINT64, 0, // NOTE: Gstreamer has gint64 range + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:subtitle-offset: + * + * Subtitle stream offset relative to video. + */ + param_specs[PROP_SUBTITLE_OFFSET] = g_param_spec_double ("subtitle-offset", + NULL, NULL, G_MININT64, G_MAXINT64, 0, // NOTE: Gstreamer has gint64 range + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer:subtitle-font-desc: + * + * Subtitle stream font description. + */ + param_specs[PROP_SUBTITLE_FONT_DESC] = g_param_spec_string ("subtitle-font-desc", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperPlayer::seek-done: + * @player: a #ClapperPlayer + * + * A seeking operation has finished. Player is now at playback position after seek. + */ + signals[SIGNAL_SEEK_DONE] = g_signal_new ("seek-done", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + + /** + * ClapperPlayer::missing-plugin: + * @player: a #ClapperPlayer + * @name: a localised string describing the missing feature, for use in + * error dialogs and the like. + * @installer_detail: (nullable): a string containing all the details about the missing + * element to be passed to an external installer called via either + * gst_install_plugins_async() or gst_install_plugins_sync() function. + * + * A #GStreamer plugin or one of its features needed for playback is missing. + * + * The @description and @installer_detail can be used to present the user more info + * about what is missing and prompt him to install it with an external installer. + */ + signals[SIGNAL_MISSING_PLUGIN] = g_signal_new ("missing-plugin", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); + + /** + * ClapperPlayer::warning: + * @player: a #ClapperPlayer + * @error: a #GError + * @debug_info: (nullable): an additional debug message. + * + * These are some usually more minor error messages that should + * be treated like warnings. Should not generally prevent/stop playback. + */ + signals[SIGNAL_WARNING] = g_signal_new ("warning", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_ERROR, G_TYPE_STRING); + + /** + * ClapperPlayer::error: + * @player: a #ClapperPlayer + * @error: a #GError + * @debug_info: (nullable): an additional debug message. + * + * These are normal error messages. Upon emitting this signal, + * playback will stop due to the error. + */ + signals[SIGNAL_ERROR] = g_signal_new ("error", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_ERROR, G_TYPE_STRING); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); + + threaded_object->thread_start = clapper_player_thread_start; + threaded_object->thread_stop = clapper_player_thread_stop; +} diff --git a/src/lib/clapper/clapper-player.h b/src/lib/clapper/clapper-player.h new file mode 100644 index 00000000..993eb1d1 --- /dev/null +++ b/src/lib/clapper/clapper-player.h @@ -0,0 +1,128 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_PLAYER (clapper_player_get_type()) +#define CLAPPER_PLAYER_CAST(obj) ((ClapperPlayer *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperPlayer, clapper_player, CLAPPER, PLAYER, ClapperThreadedObject) + +ClapperPlayer * clapper_player_new (void); + +ClapperQueue * clapper_player_get_queue (ClapperPlayer *player); + +ClapperStreamList * clapper_player_get_video_streams (ClapperPlayer *player); + +ClapperStreamList * clapper_player_get_audio_streams (ClapperPlayer *player); + +ClapperStreamList * clapper_player_get_subtitle_streams (ClapperPlayer *player); + +void clapper_player_set_autoplay (ClapperPlayer *player, gboolean enabled); + +gboolean clapper_player_get_autoplay (ClapperPlayer *player); + +gdouble clapper_player_get_position (ClapperPlayer *player); + +ClapperPlayerState clapper_player_get_state (ClapperPlayer *player); + +void clapper_player_set_mute (ClapperPlayer *player, gboolean mute); + +gboolean clapper_player_get_mute (ClapperPlayer *player); + +void clapper_player_set_volume (ClapperPlayer *player, gdouble volume); + +gdouble clapper_player_get_volume (ClapperPlayer *player); + +void clapper_player_set_speed (ClapperPlayer *player, gdouble speed); + +gdouble clapper_player_get_speed (ClapperPlayer *player); + +void clapper_player_set_video_sink (ClapperPlayer *player, GstElement *element); + +GstElement * clapper_player_get_video_sink (ClapperPlayer *player); + +void clapper_player_set_audio_sink (ClapperPlayer *player, GstElement *element); + +GstElement * clapper_player_get_audio_sink (ClapperPlayer *player); + +void clapper_player_set_video_filter (ClapperPlayer *player, GstElement *element); + +GstElement * clapper_player_get_video_filter (ClapperPlayer *player); + +void clapper_player_set_audio_filter (ClapperPlayer *player, GstElement *element); + +GstElement * clapper_player_get_audio_filter (ClapperPlayer *player); + +GstElement * clapper_player_get_current_video_decoder (ClapperPlayer *player); + +GstElement * clapper_player_get_current_audio_decoder (ClapperPlayer *player); + +void clapper_player_set_video_enabled (ClapperPlayer *player, gboolean enabled); + +gboolean clapper_player_get_video_enabled (ClapperPlayer *player); + +void clapper_player_set_audio_enabled (ClapperPlayer *player, gboolean enabled); + +gboolean clapper_player_get_audio_enabled (ClapperPlayer *player); + +void clapper_player_set_subtitles_enabled (ClapperPlayer *player, gboolean enabled); + +gboolean clapper_player_get_subtitles_enabled (ClapperPlayer *player); + +void clapper_player_set_audio_offset (ClapperPlayer *player, gdouble offset); + +gdouble clapper_player_get_audio_offset (ClapperPlayer *player); + +void clapper_player_set_subtitle_offset (ClapperPlayer *player, gdouble offset); + +gdouble clapper_player_get_subtitle_offset (ClapperPlayer *player); + +void clapper_player_set_subtitle_font_desc (ClapperPlayer *player, const gchar *font_desc); + +gchar * clapper_player_get_subtitle_font_desc (ClapperPlayer *player); + +void clapper_player_play (ClapperPlayer *player); + +void clapper_player_pause (ClapperPlayer *player); + +void clapper_player_stop (ClapperPlayer *player); + +void clapper_player_seek (ClapperPlayer *player, gdouble position); + +void clapper_player_seek_custom (ClapperPlayer *player, gdouble position, ClapperPlayerSeekMethod method); + +void clapper_player_add_feature (ClapperPlayer *player, ClapperFeature *feature); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-queue-private.h b/src/lib/clapper/clapper-queue-private.h new file mode 100644 index 00000000..71b0bc9d --- /dev/null +++ b/src/lib/clapper/clapper-queue-private.h @@ -0,0 +1,39 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "clapper-queue.h" +#include "clapper-media-item.h" +#include "clapper-player.h" +#include "clapper-app-bus-private.h" + +G_BEGIN_DECLS + +ClapperQueue * clapper_queue_new (void); + +void clapper_queue_handle_played_item_changed (ClapperQueue *queue, ClapperMediaItem *played_item, ClapperAppBus *app_bus); + +void clapper_queue_handle_about_to_finish (ClapperQueue *queue, ClapperPlayer *player); + +gboolean clapper_queue_handle_eos (ClapperQueue *queue, ClapperPlayer *player); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-queue.c b/src/lib/clapper/clapper-queue.c new file mode 100644 index 00000000..c00d6524 --- /dev/null +++ b/src/lib/clapper/clapper-queue.c @@ -0,0 +1,1299 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperQueue: + * + * A queue of media to be played. + */ + +#include + +#include "clapper-queue-private.h" +#include "clapper-media-item-private.h" +#include "clapper-player-private.h" +#include "clapper-playbin-bus-private.h" + +#define CLAPPER_QUEUE_GET_REC_LOCK(obj) (&CLAPPER_QUEUE_CAST(obj)->rec_lock) +#define CLAPPER_QUEUE_REC_LOCK(obj) g_rec_mutex_lock (CLAPPER_QUEUE_GET_REC_LOCK(obj)) +#define CLAPPER_QUEUE_REC_UNLOCK(obj) g_rec_mutex_unlock (CLAPPER_QUEUE_GET_REC_LOCK(obj)) + +#define DEFAULT_PROGRESSION_MODE CLAPPER_QUEUE_PROGRESSION_NONE +#define DEFAULT_GAPLESS FALSE +#define DEFAULT_INSTANT FALSE + +#define GST_CAT_DEFAULT clapper_queue_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperQueue +{ + GstObject parent; + + GRecMutex rec_lock; + + GPtrArray *items; + ClapperMediaItem *current_item; + guint current_index; + + ClapperQueueProgressionMode progression_mode; + gboolean gapless; + gboolean instant; + + /* Avoid scenario when "gapless" prop is changed + * between "about-to-finish" and "EOS" */ + gboolean handled_gapless; +}; + +enum +{ + PROP_0, + PROP_CURRENT_ITEM, + PROP_CURRENT_INDEX, + PROP_N_ITEMS, + PROP_PROGRESSION_MODE, + PROP_GAPLESS, + PROP_INSTANT, + PROP_LAST +}; + +static GType +clapper_queue_list_model_get_item_type (GListModel *model) +{ + return CLAPPER_TYPE_MEDIA_ITEM; +} + +static guint +clapper_queue_list_model_get_n_items (GListModel *model) +{ + ClapperQueue *self = CLAPPER_QUEUE_CAST (model); + guint n_items; + + CLAPPER_QUEUE_REC_LOCK (self); + n_items = self->items->len; + CLAPPER_QUEUE_REC_UNLOCK (self); + + return n_items; +} + +static gpointer +clapper_queue_list_model_get_item (GListModel *model, guint index) +{ + ClapperQueue *self = CLAPPER_QUEUE_CAST (model); + ClapperMediaItem *item = NULL; + + CLAPPER_QUEUE_REC_LOCK (self); + if (G_LIKELY (index < self->items->len)) { + GST_LOG_OBJECT (self, "Reading queue item: %u", index); + item = g_object_ref (g_ptr_array_index (self->items, index)); + } + CLAPPER_QUEUE_REC_UNLOCK (self); + + return item; +} + +static void +clapper_queue_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = clapper_queue_list_model_get_item_type; + iface->get_n_items = clapper_queue_list_model_get_n_items; + iface->get_item = clapper_queue_list_model_get_item; +} + +#define parent_class clapper_queue_parent_class +G_DEFINE_TYPE_WITH_CODE (ClapperQueue, clapper_queue, GST_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, clapper_queue_list_model_iface_init)); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void +_announce_model_update (ClapperQueue *self, guint index, guint removed, guint added, + ClapperMediaItem *changed_item) +{ + GST_DEBUG_OBJECT (self, "Announcing model update, index: %u, removed: %u, added: %u", + index, removed, added); + + /* We handle reposition separately */ + if (removed != added) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + if (player && clapper_player_get_have_features (player)) { + if (added == 1) // addition + clapper_features_manager_trigger_queue_item_added (player->features_manager, changed_item, index); + else if (removed == 1) // removal + clapper_features_manager_trigger_queue_item_removed (player->features_manager, changed_item, index); + else if (removed > 1 && added == 0) // queue cleared + clapper_features_manager_trigger_queue_cleared (player->features_manager); + else + g_assert_not_reached (); + } + + gst_clear_object (&player); + } + + g_list_model_items_changed (G_LIST_MODEL (self), index, removed, added); + + if (removed != added) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_N_ITEMS]); +} + +static void +_announce_reposition (ClapperQueue *self, guint before, guint after) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Announcing item reposition: %u -> %u", before, after); + + if ((player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)))) { + if (clapper_player_get_have_features (player)) + clapper_features_manager_trigger_queue_item_repositioned (player->features_manager, before, after); + + gst_object_unref (player); + } +} + +/* + * Notify about current index change. This is needed only if some items + * are added/removed before current selection, otherwise if selection + * also changes use _announce_current_item_and_index_change() instead. + */ +static void +_announce_current_index_change (ClapperQueue *self) +{ + gboolean is_main_thread = g_main_context_is_owner (g_main_context_default ()); + + GST_DEBUG_OBJECT (self, "Announcing current index change from %smain thread, now: %u", + (is_main_thread) ? "" : "non-", self->current_index); + + if (is_main_thread) { + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_INDEX]); + } else { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + if (G_LIKELY (player != NULL)) { + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]); + + gst_object_unref (player); + } + } +} + +/* + * Notify about both current item and its index changes. + * Needs to be called while holding CLAPPER_QUEUE_REC_LOCK. + */ +static void +_announce_current_item_and_index_change (ClapperQueue *self) +{ + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + gboolean instant, is_main_thread; + + if (G_UNLIKELY (player == NULL)) + return; + + is_main_thread = g_main_context_is_owner (g_main_context_default ()); + + GST_DEBUG_OBJECT (self, "Announcing current item change from %smain thread," + " now: %" GST_PTR_FORMAT " (index: %u)", + (is_main_thread) ? "" : "non-", self->current_item, self->current_index); + + GST_OBJECT_LOCK (self); + instant = self->instant; + GST_OBJECT_UNLOCK (self); + + clapper_playbin_bus_post_current_item_change (player->bus, self->current_item, + (instant) ? CLAPPER_QUEUE_ITEM_CHANGE_INSTANT : CLAPPER_QUEUE_ITEM_CHANGE_NORMAL); + + if (is_main_thread) { + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_ITEM]); + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_INDEX]); + } else { + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_ITEM]); + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]); + } + + gst_object_unref (player); +} + +static inline gboolean +_replace_current_item_unlocked (ClapperQueue *self, ClapperMediaItem *item, guint index) +{ + if (gst_object_replace ((GstObject **) &self->current_item, GST_OBJECT_CAST (item))) { + self->current_index = index; + + if (self->current_item) + clapper_media_item_set_used (self->current_item, TRUE); + + GST_TRACE_OBJECT (self, "Current item replaced, now: %" GST_PTR_FORMAT, self->current_item); + + return TRUE; + } + + return FALSE; +} + +static void +_reset_shuffle_unlocked (ClapperQueue *self) +{ + guint i; + + for (i = 0; i < self->items->len; ++i) { + ClapperMediaItem *item = g_ptr_array_index (self->items, i); + clapper_media_item_set_used (item, FALSE); + } +} + +static ClapperMediaItem * +_get_next_item_unlocked (ClapperQueue *self, ClapperQueueProgressionMode mode) +{ + ClapperMediaItem *next_item = NULL; + + GST_DEBUG_OBJECT (self, "Handling progression mode: %u", mode); + + if (self->current_index == CLAPPER_QUEUE_INVALID_POSITION) { + GST_DEBUG_OBJECT (self, "No current item, can not advance"); + return NULL; + } + + switch (mode) { + case CLAPPER_QUEUE_PROGRESSION_NONE: + break; + case CLAPPER_QUEUE_PROGRESSION_CAROUSEL: + next_item = g_ptr_array_index (self->items, 0); + G_GNUC_FALLTHROUGH; + case CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE: + if (self->current_index + 1 < self->items->len) + next_item = g_ptr_array_index (self->items, self->current_index + 1); + break; + case CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM: + next_item = self->current_item; + break; + case CLAPPER_QUEUE_PROGRESSION_SHUFFLE:{ + GList *unused = NULL; + GRand *rand = g_rand_new (); + guint i; + + for (i = 0; i < self->items->len; ++i) { + ClapperMediaItem *item = g_ptr_array_index (self->items, i); + + if (!clapper_media_item_get_used (item)) + unused = g_list_append (unused, item); + } + + if (unused) { + next_item = g_list_nth_data (unused, + g_rand_int_range (rand, 0, g_list_length (unused))); + g_list_free (unused); + } else { + _reset_shuffle_unlocked (self); + next_item = g_ptr_array_index (self->items, + g_rand_int_range (rand, 0, self->items->len)); + } + + g_rand_free (rand); + break; + } + default: + g_assert_not_reached (); + break; + } + + if (next_item) + gst_object_ref (next_item); + + return next_item; +} + +/* + * For gapless we need to manually replace current item in queue when it starts + * playing and emit notify about change, this function will do that if necessary + */ +void +clapper_queue_handle_played_item_changed (ClapperQueue *self, ClapperMediaItem *played_item, + ClapperAppBus *app_bus) +{ + guint index = 0; + gboolean changed = FALSE; + + CLAPPER_QUEUE_REC_LOCK (self); + + /* Item is often the same here (when selected from queue), + * so compare pointers first to avoid iterating queue */ + if (played_item != self->current_item + && g_ptr_array_find (self->items, played_item, &index)) + changed = _replace_current_item_unlocked (self, played_item, index); + + CLAPPER_QUEUE_REC_UNLOCK (self); + + if (changed) { + clapper_app_bus_post_prop_notify (app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_ITEM]); + clapper_app_bus_post_prop_notify (app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]); + } +} + +void +clapper_queue_handle_about_to_finish (ClapperQueue *self, ClapperPlayer *player) +{ + ClapperMediaItem *next_item; + ClapperQueueProgressionMode progression_mode; + + GST_INFO_OBJECT (self, "Handling \"about-to-finish\""); + + GST_OBJECT_LOCK (self); + if (!(self->handled_gapless = self->gapless)) { + GST_OBJECT_UNLOCK (self); + return; + } + progression_mode = self->progression_mode; + GST_OBJECT_UNLOCK (self); + + CLAPPER_QUEUE_REC_LOCK (self); + next_item = _get_next_item_unlocked (self, progression_mode); + CLAPPER_QUEUE_REC_UNLOCK (self); + + if (next_item) { + clapper_player_set_pending_item (player, next_item, CLAPPER_QUEUE_ITEM_CHANGE_GAPLESS); + gst_object_unref (next_item); + } +} + +gboolean +clapper_queue_handle_eos (ClapperQueue *self, ClapperPlayer *player) +{ + ClapperMediaItem *next_item = NULL; + ClapperQueueProgressionMode progression_mode; + gboolean handled_eos = FALSE; + + /* On gapless "about-to-finish" selects next item instead and + * we can reach EOS only if there was either nothing to select or + * some playback error ocurred */ + + GST_INFO_OBJECT (self, "Handling EOS"); + + GST_OBJECT_LOCK (self); + if (self->handled_gapless) { + self->handled_gapless = FALSE; // reset + GST_OBJECT_UNLOCK (self); + return FALSE; + } + progression_mode = self->progression_mode; + GST_OBJECT_UNLOCK (self); + + CLAPPER_QUEUE_REC_LOCK (self); + if ((next_item = _get_next_item_unlocked (self, progression_mode))) { + if (next_item == self->current_item) + clapper_player_seek (player, 0); + else + clapper_queue_select_item (self, next_item); + + handled_eos = TRUE; + gst_object_unref (next_item); + } + CLAPPER_QUEUE_REC_UNLOCK (self); + + return handled_eos; +} + +/* + * clapper_queue_new: + * + * Returns: (transfer full): a new #ClapperQueue instance + */ +ClapperQueue * +clapper_queue_new (void) +{ + ClapperQueue *queue; + + queue = g_object_new (CLAPPER_TYPE_QUEUE, NULL); + gst_object_ref_sink (queue); + + return queue; +} + +/** + * clapper_queue_add_item: + * @queue: a #ClapperQueue + * @item: a #ClapperMediaItem + * + * Add another #ClapperMediaItem to the end of queue. + * + * If item is already in queue, this function will do nothing, + * so it is safe to call multiple times if unsure. + */ +void +clapper_queue_add_item (ClapperQueue *self, ClapperMediaItem *item) +{ + clapper_queue_insert_item (self, item, -1); +} + +/** + * clapper_queue_insert_item: + * @queue: a #ClapperQueue + * @item: a #ClapperMediaItem + * @index: the index to place @item in queue, -1 to append + * + * Insert another #ClapperMediaItem at @index position to the queue. + * + * If item is already in queue, this function will do nothing, + * so it is safe to call multiple times if unsure. + */ +void +clapper_queue_insert_item (ClapperQueue *self, ClapperMediaItem *item, gint index) +{ + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + g_return_if_fail (CLAPPER_IS_MEDIA_ITEM (item)); + g_return_if_fail (index >= -1); + + CLAPPER_QUEUE_REC_LOCK (self); + + if (!g_ptr_array_find (self->items, item, NULL)) { + guint prev_length = self->items->len; + + g_ptr_array_insert (self->items, index, gst_object_ref (item)); + gst_object_set_parent (GST_OBJECT_CAST (item), GST_OBJECT_CAST (self)); + + /* In append we inserted at array length */ + if (index < 0) + index = prev_length; + + _announce_model_update (self, index, 0, 1, item); + + /* If has selection and inserting before it */ + if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION + && (guint) index <= self->current_index) { + self->current_index++; + _announce_current_index_change (self); + } else if (prev_length == 0 && _replace_current_item_unlocked (self, item, 0)) { + /* If queue was empty, auto select first item and announce it */ + _announce_current_item_and_index_change (self); + } else if (self->current_index == prev_length - 1 + && clapper_queue_get_progression_mode (self) == CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + gboolean after_eos = (gboolean) g_atomic_int_get (&player->eos); + + /* In consecutive progression automatically select next item + * if we were after EOS of last queue item */ + if (after_eos && _replace_current_item_unlocked (self, item, index)) + _announce_current_item_and_index_change (self); + + gst_object_unref (player); + } + } + + CLAPPER_QUEUE_REC_UNLOCK (self); +} + +/** + * clapper_queue_reposition_item: + * @queue: a #ClapperQueue + * @item: a #ClapperMediaItem + * @index: the index to place @item in queue, -1 to place at the end + * + * Change position of one #ClapperMediaItem within the queue. + * + * Note that the @index is the new position you expect item to be + * after whole reposition operation is finished. + * + * If item is not in the queue, this function will do nothing. + */ +void +clapper_queue_reposition_item (ClapperQueue *self, ClapperMediaItem *item, gint index) +{ + guint index_old = 0; + + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + g_return_if_fail (CLAPPER_IS_MEDIA_ITEM (item)); + g_return_if_fail (index >= -1); + + CLAPPER_QUEUE_REC_LOCK (self); + + if (g_ptr_array_find (self->items, item, &index_old)) { + ClapperMediaItem *removed_item; + guint index_new, start_index, end_index, n_changed; + + index_new = (index < 0) + ? self->items->len - 1 + : (guint) index; + + GST_DEBUG_OBJECT (self, "Reposition item %u -> %u, is_current: %s", + index_old, index_new, (item == self->current_item) ? "yes" : "no"); + + removed_item = g_ptr_array_steal_index (self->items, index_old); + g_ptr_array_insert (self->items, index_new, removed_item); + + _announce_reposition (self, index_old, index_new); + + if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION) { + guint before = self->current_index; + + if (index_old > self->current_index && index_new <= self->current_index) + self->current_index++; // Moved before current item + else if (index_old < self->current_index && index_new >= self->current_index) + self->current_index--; // Moved after current item + else if (index_old == self->current_index) + self->current_index = index_new; // Moved current item + + if (self->current_index != before) + _announce_current_index_change (self); + } + + start_index = MIN (index_old, index_new); + end_index = MAX (index_old, index_new); + n_changed = end_index - start_index + 1; + + _announce_model_update (self, start_index, n_changed, n_changed, item); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); +} + +/** + * clapper_queue_remove_item: + * @queue: a #ClapperQueue + * @item: a #ClapperMediaItem + * + * Removes #ClapperMediaItem from the queue. + * + * If item either was never in the queue or was removed from + * it earlier, this function will do nothing, so it is safe + * to call multiple times if unsure. + */ +void +clapper_queue_remove_item (ClapperQueue *self, ClapperMediaItem *item) +{ + guint index = 0; + + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + g_return_if_fail (CLAPPER_IS_MEDIA_ITEM (item)); + + CLAPPER_QUEUE_REC_LOCK (self); + + if (g_ptr_array_find (self->items, item, &index)) + clapper_queue_remove_index (self, index); + + CLAPPER_QUEUE_REC_UNLOCK (self); +} + +/** + * clapper_queue_remove_index: + * @queue: a #ClapperQueue + * @index: an item index + * + * Removes #ClapperMediaItem at @index from the queue. + */ +void +clapper_queue_remove_index (ClapperQueue *self, guint index) +{ + ClapperMediaItem *item = clapper_queue_steal_index (self, index); + gst_clear_object (&item); +} + +/** + * clapper_queue_steal_index: + * @queue: a #ClapperQueue + * @index: an item index + * + * Removes #ClapperMediaItem at @index from the queue. + * + * Returns: (transfer full) (nullable): The removed #ClapperMediaItem at @index. + */ +ClapperMediaItem * +clapper_queue_steal_index (ClapperQueue *self, guint index) +{ + ClapperMediaItem *removed_item = NULL; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), NULL); + g_return_val_if_fail (index != CLAPPER_QUEUE_INVALID_POSITION, NULL); + + CLAPPER_QUEUE_REC_LOCK (self); + + if (index < self->items->len) { + if (index == self->current_index + && _replace_current_item_unlocked (self, NULL, CLAPPER_QUEUE_INVALID_POSITION)) { + _announce_current_item_and_index_change (self); + } else if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION + && index < self->current_index) { + /* If has selection and removed before it */ + self->current_index--; + _announce_current_index_change (self); + } + + removed_item = g_ptr_array_steal_index (self->items, index); + gst_object_unparent (GST_OBJECT_CAST (removed_item)); + + _announce_model_update (self, index, 1, 0, removed_item); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); + + return removed_item; +} + +/** + * clapper_queue_clear: + * @queue: a #ClapperQueue + * + * Removes all media items from the queue. + * + * If queue is empty, this function will do nothing, + * so it is safe to call multiple times if unsure. + */ +void +clapper_queue_clear (ClapperQueue *self) +{ + guint n_items; + + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + + CLAPPER_QUEUE_REC_LOCK (self); + + n_items = self->items->len; + + if (n_items > 0) { + if (_replace_current_item_unlocked (self, NULL, CLAPPER_QUEUE_INVALID_POSITION)) + _announce_current_item_and_index_change (self); + + g_ptr_array_remove_range (self->items, 0, n_items); + _announce_model_update (self, 0, n_items, 0, NULL); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); +} + +/** + * clapper_queue_select_item: + * @queue: a #ClapperQueue + * @item: (nullable): a #ClapperMediaItem or %NULL to unselect + * + * Selects #ClapperMediaItem from @queue as current one or + * unselects currently selected item when @item is %NULL. + * + * Returns: %TRUE if item could be selected/unselected, + * %FALSE if it was not in the queue. + */ +gboolean +clapper_queue_select_item (ClapperQueue *self, ClapperMediaItem *item) +{ + gboolean success = FALSE; + guint index = 0; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + g_return_val_if_fail (item == NULL || CLAPPER_IS_MEDIA_ITEM (item), FALSE); + + CLAPPER_QUEUE_REC_LOCK (self); + if (!item) + success = clapper_queue_select_index (self, CLAPPER_QUEUE_INVALID_POSITION); + else if (g_ptr_array_find (self->items, item, &index)) + success = clapper_queue_select_index (self, index); + CLAPPER_QUEUE_REC_UNLOCK (self); + + return success; +} + +/** + * clapper_queue_select_index: + * @queue: a #ClapperQueue + * @index: an item index or [const@Clapper.QUEUE_INVALID_POSITION] to unselect + * + * Selects #ClapperMediaItem at @index from @queue as current one or + * unselects currently selected index when @index is [const@Clapper.QUEUE_INVALID_POSITION]. + * + * Returns: %TRUE if item at @index could be selected/unselected, + * %FALSE if index was out of queue range. + */ +gboolean +clapper_queue_select_index (ClapperQueue *self, guint index) +{ + ClapperMediaItem *item = NULL; + gboolean success; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + + CLAPPER_QUEUE_REC_LOCK (self); + if (index != CLAPPER_QUEUE_INVALID_POSITION && index < self->items->len) + item = g_ptr_array_index (self->items, index); + if ((success = (index == CLAPPER_QUEUE_INVALID_POSITION + || index < self->items->len))) { + if (_replace_current_item_unlocked (self, item, index)) + _announce_current_item_and_index_change (self); + } + CLAPPER_QUEUE_REC_UNLOCK (self); + + return success; +} + +/** + * clapper_queue_select_next_item: + * @queue: a #ClapperQueue + * + * Selects next #ClapperMediaItem from @queue for playback. + * + * Note that this will try to select next item in the order + * of the queue, regardless of #ClapperQueueProgressionMode set. + * + * Returns: %TRUE if there was another media item in queue, %FALSE otherwise. + */ +gboolean +clapper_queue_select_next_item (ClapperQueue *self) +{ + gboolean success = FALSE; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + + CLAPPER_QUEUE_REC_LOCK (self); + + if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION + && self->current_index < self->items->len - 1) { + GST_DEBUG_OBJECT (self, "Selecting next queue item"); + success = clapper_queue_select_index (self, self->current_index + 1); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); + + return success; +} + +/** + * clapper_queue_select_previous_item: + * @queue: a #ClapperQueue + * + * Selects previous #ClapperMediaItem from @queue for playback. + * + * Note that this will try to select previous item in the order + * of the queue, regardless of #ClapperQueueProgressionMode set. + * + * Returns: %TRUE if there was previous media item in queue, %FALSE otherwise. + */ +gboolean +clapper_queue_select_previous_item (ClapperQueue *self) +{ + gboolean success = FALSE; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + + CLAPPER_QUEUE_REC_LOCK (self); + + if (self->current_index != CLAPPER_QUEUE_INVALID_POSITION + && self->current_index > 0) { + GST_DEBUG_OBJECT (self, "Selecting previous queue item"); + success = clapper_queue_select_index (self, self->current_index - 1); + } + + CLAPPER_QUEUE_REC_UNLOCK (self); + + return success; +} + +/** + * clapper_queue_get_item: (skip) + * @queue: a #ClapperQueue + * @index: an item index + * + * Get the #ClapperMediaItem at index. + * + * This behaves the same as [method@Gio.ListModel.get_item], and is here + * for code uniformity and convenience to avoid type casting by user. + * + * This function is not available in bindings as they already + * inherit `get_item()` method from [iface@Gio.ListModel] interface. + * + * Returns: (transfer full) (nullable): The #ClapperMediaItem at @index. + */ +ClapperMediaItem * +clapper_queue_get_item (ClapperQueue *self, guint index) +{ + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), NULL); + + return g_list_model_get_item (G_LIST_MODEL (self), index); +} + +/** + * clapper_queue_get_current_item: + * @queue: a #ClapperQueue + * + * Get the currently selected #ClapperMediaItem. + * + * Returns: (transfer full) (nullable): The current #ClapperMediaItem. + */ +ClapperMediaItem * +clapper_queue_get_current_item (ClapperQueue *self) +{ + ClapperMediaItem *item = NULL; + + /* XXX: For updating media item during playback we should + * use `player->played_item` instead to not be racy when + * changing and updating current item at the same time */ + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), NULL); + + CLAPPER_QUEUE_REC_LOCK (self); + if (self->current_item) + item = gst_object_ref (self->current_item); + CLAPPER_QUEUE_REC_UNLOCK (self); + + return item; +} + +/** + * clapper_queue_get_current_index: + * @queue: a #ClapperQueue + * + * Get index of the currently selected #ClapperMediaItem. + * + * Returns: Current item index or [const@Clapper.QUEUE_INVALID_POSITION] + * when nothing is selected. + */ +guint +clapper_queue_get_current_index (ClapperQueue *self) +{ + guint index; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), CLAPPER_QUEUE_INVALID_POSITION); + + CLAPPER_QUEUE_REC_LOCK (self); + index = self->current_index; + CLAPPER_QUEUE_REC_UNLOCK (self); + + return index; +} + +/** + * clapper_queue_item_is_current: + * @queue: a #ClapperQueue + * @item: a #ClapperMediaItem to check + * + * Checks if given #ClapperMediaItem is currently selected. + * + * Returns: %TRUE if @item is a current media item, %FALSE otherwise. + */ +gboolean +clapper_queue_item_is_current (ClapperQueue *self, ClapperMediaItem *item) +{ + gboolean is_current; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (item), FALSE); + + CLAPPER_QUEUE_REC_LOCK (self); + is_current = (item == self->current_item); + CLAPPER_QUEUE_REC_UNLOCK (self); + + return is_current; +} + +/** + * clapper_queue_find_item: + * @queue: a #ClapperQueue + * @item: a #ClapperMediaItem to search for + * @index: (optional) (out): return location for the index of + * the element, if found + * + * Get the index of #ClapperMediaItem within #ClapperQueue. + * + * Returns: %TRUE if @item is one of the elements of queue. + */ +gboolean +clapper_queue_find_item (ClapperQueue *self, ClapperMediaItem *item, guint *index) +{ + gboolean found; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (item), FALSE); + + CLAPPER_QUEUE_REC_LOCK (self); + found = g_ptr_array_find (self->items, item, index); + CLAPPER_QUEUE_REC_UNLOCK (self); + + return found; +} + +/** + * clapper_queue_get_n_items: (skip) + * @queue: a #ClapperQueue + * + * Get the number of items in #ClapperQueue. + * + * This behaves the same as g_list_model_get_n_items(), and is here + * for code uniformity and convenience to avoid type casting by user. + * + * This function is not available in bindings as they already + * inherit get_n_items() method from #GListModel interface. + * + * Returns: The number of items in #ClapperQueue. + */ +guint +clapper_queue_get_n_items (ClapperQueue *self) +{ + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), 0); + + return g_list_model_get_n_items (G_LIST_MODEL (self)); +} + +/** + * clapper_queue_set_progression_mode: + * @queue: a #ClapperQueue + * @mode: a #ClapperQueueProgressionMode + * + * Set the #ClapperQueueProgressionMode of the #ClapperQueue. + * + * Changing the mode set will alter next item selection at the + * end of playback. For possible values and their descriptions, + * see #ClapperQueueProgressionMode documentation. + */ +void +clapper_queue_set_progression_mode (ClapperQueue *self, ClapperQueueProgressionMode mode) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + + GST_OBJECT_LOCK (self); + if ((changed = self->progression_mode != mode)) + self->progression_mode = mode; + GST_OBJECT_UNLOCK (self); + + if (changed) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + /* Start shuffle from the current item, allowing + * reselecting past items already used without it */ + if (mode == CLAPPER_QUEUE_PROGRESSION_SHUFFLE) { + CLAPPER_QUEUE_REC_LOCK (self); + + _reset_shuffle_unlocked (self); + if (self->current_item) + clapper_media_item_set_used (self->current_item, TRUE); + + CLAPPER_QUEUE_REC_UNLOCK (self); + } + + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_PROGRESSION_MODE]); + if (clapper_player_get_have_features (player)) + clapper_features_manager_trigger_queue_progression_changed (player->features_manager, mode); + + gst_object_unref (player); + } +} + +/** + * clapper_queue_get_progression_mode: + * @queue: a #ClapperQueue + * + * Get the #ClapperQueueProgressionMode of the #ClapperQueue. + * + * Returns: a currently set #ClapperQueueProgressionMode. + */ +ClapperQueueProgressionMode +clapper_queue_get_progression_mode (ClapperQueue *self) +{ + ClapperQueueProgressionMode mode; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), DEFAULT_PROGRESSION_MODE); + + GST_OBJECT_LOCK (self); + mode = self->progression_mode; + GST_OBJECT_UNLOCK (self); + + return mode; +} + +/** + * clapper_queue_set_gapless: + * @queue: a #ClapperQueue + * @gapless: %TRUE to enable, %FALSE otherwise. + * + * Set #ClapperQueue progression to be gapless. + * + * Gapless playback will try to re-use as much as possible of underlying + * GStreamer elements when #ClapperQueue progresses, removing any + * potential gap in the data. + * + * Enabling this option mostly makes sense when used together with + * [property@Clapper.Queue:progression-mode] property set to + * [enum@Clapper.QueueProgressionMode.CONSECUTIVE]. + * + * NOTE: This feature within GStreamer is rather new and + * might still cause playback issues. Disabled by default. + */ +void +clapper_queue_set_gapless (ClapperQueue *self, gboolean gapless) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + + GST_OBJECT_LOCK (self); + if ((changed = self->gapless != gapless)) + self->gapless = gapless; + GST_OBJECT_UNLOCK (self); + + if (changed) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_GAPLESS]); + + gst_object_unref (player); + } +} + +/** + * clapper_queue_get_gapless: + * @queue: a #ClapperQueue + * + * Get if #ClapperQueue is set to use gapless progression. + * + * Returns: %TRUE if enabled, %FALSE otherwise. + */ +gboolean +clapper_queue_get_gapless (ClapperQueue *self) +{ + gboolean gapless; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + + GST_OBJECT_LOCK (self); + gapless = self->gapless; + GST_OBJECT_UNLOCK (self); + + return gapless; +} + +/** + * clapper_queue_set_instant: + * @queue: a #ClapperQueue + * @instant: %TRUE to enable, %FALSE otherwise. + * + * Set #ClapperQueue media item changes to be instant. + * + * Instant will try to re-use as much as possible of underlying + * GStreamer elements when #ClapperMediaItem is selected, allowing + * media item change requests to be faster. + * + * NOTE: This feature within GStreamer is rather new and + * might still cause playback issues. Disabled by default. + */ +void +clapper_queue_set_instant (ClapperQueue *self, gboolean instant) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_QUEUE (self)); + + GST_OBJECT_LOCK (self); + if ((changed = self->instant != instant)) + self->instant = instant; + GST_OBJECT_UNLOCK (self); + + if (changed) { + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_INSTANT]); + + gst_object_unref (player); + } +} + +/** + * clapper_queue_get_instant: + * @queue: a #ClapperQueue + * + * Get if #ClapperQueue is set to use instant media item changes. + * + * Returns: %TRUE if enabled, %FALSE otherwise. + */ +gboolean +clapper_queue_get_instant (ClapperQueue *self) +{ + gboolean instant; + + g_return_val_if_fail (CLAPPER_IS_QUEUE (self), FALSE); + + GST_OBJECT_LOCK (self); + instant = self->instant; + GST_OBJECT_UNLOCK (self); + + return instant; +} + +static void +_item_remove_func (ClapperMediaItem *item) +{ + gst_object_unparent (GST_OBJECT_CAST (item)); + gst_object_unref (item); +} + +static void +clapper_queue_init (ClapperQueue *self) +{ + g_rec_mutex_init (&self->rec_lock); + + self->items = g_ptr_array_new_with_free_func ((GDestroyNotify) _item_remove_func); + + self->current_index = CLAPPER_QUEUE_INVALID_POSITION; + self->progression_mode = DEFAULT_PROGRESSION_MODE; + self->gapless = DEFAULT_GAPLESS; + self->instant = DEFAULT_INSTANT; +} + +static void +clapper_queue_finalize (GObject *object) +{ + ClapperQueue *self = CLAPPER_QUEUE_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + g_rec_mutex_clear (&self->rec_lock); + + gst_clear_object (&self->current_item); + g_ptr_array_unref (self->items); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_queue_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperQueue *self = CLAPPER_QUEUE_CAST (object); + + switch (prop_id) { + case PROP_CURRENT_ITEM: + g_value_take_object (value, clapper_queue_get_current_item (self)); + break; + case PROP_CURRENT_INDEX: + g_value_set_uint (value, clapper_queue_get_current_index (self)); + break; + case PROP_N_ITEMS: + g_value_set_uint (value, clapper_queue_get_n_items (self)); + break; + case PROP_PROGRESSION_MODE: + g_value_set_enum (value, clapper_queue_get_progression_mode (self)); + break; + case PROP_GAPLESS: + g_value_set_boolean (value, clapper_queue_get_gapless (self)); + break; + case PROP_INSTANT: + g_value_set_boolean (value, clapper_queue_get_instant (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_queue_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperQueue *self = CLAPPER_QUEUE_CAST (object); + + switch (prop_id) { + case PROP_CURRENT_INDEX: + clapper_queue_select_index (self, g_value_get_uint (value)); + break; + case PROP_PROGRESSION_MODE: + clapper_queue_set_progression_mode (self, g_value_get_enum (value)); + break; + case PROP_GAPLESS: + clapper_queue_set_gapless (self, g_value_get_boolean (value)); + break; + case PROP_INSTANT: + clapper_queue_set_instant (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_queue_class_init (ClapperQueueClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperqueue", 0, + "Clapper Queue"); + + gobject_class->get_property = clapper_queue_get_property; + gobject_class->set_property = clapper_queue_set_property; + gobject_class->finalize = clapper_queue_finalize; + + /** + * ClapperQueue:current-item: + * + * Currently selected media item for playback. + */ + param_specs[PROP_CURRENT_ITEM] = g_param_spec_object ("current-item", + NULL, NULL, CLAPPER_TYPE_MEDIA_ITEM, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperQueue:current-index: + * + * Index of currently selected media item for playback. + */ + param_specs[PROP_CURRENT_INDEX] = g_param_spec_uint ("current-index", + NULL, NULL, 0, G_MAXUINT, CLAPPER_QUEUE_INVALID_POSITION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperQueue:n-items: + * + * Number of media items in the queue. + */ + param_specs[PROP_N_ITEMS] = g_param_spec_uint ("n-items", + NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperQueue:progression-mode: + * + * Queue progression mode. + */ + param_specs[PROP_PROGRESSION_MODE] = g_param_spec_enum ("progression-mode", + NULL, NULL, CLAPPER_TYPE_QUEUE_PROGRESSION_MODE, DEFAULT_PROGRESSION_MODE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperQueue:gapless: + * + * Use gapless progression. + */ + param_specs[PROP_GAPLESS] = g_param_spec_boolean ("gapless", + NULL, NULL, DEFAULT_GAPLESS, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperQueue:instant: + * + * Use instant media item changes. + */ + param_specs[PROP_INSTANT] = g_param_spec_boolean ("instant", + NULL, NULL, DEFAULT_INSTANT, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-queue.h b/src/lib/clapper/clapper-queue.h new file mode 100644 index 00000000..162c236d --- /dev/null +++ b/src/lib/clapper/clapper-queue.h @@ -0,0 +1,93 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_QUEUE (clapper_queue_get_type()) +#define CLAPPER_QUEUE_CAST(obj) ((ClapperQueue *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperQueue, clapper_queue, CLAPPER, QUEUE, GstObject) + +/** + * CLAPPER_QUEUE_INVALID_POSITION: + * + * The value used to refer to an invalid position in a #ClapperQueue + */ +#define CLAPPER_QUEUE_INVALID_POSITION ((guint) 0xffffffff) + +void clapper_queue_add_item (ClapperQueue *queue, ClapperMediaItem *item); + +void clapper_queue_insert_item (ClapperQueue *queue, ClapperMediaItem *item, gint index); + +void clapper_queue_reposition_item (ClapperQueue *queue, ClapperMediaItem *item, gint index); + +void clapper_queue_remove_item (ClapperQueue *queue, ClapperMediaItem *item); + +void clapper_queue_remove_index (ClapperQueue *queue, guint index); + +ClapperMediaItem * clapper_queue_steal_index (ClapperQueue *queue, guint index); + +void clapper_queue_clear (ClapperQueue *queue); + +gboolean clapper_queue_select_item (ClapperQueue *queue, ClapperMediaItem *item); + +gboolean clapper_queue_select_index (ClapperQueue *queue, guint index); + +gboolean clapper_queue_select_next_item (ClapperQueue *queue); + +gboolean clapper_queue_select_previous_item (ClapperQueue *queue); + +ClapperMediaItem * clapper_queue_get_item (ClapperQueue *queue, guint index); + +ClapperMediaItem * clapper_queue_get_current_item (ClapperQueue *queue); + +guint clapper_queue_get_current_index (ClapperQueue *queue); + +gboolean clapper_queue_item_is_current (ClapperQueue *queue, ClapperMediaItem *item); + +gboolean clapper_queue_find_item (ClapperQueue *queue, ClapperMediaItem *item, guint *index); + +guint clapper_queue_get_n_items (ClapperQueue *queue); + +void clapper_queue_set_progression_mode (ClapperQueue *queue, ClapperQueueProgressionMode mode); + +ClapperQueueProgressionMode clapper_queue_get_progression_mode (ClapperQueue *queue); + +void clapper_queue_set_gapless (ClapperQueue *queue, gboolean gapless); + +gboolean clapper_queue_get_gapless (ClapperQueue *queue); + +void clapper_queue_set_instant (ClapperQueue *queue, gboolean instant); + +gboolean clapper_queue_get_instant (ClapperQueue *queue); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-stream-list-private.h b/src/lib/clapper/clapper-stream-list-private.h new file mode 100644 index 00000000..56d98294 --- /dev/null +++ b/src/lib/clapper/clapper-stream-list-private.h @@ -0,0 +1,37 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "clapper-stream-list.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperStreamList * clapper_stream_list_new (void); + +G_GNUC_INTERNAL +void clapper_stream_list_replace_streams (ClapperStreamList *list, GList *streams); + +G_GNUC_INTERNAL +ClapperStream * clapper_stream_list_get_stream_for_gst_stream (ClapperStreamList *list, GstStream *gst_stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-stream-list.c b/src/lib/clapper/clapper-stream-list.c new file mode 100644 index 00000000..1e17fd98 --- /dev/null +++ b/src/lib/clapper/clapper-stream-list.c @@ -0,0 +1,546 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperStreamList: + * + * A list of media streams. + */ + +#include + +#include "clapper-stream-list-private.h" +#include "clapper-stream-private.h" +#include "clapper-player-private.h" +#include "clapper-playbin-bus-private.h" + +#define GST_CAT_DEFAULT clapper_stream_list_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperStreamList +{ + GstObject parent; + + GPtrArray *streams; + + ClapperStream *current_stream; + guint current_index; + + gboolean in_refresh; +}; + +enum +{ + PROP_0, + PROP_CURRENT_STREAM, + PROP_CURRENT_INDEX, + PROP_N_STREAMS, + PROP_LAST +}; + +static void clapper_stream_list_model_iface_init (GListModelInterface *iface); + +#define parent_class clapper_stream_list_parent_class +G_DEFINE_TYPE_WITH_CODE (ClapperStreamList, clapper_stream_list, GST_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, clapper_stream_list_model_iface_init)); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static GType +clapper_stream_list_model_get_item_type (GListModel *model) +{ + return CLAPPER_TYPE_STREAM; +} + +static guint +clapper_stream_list_model_get_n_items (GListModel *model) +{ + ClapperStreamList *self = CLAPPER_STREAM_LIST_CAST (model); + guint n_streams; + + GST_OBJECT_LOCK (self); + n_streams = self->streams->len; + GST_OBJECT_UNLOCK (self); + + return n_streams; +} + +static gpointer +clapper_stream_list_model_get_item (GListModel *model, guint index) +{ + ClapperStreamList *self = CLAPPER_STREAM_LIST_CAST (model); + ClapperStream *stream = NULL; + + GST_OBJECT_LOCK (self); + if (G_LIKELY (index < self->streams->len)) + stream = gst_object_ref (g_ptr_array_index (self->streams, index)); + GST_OBJECT_UNLOCK (self); + + return stream; +} + +static void +clapper_stream_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = clapper_stream_list_model_get_item_type; + iface->get_n_items = clapper_stream_list_model_get_n_items; + iface->get_item = clapper_stream_list_model_get_item; +} + +static void +_post_stream_change (ClapperStreamList *self) +{ + ClapperPlayer *player; + + GST_OBJECT_LOCK (self); + /* We will do a single initial selection ourselves + * after all lists are refreshed, so do nothing here yet */ + if (G_UNLIKELY (self->in_refresh)) { + GST_WARNING_OBJECT (self, "Trying to select/autoselect stream before" + " initial selection. This is not supported, please fix your app."); + GST_OBJECT_UNLOCK (self); + return; + } + GST_OBJECT_UNLOCK (self); + + player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + if (G_LIKELY (player != NULL)) { + clapper_playbin_bus_post_stream_change (player->bus); + gst_object_unref (player); + } +} + +static void +_announce_current_stream_and_index_change (ClapperStreamList *self) +{ + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + gboolean is_main_thread; + + if (G_UNLIKELY (player == NULL)) + return; + + is_main_thread = g_main_context_is_owner (g_main_context_default ()); + + GST_DEBUG_OBJECT (self, "Announcing current stream change from %smain thread," + " now: %" GST_PTR_FORMAT " (index: %u)", + (is_main_thread) ? "" : "non-", self->current_stream, self->current_index); + + if (is_main_thread) { + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_STREAM]); + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_INDEX]); + } else { + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_STREAM]); + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CURRENT_INDEX]); + } + + gst_object_unref (player); +} + +static gboolean +clapper_stream_list_select_index_unlocked (ClapperStreamList *self, guint index) +{ + ClapperStream *stream = NULL; + + if (index != CLAPPER_STREAM_LIST_INVALID_POSITION) + stream = g_ptr_array_index (self->streams, index); + + if (gst_object_replace ((GstObject **) &self->current_stream, GST_OBJECT_CAST (stream))) { + self->current_index = index; + return TRUE; + } + + return FALSE; +} + +/* + * clapper_stream_list_new: + * + * Returns: (transfer full): a new #ClapperStreamList instance + */ +ClapperStreamList * +clapper_stream_list_new (void) +{ + ClapperStreamList *list; + + list = g_object_new (CLAPPER_TYPE_STREAM_LIST, NULL); + gst_object_ref_sink (list); + + return list; +} + +/** + * clapper_stream_list_select_stream: + * @list: a #ClapperStreamList + * @stream: a #ClapperStream + * + * Selects #ClapperStream from @list to be activated. + * + * Returns: %TRUE if stream was in the @list, %FALSE otherwise. + */ +gboolean +clapper_stream_list_select_stream (ClapperStreamList *self, ClapperStream *stream) +{ + gboolean found, changed = FALSE; + guint index = 0; + + g_return_val_if_fail (CLAPPER_IS_STREAM_LIST (self), FALSE); + g_return_val_if_fail (CLAPPER_IS_STREAM (stream), FALSE); + + GST_OBJECT_LOCK (self); + if ((found = g_ptr_array_find (self->streams, stream, &index))) + changed = clapper_stream_list_select_index_unlocked (self, index); + GST_OBJECT_UNLOCK (self); + + if (changed) { + _post_stream_change (self); + _announce_current_stream_and_index_change (self); + } + + return found; +} + +/** + * clapper_stream_list_select_index: + * @list: a #ClapperStreamList + * @index: a stream index + * + * Selects #ClapperStream at @index from @list as current one. + * + * Returns: %TRUE if stream could be selected, %FALSE otherwise. + */ +gboolean +clapper_stream_list_select_index (ClapperStreamList *self, guint index) +{ + gboolean found, changed = FALSE; + + g_return_val_if_fail (CLAPPER_IS_STREAM_LIST (self), FALSE); + g_return_val_if_fail (index != CLAPPER_STREAM_LIST_INVALID_POSITION, FALSE); + + GST_OBJECT_LOCK (self); + if ((found = index < self->streams->len)) + changed = clapper_stream_list_select_index_unlocked (self, index); + GST_OBJECT_UNLOCK (self); + + if (changed) { + _post_stream_change (self); + _announce_current_stream_and_index_change (self); + } + + return found; +} + +/** + * clapper_stream_list_get_stream: + * @list: a #ClapperStreamList + * @index: a stream index + * + * Get the #ClapperStream at index. + * + * This behaves the same as [method@Gio.ListModel.get_item], and is here + * for code uniformity and convenience to avoid type casting by user. + * + * Returns: (transfer full) (nullable): The #ClapperStream at @index. + */ +ClapperStream * +clapper_stream_list_get_stream (ClapperStreamList *self, guint index) +{ + g_return_val_if_fail (CLAPPER_IS_STREAM_LIST (self), NULL); + + return g_list_model_get_item (G_LIST_MODEL (self), index); +} + +/** + * clapper_stream_list_get_current_stream: + * @list: a #ClapperStreamList + * + * Get the currently selected #ClapperStream. + * + * Returns: (transfer full) (nullable): The current #ClapperStream. + */ +ClapperStream * +clapper_stream_list_get_current_stream (ClapperStreamList *self) +{ + ClapperStream *stream = NULL; + + g_return_val_if_fail (CLAPPER_IS_STREAM_LIST (self), NULL); + + GST_OBJECT_LOCK (self); + if (self->current_stream) + stream = gst_object_ref (self->current_stream); + GST_OBJECT_UNLOCK (self); + + return stream; +} + +/** + * clapper_stream_list_get_current_index: + * @list: a #ClapperStreamList + * + * Get index of the currently selected #ClapperStream. + * + * Returns: Current stream index or [const@Clapper.STREAM_LIST_INVALID_POSITION] + * when nothing is selected. + */ +guint +clapper_stream_list_get_current_index (ClapperStreamList *self) +{ + guint index; + + g_return_val_if_fail (CLAPPER_IS_STREAM_LIST (self), CLAPPER_STREAM_LIST_INVALID_POSITION); + + GST_OBJECT_LOCK (self); + index = self->current_index; + GST_OBJECT_UNLOCK (self); + + return index; +} + +/** + * clapper_stream_list_get_n_streams: + * @list: a #ClapperStreamList + * + * Get the number of streams in #ClapperStreamList. + * + * This behaves the same as g_list_model_get_n_items(), and is here + * for code uniformity and convenience to avoid type casting by user. + * + * Returns: The number of streams in #ClapperStreamList. + */ +guint +clapper_stream_list_get_n_streams (ClapperStreamList *self) +{ + g_return_val_if_fail (CLAPPER_IS_STREAM_LIST (self), 0); + + return g_list_model_get_n_items (G_LIST_MODEL (self)); +} + +void +clapper_stream_list_replace_streams (ClapperStreamList *self, GList *streams) +{ + GList *st; + guint prev_n_streams, n_streams; + guint index = 0, selected_index = 0; + gboolean changed, selected = FALSE; + + GST_OBJECT_LOCK (self); + + self->in_refresh = TRUE; + prev_n_streams = self->streams->len; + + if (prev_n_streams > 0) + g_ptr_array_remove_range (self->streams, 0, prev_n_streams); + + for (st = streams; st != NULL; st = st->next) { + ClapperStream *stream = CLAPPER_STREAM_CAST (st->data); + + /* Try to select first "default" stream, while avoiding + * streams that should not be selected by default. + * NOTE: This works only with playbin3 */ + if (!selected) { + GstStream *gst_stream = clapper_stream_get_gst_stream (stream); + GstStreamFlags flags = gst_stream_get_stream_flags (gst_stream); + + GST_LOG_OBJECT (self, "Stream flags: %i", flags); + + if ((flags & GST_STREAM_FLAG_SELECT) == GST_STREAM_FLAG_SELECT) { + GST_DEBUG_OBJECT (self, "Stream has \"select\" stream flag"); + selected = TRUE; + selected_index = index; + } else if ((flags & GST_STREAM_FLAG_UNSELECT) == GST_STREAM_FLAG_UNSELECT) { + GST_DEBUG_OBJECT (self, "Stream has \"unselect\" stream flag"); + if (selected_index == index) + selected_index++; + } + } + + g_ptr_array_add (self->streams, stream); + gst_object_set_parent (GST_OBJECT_CAST (stream), GST_OBJECT_CAST (self)); + + index++; + } + + n_streams = self->streams->len; + + GST_OBJECT_UNLOCK (self); + + if (prev_n_streams > 0 || n_streams > 0) { + g_list_model_items_changed (G_LIST_MODEL (self), 0, prev_n_streams, n_streams); + + if (prev_n_streams != n_streams) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_N_STREAMS]); + } + + /* This can happen when ALL streams had "unselect" flag. + * In this case just select the first one. */ + if (n_streams > 0) { + if (G_UNLIKELY (selected_index > n_streams - 1)) + selected_index = 0; + } else { + selected_index = CLAPPER_STREAM_LIST_INVALID_POSITION; + } + + /* TODO: Consider adding an API (or signal that returns index) + * to select preferred initial stream (by e.g. language) */ + + GST_OBJECT_LOCK (self); + changed = clapper_stream_list_select_index_unlocked (self, selected_index); + GST_OBJECT_UNLOCK (self); + + if (changed) { + GST_INFO_OBJECT (self, "Initially selecting stream index: %u", selected_index); + _announce_current_stream_and_index_change (self); + } + + GST_OBJECT_LOCK (self); + self->in_refresh = FALSE; + GST_OBJECT_UNLOCK (self); +} + +ClapperStream * +clapper_stream_list_get_stream_for_gst_stream (ClapperStreamList *self, GstStream *gst_stream) +{ + ClapperStream *found_stream = NULL; + guint i; + + GST_OBJECT_LOCK (self); + + for (i = 0; i < self->streams->len; ++i) { + ClapperStream *stream = g_ptr_array_index (self->streams, i); + GstStream *list_gst_stream = clapper_stream_get_gst_stream (stream); + + if (gst_stream == list_gst_stream) { + found_stream = gst_object_ref (stream); + break; + } + } + + GST_OBJECT_UNLOCK (self); + + return found_stream; +} + +static void +_stream_remove_func (ClapperStream *stream) +{ + gst_object_unparent (GST_OBJECT_CAST (stream)); + gst_object_unref (stream); +} + +static void +clapper_stream_list_init (ClapperStreamList *self) +{ + self->streams = g_ptr_array_new_with_free_func ((GDestroyNotify) _stream_remove_func); + self->current_index = CLAPPER_STREAM_LIST_INVALID_POSITION; +} + +static void +clapper_stream_list_finalize (GObject *object) +{ + ClapperStreamList *self = CLAPPER_STREAM_LIST_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + gst_clear_object (&self->current_stream); + g_ptr_array_unref (self->streams); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_stream_list_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperStreamList *self = CLAPPER_STREAM_LIST_CAST (object); + + switch (prop_id) { + case PROP_CURRENT_STREAM: + g_value_take_object (value, clapper_stream_list_get_current_stream (self)); + break; + case PROP_CURRENT_INDEX: + g_value_set_uint (value, clapper_stream_list_get_current_index (self)); + break; + case PROP_N_STREAMS: + g_value_set_uint (value, clapper_stream_list_get_n_streams (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_stream_list_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperStreamList *self = CLAPPER_STREAM_LIST_CAST (object); + + switch (prop_id) { + case PROP_CURRENT_INDEX: + clapper_stream_list_select_index (self, g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_stream_list_class_init (ClapperStreamListClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperstreamlist", 0, + "Clapper Stream List"); + + gobject_class->get_property = clapper_stream_list_get_property; + gobject_class->set_property = clapper_stream_list_set_property; + gobject_class->finalize = clapper_stream_list_finalize; + + /** + * ClapperStreamList:current-stream: + * + * Currently selected stream. + */ + param_specs[PROP_CURRENT_STREAM] = g_param_spec_object ("current-stream", + NULL, NULL, CLAPPER_TYPE_STREAM, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperStreamList:current-index: + * + * Index of currently selected stream. + */ + param_specs[PROP_CURRENT_INDEX] = g_param_spec_uint ("current-index", + NULL, NULL, 0, G_MAXUINT, CLAPPER_STREAM_LIST_INVALID_POSITION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperStreamList:n-streams: + * + * Number of streams in the list. + */ + param_specs[PROP_N_STREAMS] = g_param_spec_uint ("n-streams", + NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-stream-list.h b/src/lib/clapper/clapper-stream-list.h new file mode 100644 index 00000000..5c6f8159 --- /dev/null +++ b/src/lib/clapper/clapper-stream-list.h @@ -0,0 +1,58 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_STREAM_LIST (clapper_stream_list_get_type()) +#define CLAPPER_STREAM_LIST_CAST(obj) ((ClapperStreamList *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperStreamList, clapper_stream_list, CLAPPER, STREAM_LIST, GstObject) + +/** + * CLAPPER_STREAM_LIST_INVALID_POSITION: + * + * The value used to refer to an invalid position in a #ClapperStreamList + */ +#define CLAPPER_STREAM_LIST_INVALID_POSITION ((guint) 0xffffffff) + +gboolean clapper_stream_list_select_stream (ClapperStreamList *list, ClapperStream *stream); + +gboolean clapper_stream_list_select_index (ClapperStreamList *list, guint index); + +ClapperStream * clapper_stream_list_get_stream (ClapperStreamList *list, guint index); + +ClapperStream * clapper_stream_list_get_current_stream (ClapperStreamList *list); + +guint clapper_stream_list_get_current_index (ClapperStreamList *list); + +guint clapper_stream_list_get_n_streams (ClapperStreamList *list); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-stream-private.h b/src/lib/clapper/clapper-stream-private.h new file mode 100644 index 00000000..901f27f7 --- /dev/null +++ b/src/lib/clapper/clapper-stream-private.h @@ -0,0 +1,53 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-stream.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperStream * clapper_stream_new (GstStream *gst_stream); + +G_GNUC_INTERNAL +void clapper_stream_set_gst_stream (ClapperStream *stream, GstStream *gst_stream); + +G_GNUC_INTERNAL +GstStream * clapper_stream_get_gst_stream (ClapperStream *stream); + +G_GNUC_INTERNAL +void clapper_stream_take_string_prop (ClapperStream *stream, GParamSpec *pspec, gchar **ptr, gchar *value); + +G_GNUC_INTERNAL +void clapper_stream_set_string_prop (ClapperStream *stream, GParamSpec *pspec, gchar **ptr, const gchar *value); + +G_GNUC_INTERNAL +void clapper_stream_set_int_prop (ClapperStream *stream, GParamSpec *pspec, gint *ptr, gint value); + +G_GNUC_INTERNAL +void clapper_stream_set_uint_prop (ClapperStream *stream, GParamSpec *pspec, guint *ptr, guint value); + +G_GNUC_INTERNAL +void clapper_stream_set_double_prop (ClapperStream *stream, GParamSpec *pspec, gdouble *ptr, gdouble value); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-stream.c b/src/lib/clapper/clapper-stream.c new file mode 100644 index 00000000..5aa84298 --- /dev/null +++ b/src/lib/clapper/clapper-stream.c @@ -0,0 +1,377 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperStream: + * + * Represents a stream within media. + */ + +#include + +#include "clapper-stream-private.h" +#include "clapper-player-private.h" + +#define GST_CAT_DEFAULT clapper_stream_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +typedef struct _ClapperStreamPrivate ClapperStreamPrivate; + +struct _ClapperStreamPrivate +{ + GstStream *gst_stream; + + ClapperStreamType stream_type; + gchar *title; +}; + +enum +{ + PROP_0, + PROP_STREAM_TYPE, + PROP_TITLE, + PROP_LAST +}; + +#define parent_class clapper_stream_parent_class +G_DEFINE_TYPE_WITH_PRIVATE (ClapperStream, clapper_stream, GST_TYPE_OBJECT); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void +_update_using_tags (ClapperStream *self, GstTagList *tags) +{ + ClapperStreamPrivate *priv = clapper_stream_get_instance_private (self); + gchar *title = NULL; + + gst_tag_list_get_string_index (tags, GST_TAG_TITLE, 0, &title); + clapper_stream_take_string_prop (self, param_specs[PROP_TITLE], &priv->title, title); +} + +ClapperStream * +clapper_stream_new (GstStream *gst_stream) +{ + ClapperStream *stream; + ClapperStreamPrivate *priv; + + stream = g_object_new (CLAPPER_TYPE_STREAM, NULL); + gst_object_ref_sink (stream); + + priv = clapper_stream_get_instance_private (stream); + priv->gst_stream = gst_object_ref (gst_stream); + + return stream; +} + +/* + * This should be called only during stream construction + */ +void +clapper_stream_set_gst_stream (ClapperStream *stream, GstStream *gst_stream) +{ + ClapperStreamPrivate *priv = clapper_stream_get_instance_private (stream); + + if (G_LIKELY (gst_object_replace ((GstObject **) &priv->gst_stream, + GST_OBJECT_CAST (gst_stream)))) { + GstCaps *caps = gst_stream_get_caps (gst_stream); + GstTagList *tags = gst_stream_get_tags (gst_stream); + + if (caps || tags) { + ClapperStreamClass *stream_class = CLAPPER_STREAM_GET_CLASS (stream); + + stream_class->internal_stream_updated (stream, caps, tags); + + gst_clear_caps (&caps); + gst_clear_tag_list (&tags); + } + } +} + +/** + * clapper_stream_get_stream_type: + * @stream: a #ClapperStream + * + * Get the #ClapperStreamType of @stream. + * + * Returns: type of stream. + */ +ClapperStreamType +clapper_stream_get_stream_type (ClapperStream *self) +{ + ClapperStreamPrivate *priv; + + g_return_val_if_fail (CLAPPER_IS_STREAM (self), CLAPPER_STREAM_TYPE_UNKNOWN); + + priv = clapper_stream_get_instance_private (self); + + return priv->stream_type; +} + +/** + * clapper_stream_get_title: + * @stream: a #ClapperStream + * + * Get the title of @stream, if any. + * + * Returns: (transfer full) (nullable): title of stream. + */ +gchar * +clapper_stream_get_title (ClapperStream *self) +{ + ClapperStreamPrivate *priv; + gchar *title; + + g_return_val_if_fail (CLAPPER_IS_STREAM (self), NULL); + + priv = clapper_stream_get_instance_private (self); + + GST_OBJECT_LOCK (self); + title = g_strdup (priv->title); + GST_OBJECT_UNLOCK (self); + + return title; +} + +GstStream * +clapper_stream_get_gst_stream (ClapperStream *self) +{ + ClapperStreamPrivate *priv = clapper_stream_get_instance_private (self); + + return priv->gst_stream; +} + +static void +clapper_stream_prop_notify (ClapperStream *self, GParamSpec *pspec) +{ + ClapperPlayer *player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)); + + /* NOTE: This happens when props are initially set during construction + * (before parented) which is fine, since we do not have to notify + * when user does not have access to object yet. */ + if (!player) + return; + + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), pspec); + + gst_object_unref (player); +} + +void +clapper_stream_take_string_prop (ClapperStream *self, + GParamSpec *pspec, gchar **ptr, gchar *value) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = g_strcmp0 (*ptr, value) != 0)) { + g_free (*ptr); + *ptr = value; + + GST_DEBUG_OBJECT (self, "Set %s: %s", + g_param_spec_get_name (pspec), value); + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_stream_prop_notify (self, pspec); + else + g_free (value); +} + +void +clapper_stream_set_string_prop (ClapperStream *self, + GParamSpec *pspec, gchar **ptr, const gchar *value) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = g_set_str (ptr, value))) { + GST_DEBUG_OBJECT (self, "Set %s: %s", + g_param_spec_get_name (pspec), value); + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_stream_prop_notify (self, pspec); +} + +void +clapper_stream_set_int_prop (ClapperStream *self, + GParamSpec *pspec, gint *ptr, gint value) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = *ptr != value)) { + *ptr = value; + + GST_DEBUG_OBJECT (self, "Set %s: %i", + g_param_spec_get_name (pspec), value); + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_stream_prop_notify (self, pspec); +} + +void +clapper_stream_set_uint_prop (ClapperStream *self, + GParamSpec *pspec, guint *ptr, guint value) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = *ptr != value)) { + *ptr = value; + + GST_DEBUG_OBJECT (self, "Set %s: %u", + g_param_spec_get_name (pspec), value); + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_stream_prop_notify (self, pspec); +} + +void +clapper_stream_set_double_prop (ClapperStream *self, + GParamSpec *pspec, gdouble *ptr, gdouble value) +{ + gboolean changed; + + GST_OBJECT_LOCK (self); + if ((changed = !G_APPROX_VALUE (*ptr, value, FLT_EPSILON))) { + *ptr = value; + + GST_DEBUG_OBJECT (self, "Set %s: %lf", + g_param_spec_get_name (pspec), value); + } + GST_OBJECT_UNLOCK (self); + + if (changed) + clapper_stream_prop_notify (self, pspec); +} + +static void +clapper_stream_init (ClapperStream *self) +{ + ClapperStreamPrivate *priv = clapper_stream_get_instance_private (self); + + priv->stream_type = CLAPPER_STREAM_TYPE_UNKNOWN; +} + +static void +clapper_stream_finalize (GObject *object) +{ + ClapperStream *self = CLAPPER_STREAM_CAST (object); + ClapperStreamPrivate *priv = clapper_stream_get_instance_private (self); + + GST_TRACE_OBJECT (self, "Finalize"); + + gst_clear_object (&priv->gst_stream); + + g_free (priv->title); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_stream_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperStream *self = CLAPPER_STREAM_CAST (object); + + switch (prop_id) { + case PROP_STREAM_TYPE: + g_value_set_enum (value, clapper_stream_get_stream_type (self)); + break; + case PROP_TITLE: + g_value_take_string (value, clapper_stream_get_title (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_stream_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperStream *self = CLAPPER_STREAM_CAST (object); + ClapperStreamPrivate *priv = clapper_stream_get_instance_private (self); + + switch (prop_id) { + case PROP_STREAM_TYPE: + priv->stream_type = g_value_get_enum (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_stream_internal_stream_updated (ClapperStream *self, + GstCaps *caps, GstTagList *tags) +{ + if (caps) + GST_LOG_OBJECT (self, "Caps: %" GST_PTR_FORMAT, caps); + if (tags) { + GST_LOG_OBJECT (self, "Tags: %" GST_PTR_FORMAT, tags); + _update_using_tags (self, tags); + } +} + +static void +clapper_stream_class_init (ClapperStreamClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperStreamClass *stream_class = (ClapperStreamClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperstream", 0, + "Clapper Stream"); + + gobject_class->get_property = clapper_stream_get_property; + gobject_class->set_property = clapper_stream_set_property; + gobject_class->finalize = clapper_stream_finalize; + + stream_class->internal_stream_updated = clapper_stream_internal_stream_updated; + + /** + * ClapperStream:stream-type: + * + * Type of stream. + */ + param_specs[PROP_STREAM_TYPE] = g_param_spec_enum ("stream-type", + NULL, NULL, CLAPPER_TYPE_STREAM_TYPE, CLAPPER_STREAM_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperStream:title: + * + * Title of stream. + */ + param_specs[PROP_TITLE] = g_param_spec_string ("title", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-stream.h b/src/lib/clapper/clapper-stream.h new file mode 100644 index 00000000..14369533 --- /dev/null +++ b/src/lib/clapper/clapper-stream.h @@ -0,0 +1,65 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_STREAM (clapper_stream_get_type()) +#define CLAPPER_STREAM_CAST(obj) ((ClapperStream *)(obj)) + +G_DECLARE_DERIVABLE_TYPE (ClapperStream, clapper_stream, CLAPPER, STREAM, GstObject) + +struct _ClapperStreamClass +{ + GstObjectClass parent_class; + + /** + * ClapperStreamClass::internal_stream_updated: + * @stream: a #ClapperStream + * @caps: (nullable): an updated #GstCaps if changed + * @tags: (nullable): an updated #GstTagList if changed + * + * This function is called when internal #GstStream gets updated. + * Meant for internal usage only. Used for subclasses to update + * their properties accordingly. + * + * Note that this vfunc is called from different threads. + */ + void (* internal_stream_updated) (ClapperStream *stream, GstCaps *caps, GstTagList *tags); + + /*< private >*/ + gpointer padding[4]; +}; + +ClapperStreamType clapper_stream_get_stream_type (ClapperStream *stream); + +gchar * clapper_stream_get_title (ClapperStream *stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-subtitle-stream-private.h b/src/lib/clapper/clapper-subtitle-stream-private.h new file mode 100644 index 00000000..b235612f --- /dev/null +++ b/src/lib/clapper/clapper-subtitle-stream-private.h @@ -0,0 +1,32 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-subtitle-stream.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperStream * clapper_subtitle_stream_new (GstStream *gst_stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-subtitle-stream.c b/src/lib/clapper/clapper-subtitle-stream.c new file mode 100644 index 00000000..65b114ac --- /dev/null +++ b/src/lib/clapper/clapper-subtitle-stream.c @@ -0,0 +1,214 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperSubtitleStream: + * + * Represents a subtitle stream within media. + */ + +#include + +#include "clapper-subtitle-stream-private.h" +#include "clapper-stream-private.h" + +#define GST_CAT_DEFAULT clapper_subtitle_stream_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperSubtitleStream +{ + ClapperStream parent; + + gchar *lang_code; + gchar *lang_name; +}; + +#define parent_class clapper_subtitle_stream_parent_class +G_DEFINE_TYPE (ClapperSubtitleStream, clapper_subtitle_stream, CLAPPER_TYPE_STREAM); + +enum +{ + PROP_0, + PROP_LANG_CODE, + PROP_LANG_NAME, + PROP_LAST +}; + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void +_update_using_tags (ClapperSubtitleStream *self, GstTagList *tags) +{ + ClapperStream *stream = CLAPPER_STREAM_CAST (self); + gchar *lang_code = NULL, *lang_name = NULL; + + /* Prefer code (and name from it), fallback to extracted name */ + if (!gst_tag_list_get_string_index (tags, GST_TAG_LANGUAGE_CODE, 0, &lang_code)) + gst_tag_list_get_string_index (tags, GST_TAG_LANGUAGE_NAME, 0, &lang_name); + + clapper_stream_take_string_prop (stream, param_specs[PROP_LANG_CODE], &self->lang_code, lang_code); + clapper_stream_take_string_prop (stream, param_specs[PROP_LANG_NAME], &self->lang_name, lang_name); +} + +ClapperStream * +clapper_subtitle_stream_new (GstStream *gst_stream) +{ + ClapperSubtitleStream *subtitle_stream; + + subtitle_stream = g_object_new (CLAPPER_TYPE_SUBTITLE_STREAM, + "stream-type", CLAPPER_STREAM_TYPE_SUBTITLE, NULL); + gst_object_ref_sink (subtitle_stream); + + clapper_stream_set_gst_stream (CLAPPER_STREAM_CAST (subtitle_stream), gst_stream); + + return CLAPPER_STREAM_CAST (subtitle_stream); +} + +/** + * clapper_subtitle_stream_get_lang_code: + * @stream: a #ClapperSubtitleStream + * + * Get an ISO-639 language code of the @stream. + * + * Returns: (transfer full) (nullable): the language code of subtitle stream. + */ +gchar * +clapper_subtitle_stream_get_lang_code (ClapperSubtitleStream *self) +{ + gchar *lang_code; + + g_return_val_if_fail (CLAPPER_IS_SUBTITLE_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + lang_code = g_strdup (self->lang_code); + GST_OBJECT_UNLOCK (self); + + return lang_code; +} + +/** + * clapper_subtitle_stream_get_lang_name: + * @stream: a #ClapperSubtitleStream + * + * Get an ISO-639 language code of the @stream. + * + * Returns: (transfer full) (nullable): the language code of subtitle stream. + */ +gchar * +clapper_subtitle_stream_get_lang_name (ClapperSubtitleStream *self) +{ + gchar *lang_name = NULL; + + g_return_val_if_fail (CLAPPER_IS_SUBTITLE_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + + /* Prefer from code as its translated to user locale, + * otherwise try to fallback to the one sent in tags */ + if (self->lang_code) + lang_name = g_strdup (gst_tag_get_language_name (self->lang_code)); + if (!lang_name) + lang_name = g_strdup (self->lang_name); + + GST_OBJECT_UNLOCK (self); + + return lang_name; +} + +static void +clapper_subtitle_stream_init (ClapperSubtitleStream *self) +{ +} + +static void +clapper_subtitle_stream_finalize (GObject *object) +{ + ClapperSubtitleStream *self = CLAPPER_SUBTITLE_STREAM_CAST (object); + + g_free (self->lang_code); + g_free (self->lang_name); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_subtitle_stream_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperSubtitleStream *self = CLAPPER_SUBTITLE_STREAM_CAST (object); + + switch (prop_id) { + case PROP_LANG_CODE: + g_value_take_string (value, clapper_subtitle_stream_get_lang_code (self)); + break; + case PROP_LANG_NAME: + g_value_take_string (value, clapper_subtitle_stream_get_lang_name (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_subtitle_stream_internal_stream_updated (ClapperStream *stream, + GstCaps *caps, GstTagList *tags) +{ + ClapperSubtitleStream *self = CLAPPER_SUBTITLE_STREAM_CAST (stream); + + CLAPPER_STREAM_CLASS (parent_class)->internal_stream_updated (stream, caps, tags); + + if (tags) + _update_using_tags (self, tags); +} + +static void +clapper_subtitle_stream_class_init (ClapperSubtitleStreamClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperStreamClass *stream_class = (ClapperStreamClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappersubtitlestream", 0, + "Clapper Subtitle Stream"); + + gobject_class->get_property = clapper_subtitle_stream_get_property; + gobject_class->finalize = clapper_subtitle_stream_finalize; + + stream_class->internal_stream_updated = clapper_subtitle_stream_internal_stream_updated; + + /** + * ClapperSubtitleStream:lang-code: + * + * Stream language code in ISO-639 format. + */ + param_specs[PROP_LANG_CODE] = g_param_spec_string ("lang-code", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperSubtitleStream:lang-name: + * + * Stream full language name determined from lang code. + */ + param_specs[PROP_LANG_NAME] = g_param_spec_string ("lang-name", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-subtitle-stream.h b/src/lib/clapper/clapper-subtitle-stream.h new file mode 100644 index 00000000..ee3c7520 --- /dev/null +++ b/src/lib/clapper/clapper-subtitle-stream.h @@ -0,0 +1,41 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_SUBTITLE_STREAM (clapper_subtitle_stream_get_type()) +#define CLAPPER_SUBTITLE_STREAM_CAST(obj) ((ClapperSubtitleStream *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperSubtitleStream, clapper_subtitle_stream, CLAPPER, SUBTITLE_STREAM, ClapperStream) + +gchar * clapper_subtitle_stream_get_lang_code (ClapperSubtitleStream *stream); + +gchar * clapper_subtitle_stream_get_lang_name (ClapperSubtitleStream *stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-threaded-object.c b/src/lib/clapper/clapper-threaded-object.c new file mode 100644 index 00000000..9ca7ce8b --- /dev/null +++ b/src/lib/clapper/clapper-threaded-object.c @@ -0,0 +1,193 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperThreadedObject: + * + * A base class for creating objects that work within a separate thread. + */ + +#include "clapper-threaded-object.h" + +#define GST_CAT_DEFAULT clapper_threaded_object_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +typedef struct _ClapperThreadedObjectPrivate ClapperThreadedObjectPrivate; + +struct _ClapperThreadedObjectPrivate +{ + GMutex lock; // Separate mutex to not deadlock with subclass on wakeups + GCond cond; + GThread *thread; + GMainContext *context; + GMainLoop *loop; + gboolean started; +}; + +#define parent_class clapper_threaded_object_parent_class +G_DEFINE_TYPE_WITH_PRIVATE (ClapperThreadedObject, clapper_threaded_object, GST_TYPE_OBJECT) + +/** + * clapper_threaded_object_get_context: + * @threaded_object: a #ClapperThreadedObject + * + * Get the #GMainContext of the thread used by this object. + * + * Useful when you want to invoke object thread to do some + * action in it from a different thread. + * + * Returns: a #GMainContext of the object used thread. + */ +GMainContext * +clapper_threaded_object_get_context (ClapperThreadedObject *self) +{ + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + + return priv->context; +} + +static gboolean +main_loop_running_cb (ClapperThreadedObject *self) +{ + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + + GST_TRACE_OBJECT (self, "Main loop running now"); + + g_mutex_lock (&priv->lock); + priv->started = TRUE; + g_cond_signal (&priv->cond); + g_mutex_unlock (&priv->lock); + + return G_SOURCE_REMOVE; +} + +static gpointer +clapper_threaded_object_main (ClapperThreadedObject *self) +{ + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + ClapperThreadedObjectClass *threaded_object_class = CLAPPER_THREADED_OBJECT_GET_CLASS (self); + const gchar *obj_cls_name = G_OBJECT_CLASS_NAME (threaded_object_class); + GSource *idle_source; + + GST_TRACE_OBJECT (self, "%s thread: %p", obj_cls_name, g_thread_self ()); + + priv->context = g_main_context_new (); + priv->loop = g_main_loop_new (priv->context, FALSE); + + g_main_context_push_thread_default (priv->context); + + if (threaded_object_class->thread_start) + threaded_object_class->thread_start (self); + + idle_source = g_idle_source_new (); + g_source_set_callback (idle_source, + (GSourceFunc) main_loop_running_cb, self, NULL); + g_source_attach (idle_source, priv->context); + g_source_unref (idle_source); + + GST_DEBUG_OBJECT (self, "%s main loop running", obj_cls_name); + g_main_loop_run (priv->loop); + GST_DEBUG_OBJECT (self, "%s main loop stopped", obj_cls_name); + + if (threaded_object_class->thread_stop) + threaded_object_class->thread_stop (self); + + g_main_context_pop_thread_default (priv->context); + g_main_context_unref (priv->context); + + return NULL; +} + +static void +clapper_threaded_object_init (ClapperThreadedObject *self) +{ + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + + g_mutex_init (&priv->lock); + g_cond_init (&priv->cond); +} + +static void +clapper_threaded_object_constructed (GObject *object) +{ + ClapperThreadedObject *self = CLAPPER_THREADED_OBJECT_CAST (object); + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + + GST_TRACE_OBJECT (self, "Constructed from thread: %p", g_thread_self ()); + + g_mutex_lock (&priv->lock); + + priv->thread = g_thread_new (GST_OBJECT_NAME (object), + (GThreadFunc) clapper_threaded_object_main, self); + while (!priv->started) + g_cond_wait (&priv->cond, &priv->lock); + + g_mutex_unlock (&priv->lock); + + G_OBJECT_CLASS (parent_class)->constructed (object); +} + +static void +clapper_threaded_object_dispose (GObject *object) +{ + ClapperThreadedObject *self = CLAPPER_THREADED_OBJECT_CAST (object); + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + + g_mutex_lock (&priv->lock); + + if (priv->loop) { + g_main_loop_quit (priv->loop); + + if (G_LIKELY (priv->thread != g_thread_self ())) + g_thread_join (priv->thread); + else + g_thread_unref (priv->thread); + + g_clear_pointer (&priv->loop, g_main_loop_unref); + } + + g_mutex_unlock (&priv->lock); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +clapper_threaded_object_finalize (GObject *object) +{ + ClapperThreadedObject *self = CLAPPER_THREADED_OBJECT_CAST (object); + ClapperThreadedObjectPrivate *priv = clapper_threaded_object_get_instance_private (self); + + g_mutex_clear (&priv->lock); + g_cond_clear (&priv->cond); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_threaded_object_class_init (ClapperThreadedObjectClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperthreadedobject", 0, + "Clapper Threaded Object"); + + gobject_class->constructed = clapper_threaded_object_constructed; + gobject_class->dispose = clapper_threaded_object_dispose; + gobject_class->finalize = clapper_threaded_object_finalize; +} diff --git a/src/lib/clapper/clapper-threaded-object.h b/src/lib/clapper/clapper-threaded-object.h new file mode 100644 index 00000000..3b1ee8a1 --- /dev/null +++ b/src/lib/clapper/clapper-threaded-object.h @@ -0,0 +1,73 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_THREADED_OBJECT (clapper_threaded_object_get_type()) +#define CLAPPER_THREADED_OBJECT_CAST(obj) ((ClapperThreadedObject *)(obj)) + +G_DECLARE_DERIVABLE_TYPE (ClapperThreadedObject, clapper_threaded_object, CLAPPER, THREADED_OBJECT, GstObject) + +/** + * ClapperThreadedObjectClass: + * @parent_class: The object class structure. + * @thread_start: Called right after thread started. + * @thread_stop: Called when thread is going to stop. + */ +struct _ClapperThreadedObjectClass +{ + GstObjectClass parent_class; + + /** + * ClapperThreadedObjectClass::thread_start: + * @threaded_object: a #ClapperThreadedObject + * + * Called right after thread started. + * + * Useful for initializing objects that work within this new thread. + */ + void (* thread_start) (ClapperThreadedObject *threaded_object); + + /** + * ClapperThreadedObjectClass::thread_stop: + * @threaded_object: a #ClapperThreadedObject + * + * Called when thread is going to stop. + * + * Useful for cleanup of things created on thread start. + */ + void (* thread_stop) (ClapperThreadedObject *threaded_object); + + /*< private >*/ + gpointer padding[4]; +}; + +GMainContext * clapper_threaded_object_get_context (ClapperThreadedObject *threaded_object); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-timeline-private.h b/src/lib/clapper/clapper-timeline-private.h new file mode 100644 index 00000000..ceaaa2ad --- /dev/null +++ b/src/lib/clapper/clapper-timeline-private.h @@ -0,0 +1,38 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-timeline.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperTimeline * clapper_timeline_new (void); + +G_GNUC_INTERNAL +gboolean clapper_timeline_set_toc (ClapperTimeline *timeline, GstToc *toc, gboolean updated); + +G_GNUC_INTERNAL +void clapper_timeline_refresh (ClapperTimeline *timeline); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-timeline.c b/src/lib/clapper/clapper-timeline.c new file mode 100644 index 00000000..7bd911a7 --- /dev/null +++ b/src/lib/clapper/clapper-timeline.c @@ -0,0 +1,559 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperTimeline: + * + * A media timeline filled with point markers. + */ + +#include + +#include "clapper-enums.h" +#include "clapper-timeline-private.h" +#include "clapper-marker-private.h" +#include "clapper-player-private.h" + +#define GST_CAT_DEFAULT clapper_timeline_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperTimeline +{ + GstObject parent; + + GSequence *markers_seq; + + GstToc *toc; + GPtrArray *pending_markers; + gboolean needs_refresh; +}; + +enum +{ + PROP_0, + PROP_N_MARKERS, + PROP_LAST +}; + +static GType +clapper_timeline_list_model_get_item_type (GListModel *model) +{ + return CLAPPER_TYPE_MARKER; +} + +static guint +clapper_timeline_list_model_get_n_items (GListModel *model) +{ + ClapperTimeline *self = CLAPPER_TIMELINE_CAST (model); + guint n_markers; + + GST_OBJECT_LOCK (self); + n_markers = g_sequence_get_length (self->markers_seq); + GST_OBJECT_UNLOCK (self); + + return n_markers; +} + +static gpointer +clapper_timeline_list_model_get_item (GListModel *model, guint index) +{ + ClapperTimeline *self = CLAPPER_TIMELINE_CAST (model); + GSequenceIter *iter; + ClapperMarker *marker = NULL; + + GST_OBJECT_LOCK (self); + iter = g_sequence_get_iter_at_pos (self->markers_seq, index); + if (!g_sequence_iter_is_end (iter)) + marker = gst_object_ref (g_sequence_get (iter)); + GST_OBJECT_UNLOCK (self); + + return marker; +} + +static void +clapper_timeline_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = clapper_timeline_list_model_get_item_type; + iface->get_n_items = clapper_timeline_list_model_get_n_items; + iface->get_item = clapper_timeline_list_model_get_item; +} + +#define parent_class clapper_timeline_parent_class +G_DEFINE_TYPE_WITH_CODE (ClapperTimeline, clapper_timeline, GST_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, clapper_timeline_list_model_iface_init)); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void +clapper_timeline_post_item_updated (ClapperTimeline *self) +{ + ClapperPlayer *player; + + if ((player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)))) { + ClapperFeaturesManager *features_manager; + + if ((features_manager = clapper_player_get_features_manager (player))) { + ClapperMediaItem *item; + + if ((item = CLAPPER_MEDIA_ITEM (gst_object_get_parent (GST_OBJECT_CAST (self))))) { + clapper_features_manager_trigger_item_updated (features_manager, item); + gst_object_unref (item); + } + } + + gst_object_unref (player); + } +} + +static gint +_markers_compare_func (gconstpointer marker_a, gconstpointer marker_b, + gpointer user_data G_GNUC_UNUSED) +{ + gint64 val_a, val_b, result; + + /* Can happen if someone tries to insert already + * inserted marker pointer */ + if (marker_a == marker_b) + return 0; + + /* 1 millisecond accuracy should be enough */ + val_a = clapper_marker_get_start (CLAPPER_MARKER_CAST (marker_a)) * 1000; + val_b = clapper_marker_get_start (CLAPPER_MARKER_CAST (marker_b)) * 1000; + + /* If start time is the same, sort by earliest end time */ + if (val_a == val_b) { + val_a = clapper_marker_get_end (CLAPPER_MARKER_CAST (marker_a)) * 1000; + val_b = clapper_marker_get_end (CLAPPER_MARKER_CAST (marker_b)) * 1000; + + /* If both times are the same, check type and if they also are + * the same, we will assume that this is the same marker overall */ + if (val_a == val_b) { + val_a = clapper_marker_get_marker_type (CLAPPER_MARKER_CAST (marker_a)); + val_b = clapper_marker_get_marker_type (CLAPPER_MARKER_CAST (marker_b)); + } + } + + result = val_a - val_b; + + return (result > 0) ? 1 : (result < 0) ? -1 : 0; +} + +/* + * clapper_timeline_new: + * + * Returns: (transfer full): a new #ClapperTimeline instance + */ +ClapperTimeline * +clapper_timeline_new (void) +{ + ClapperTimeline *timeline; + + timeline = g_object_new (CLAPPER_TYPE_TIMELINE, NULL); + gst_object_ref_sink (timeline); + + return timeline; +} + +static inline gint +_take_marker_unlocked (ClapperTimeline *self, ClapperMarker *marker) +{ + GSequenceIter *iter; + + iter = g_sequence_insert_sorted (self->markers_seq, marker, + (GCompareDataFunc) _markers_compare_func, NULL); + gst_object_set_parent (GST_OBJECT_CAST (marker), GST_OBJECT_CAST (self)); + + return g_sequence_iter_get_position (iter); +} + +/** + * clapper_timeline_insert_marker: + * @timeline: a #ClapperTimeline + * @marker: a #ClapperMarker + * + * Insert the #ClapperMarker into @timeline. + * + * Returns: %TRUE if inserted, %FALSE if marker was + * already inserted into timeline. + */ +gboolean +clapper_timeline_insert_marker (ClapperTimeline *self, ClapperMarker *marker) +{ + gboolean success; + gint position = 0; + + g_return_val_if_fail (CLAPPER_IS_TIMELINE (self), FALSE); + g_return_val_if_fail (CLAPPER_IS_MARKER (marker), FALSE); + + GST_OBJECT_LOCK (self); + + if ((success = !g_sequence_lookup (self->markers_seq, marker, + (GCompareDataFunc) _markers_compare_func, NULL))) + position = _take_marker_unlocked (self, gst_object_ref (marker)); + + GST_OBJECT_UNLOCK (self); + + if (success) { + g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1); + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_N_MARKERS]); + + clapper_timeline_post_item_updated (self); + } + + return success; +} + +/** + * clapper_timeline_remove_marker: + * @timeline: a #ClapperTimeline + * @marker: a #ClapperMarker + * + * Removes #ClapperMarker from the timeline. + * + * If marker was not in the @timeline, this function will do nothing, + * so it is safe to call if unsure. + */ +void +clapper_timeline_remove_marker (ClapperTimeline *self, ClapperMarker *marker) +{ + GSequenceIter *iter; + gint position = 0; + gboolean success = FALSE; + + g_return_if_fail (CLAPPER_IS_TIMELINE (self)); + g_return_if_fail (CLAPPER_IS_MARKER (marker)); + + GST_OBJECT_LOCK (self); + + if ((iter = g_sequence_lookup (self->markers_seq, marker, + (GCompareDataFunc) _markers_compare_func, NULL))) { + position = g_sequence_iter_get_position (iter); + g_sequence_remove (iter); + + success = TRUE; + } + + GST_OBJECT_UNLOCK (self); + + if (success) { + g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0); + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_N_MARKERS]); + + clapper_timeline_post_item_updated (self); + } +} + +/** + * clapper_timeline_get_marker: + * @timeline: a #ClapperTimeline + * @index: a marker index + * + * Get the #ClapperMarker at index. + * + * This behaves the same as [method@Gio.ListModel.get_item], and is here + * for code uniformity and convenience to avoid type casting by user. + * + * Returns: (transfer full) (nullable): The #ClapperMarker at @index. + */ +ClapperMarker * +clapper_timeline_get_marker (ClapperTimeline *self, guint index) +{ + g_return_val_if_fail (CLAPPER_IS_TIMELINE (self), NULL); + + return g_list_model_get_item (G_LIST_MODEL (self), index); +} + +/** + * clapper_timeline_get_n_markers: + * @timeline: a #ClapperTimeline + * + * Get the number of markers in #ClapperTimeline. + * + * This behaves the same as [method@Gio.ListModel.get_n_items], and is here + * for code uniformity and convenience to avoid type casting by user. + * + * Returns: The number of markers in #ClapperTimeline. + */ +guint +clapper_timeline_get_n_markers (ClapperTimeline *self) +{ + g_return_val_if_fail (CLAPPER_IS_TIMELINE (self), 0); + + return g_list_model_get_n_items (G_LIST_MODEL (self)); +} + +static void +_append_marker_from_toc_entry (ClapperTimeline *self, GstTocEntry *entry, GList **markers) +{ + ClapperMarker *marker; + ClapperMarkerType marker_type; + GstTagList *tags; + gchar *title = NULL; + gint64 start = 0, stop = 0; + gdouble marker_start = 0, marker_end = CLAPPER_MARKER_NO_END; + + switch (gst_toc_entry_get_entry_type (entry)) { + case GST_TOC_ENTRY_TYPE_TITLE: + marker_type = CLAPPER_MARKER_TYPE_TITLE; + break; + case GST_TOC_ENTRY_TYPE_TRACK: + marker_type = CLAPPER_MARKER_TYPE_TRACK; + break; + case GST_TOC_ENTRY_TYPE_CHAPTER: + marker_type = CLAPPER_MARKER_TYPE_CHAPTER; + break; + default: + return; + } + + /* Start time is required */ + if (G_UNLIKELY (!gst_toc_entry_get_start_stop_times (entry, &start, NULL))) + return; + + marker_start = (gdouble) start / GST_SECOND; + if (gst_toc_entry_get_start_stop_times (entry, NULL, &stop)) + marker_end = (gdouble) stop / GST_SECOND; + + if ((tags = gst_toc_entry_get_tags (entry))) + gst_tag_list_get_string_index (tags, GST_TAG_TITLE, 0, &title); + + marker = clapper_marker_new_internal (marker_type, + title, marker_start, marker_end); + *markers = g_list_append (*markers, marker); + + g_free (title); +} + +static void +_iterate_toc_entries (ClapperTimeline *self, GList *entries, GList **markers) +{ + GList *en; + + for (en = entries; en != NULL; en = en->next) { + GstTocEntry *entry = (GstTocEntry *) en->data; + + if (gst_toc_entry_is_alternative (entry)) + _iterate_toc_entries (self, gst_toc_entry_get_sub_entries (entry), markers); + else if (gst_toc_entry_is_sequence (entry)) + _append_marker_from_toc_entry (self, entry, markers); + } +} + +static inline void +_prepare_markers (ClapperTimeline *self, GstToc *toc) +{ + GList *entries = gst_toc_get_entries (toc); + GList *ma, *markers = NULL; + + GST_DEBUG_OBJECT (self, "Preparing markers from TOC: %" GST_PTR_FORMAT, toc); + _iterate_toc_entries (self, entries, &markers); + + GST_OBJECT_LOCK (self); + + g_ptr_array_remove_range (self->pending_markers, 0, self->pending_markers->len); + for (ma = markers; ma != NULL; ma = ma->next) + g_ptr_array_add (self->pending_markers, CLAPPER_MARKER_CAST (ma->data)); + + self->needs_refresh = TRUE; + + GST_OBJECT_UNLOCK (self); + + if (markers) + g_list_free (markers); +} + +gboolean +clapper_timeline_set_toc (ClapperTimeline *self, GstToc *toc, gboolean updated) +{ + gboolean changed; + + if (gst_toc_get_scope (toc) != GST_TOC_SCOPE_GLOBAL) + return FALSE; + + GST_OBJECT_LOCK (self); + + if (self->toc == toc) { + changed = updated; + } else { + /* FIXME: Iterate and compare entries and their amount + * one by one, so we can avoid update between discovery and playback + * (and also when playing the same media item again) */ + changed = TRUE; + } + + if (changed) { + if (self->toc) + gst_toc_unref (self->toc); + + self->toc = gst_toc_ref (toc); + } + GST_OBJECT_UNLOCK (self); + + if (changed) + _prepare_markers (self, toc); + + return changed; +} + +/* Must be called from main thread */ +void +clapper_timeline_refresh (ClapperTimeline *self) +{ + GSequenceIter *iter; + GList *rec, *rec_markers = NULL; + gpointer *stolen_markers; + gsize n_pending = 0; + guint i, n_before, n_after; + + GST_OBJECT_LOCK (self); + + /* This prevents us from incorrect behaviour when there were multiple + * TOC objects set in a row before we reached main thread handling + * for them here and refresh will be now invoked in a row, possibly + * erasing markers on its second run */ + if (!self->needs_refresh) { + GST_OBJECT_UNLOCK (self); + return; + } + + GST_DEBUG_OBJECT (self, "Timeline refresh"); + + n_before = g_sequence_get_length (self->markers_seq); + + /* Recover markers that should remain */ + iter = g_sequence_get_begin_iter (self->markers_seq); + while (!g_sequence_iter_is_end (iter)) { + ClapperMarker *marker = CLAPPER_MARKER_CAST (g_sequence_get (iter)); + + if (!clapper_marker_is_internal (marker)) + rec_markers = g_list_append (rec_markers, gst_object_ref (marker)); + + iter = g_sequence_iter_next (iter); + } + + /* Clear sequence */ + g_sequence_remove_range ( + g_sequence_get_begin_iter (self->markers_seq), + g_sequence_get_end_iter (self->markers_seq)); + + /* Transfer pending markers into sequence */ + stolen_markers = g_ptr_array_steal (self->pending_markers, &n_pending); + for (i = 0; i < n_pending; ++i) { + g_sequence_append (self->markers_seq, CLAPPER_MARKER_CAST (stolen_markers[i])); + gst_object_set_parent (GST_OBJECT_CAST (stolen_markers[i]), GST_OBJECT_CAST (self)); + } + g_free (stolen_markers); + + /* Transfer recovered markers back into sequence */ + for (rec = rec_markers; rec != NULL; rec = rec->next) { + ClapperMarker *marker = CLAPPER_MARKER_CAST (rec->data); + + g_sequence_append (self->markers_seq, marker); + gst_object_set_parent (GST_OBJECT_CAST (marker), GST_OBJECT_CAST (self)); + } + if (rec_markers) + g_list_free (rec_markers); + + /* Sort once after all appends (this way is faster according to documentation) */ + g_sequence_sort (self->markers_seq, _markers_compare_func, NULL); + + n_after = g_sequence_get_length (self->markers_seq); + self->needs_refresh = FALSE; + + GST_OBJECT_UNLOCK (self); + + GST_DEBUG_OBJECT (self, "Timeline refreshed, n_before: %u, n_after: %u", + n_before, n_after); + + g_list_model_items_changed (G_LIST_MODEL (self), 0, n_before, n_after); + if (n_before != n_after) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_N_MARKERS]); + + clapper_timeline_post_item_updated (self); +} + +static void +_marker_remove_func (ClapperMarker *marker) +{ + gst_object_unparent (GST_OBJECT_CAST (marker)); + gst_object_unref (marker); +} + +static void +clapper_timeline_init (ClapperTimeline *self) +{ + self->markers_seq = g_sequence_new ((GDestroyNotify) _marker_remove_func); + self->pending_markers = g_ptr_array_new_with_free_func ((GDestroyNotify) gst_object_unref); +} + +static void +clapper_timeline_finalize (GObject *object) +{ + ClapperTimeline *self = CLAPPER_TIMELINE_CAST (object); + + GST_TRACE_OBJECT (self, "Finalize"); + + g_sequence_free (self->markers_seq); + + if (self->toc) + gst_toc_unref (self->toc); + + g_ptr_array_unref (self->pending_markers); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_timeline_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperTimeline *self = CLAPPER_TIMELINE_CAST (object); + + switch (prop_id) { + case PROP_N_MARKERS: + g_value_set_uint (value, clapper_timeline_get_n_markers (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_timeline_class_init (ClapperTimelineClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappertimeline", 0, + "Clapper Timeline"); + + gobject_class->get_property = clapper_timeline_get_property; + gobject_class->finalize = clapper_timeline_finalize; + + /** + * ClapperTimeline:n-markers: + * + * Number of markers in the timeline. + */ + param_specs[PROP_N_MARKERS] = g_param_spec_uint ("n-markers", + NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-timeline.h b/src/lib/clapper/clapper-timeline.h new file mode 100644 index 00000000..1906d531 --- /dev/null +++ b/src/lib/clapper/clapper-timeline.h @@ -0,0 +1,47 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_TIMELINE (clapper_timeline_get_type()) +#define CLAPPER_TIMELINE_CAST(obj) ((ClapperTimeline *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperTimeline, clapper_timeline, CLAPPER, TIMELINE, GstObject) + +gboolean clapper_timeline_insert_marker (ClapperTimeline *timeline, ClapperMarker *marker); + +void clapper_timeline_remove_marker (ClapperTimeline *timeline, ClapperMarker *marker); + +ClapperMarker * clapper_timeline_get_marker (ClapperTimeline *timeline, guint index); + +guint clapper_timeline_get_n_markers (ClapperTimeline *timeline); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-utils-private.h b/src/lib/clapper/clapper-utils-private.h new file mode 100644 index 00000000..3f990056 --- /dev/null +++ b/src/lib/clapper/clapper-utils-private.h @@ -0,0 +1,54 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "clapper-utils.h" +#include "clapper-queue.h" +#include "clapper-media-item.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +void clapper_utils_initialize (void); + +G_GNUC_INTERNAL +void clapper_utils_queue_append_on_main_sync (ClapperQueue *queue, ClapperMediaItem *item); + +G_GNUC_INTERNAL +void clapper_utils_queue_insert_on_main_sync (ClapperQueue *queue, ClapperMediaItem *item, ClapperMediaItem *after_item); + +G_GNUC_INTERNAL +void clapper_utils_queue_remove_on_main_sync (ClapperQueue *queue, ClapperMediaItem *item); + +G_GNUC_INTERNAL +void clapper_utils_queue_clear_on_main_sync (ClapperQueue *queue); + +G_GNUC_INTERNAL +gchar * clapper_utils_uri_from_file (GFile *file); + +G_GNUC_INTERNAL +gchar * clapper_utils_title_from_uri (const gchar *uri); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-utils.c b/src/lib/clapper/clapper-utils.c new file mode 100644 index 00000000..6df1d511 --- /dev/null +++ b/src/lib/clapper/clapper-utils.c @@ -0,0 +1,216 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "clapper-utils-private.h" +#include "../shared/clapper-shared-utils-private.h" + +#define GST_CAT_DEFAULT clapper_utils_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +typedef enum +{ + CLAPPER_UTILS_QUEUE_ALTER_APPEND = 1, + CLAPPER_UTILS_QUEUE_ALTER_INSERT, + CLAPPER_UTILS_QUEUE_ALTER_REMOVE, + CLAPPER_UTILS_QUEUE_ALTER_CLEAR +} ClapperUtilsQueueAlterMethod; + +typedef struct +{ + ClapperQueue *queue; + ClapperMediaItem *item; + ClapperMediaItem *after_item; + ClapperUtilsQueueAlterMethod method; +} ClapperUtilsQueueAlterData; + +void +clapper_utils_initialize (void) +{ + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperutils", 0, + "Clapper Utilities"); +} + +static ClapperUtilsQueueAlterData * +clapper_utils_queue_alter_data_new (ClapperQueue *queue, + ClapperMediaItem *item, ClapperMediaItem *after_item, + ClapperUtilsQueueAlterMethod method) +{ + ClapperUtilsQueueAlterData *data = g_new (ClapperUtilsQueueAlterData, 1); + + data->queue = queue; + data->item = item; + data->after_item = after_item; + data->method = method; + + GST_TRACE ("Created queue alter data: %p", data); + + return data; +} + +static void +clapper_utils_queue_alter_data_free (ClapperUtilsQueueAlterData *data) +{ + GST_TRACE ("Freeing queue alter data: %p", data); + + g_free (data); +} + +static gpointer +clapper_utils_queue_alter_on_main (ClapperUtilsQueueAlterData *data) +{ + GST_DEBUG ("Queue alter invoked"); + + switch (data->method) { + case CLAPPER_UTILS_QUEUE_ALTER_APPEND: + clapper_queue_add_item (data->queue, data->item); + break; + case CLAPPER_UTILS_QUEUE_ALTER_INSERT:{ + guint index; + + /* If we have "after_item" then we need to insert after it, otherwise prepend */ + if (data->after_item) { + if (clapper_queue_find_item (data->queue, data->after_item, &index)) + index++; + else // If not found, just append at the end + index = -1; + } else { + index = 0; + } + + clapper_queue_insert_item (data->queue, data->item, index); + break; + } + case CLAPPER_UTILS_QUEUE_ALTER_REMOVE: + clapper_queue_remove_item (data->queue, data->item); + break; + case CLAPPER_UTILS_QUEUE_ALTER_CLEAR: + clapper_queue_clear (data->queue); + break; + default: + g_assert_not_reached (); + break; + } + + return NULL; +} + +static inline void +clapper_utils_queue_alter_invoke_on_main_sync_take (ClapperUtilsQueueAlterData *data) +{ + GST_DEBUG ("Invoking queue alter on main..."); + + clapper_shared_utils_context_invoke_sync_full (g_main_context_default (), + (GThreadFunc) clapper_utils_queue_alter_on_main, data, + (GDestroyNotify) clapper_utils_queue_alter_data_free); + + GST_DEBUG ("Queue alter invoke finished"); +} + +void +clapper_utils_queue_append_on_main_sync (ClapperQueue *queue, ClapperMediaItem *item) +{ + ClapperUtilsQueueAlterData *data = clapper_utils_queue_alter_data_new (queue, + item, NULL, CLAPPER_UTILS_QUEUE_ALTER_APPEND); + clapper_utils_queue_alter_invoke_on_main_sync_take (data); +} + +void +clapper_utils_queue_insert_on_main_sync (ClapperQueue *queue, + ClapperMediaItem *item, ClapperMediaItem *after_item) +{ + ClapperUtilsQueueAlterData *data = clapper_utils_queue_alter_data_new (queue, + item, after_item, CLAPPER_UTILS_QUEUE_ALTER_INSERT); + clapper_utils_queue_alter_invoke_on_main_sync_take (data); +} + +void +clapper_utils_queue_remove_on_main_sync (ClapperQueue *queue, ClapperMediaItem *item) +{ + ClapperUtilsQueueAlterData *data = clapper_utils_queue_alter_data_new (queue, + item, NULL, CLAPPER_UTILS_QUEUE_ALTER_REMOVE); + clapper_utils_queue_alter_invoke_on_main_sync_take (data); +} + +void +clapper_utils_queue_clear_on_main_sync (ClapperQueue *queue) +{ + ClapperUtilsQueueAlterData *data = clapper_utils_queue_alter_data_new (queue, + NULL, NULL, CLAPPER_UTILS_QUEUE_ALTER_CLEAR); + clapper_utils_queue_alter_invoke_on_main_sync_take (data); +} + +gchar * +clapper_utils_uri_from_file (GFile *file) +{ + gchar *uri = g_file_get_uri (file); + gsize length = strlen (uri); + + /* GFile might incorrectly append "/" at the end of an URI, + * remove it to make it work with GStreamer URI handling */ + if (uri[length - 1] == '/') { + gchar *fixed_uri; + + /* NULL terminated copy without last character */ + fixed_uri = g_new0 (gchar, length); + memcpy (fixed_uri, uri, length - 1); + + g_free (uri); + uri = fixed_uri; + } + + return uri; +} + +gchar * +clapper_utils_title_from_uri (const gchar *uri) +{ + gchar *proto, *title = NULL; + + proto = gst_uri_get_protocol (uri); + + if (G_UNLIKELY (proto == NULL)) + return NULL; + + if (strcmp (proto, "file") == 0) { + gchar *filename = g_filename_from_uri (uri, NULL, NULL); + + if (filename) { + const gchar *ext; + + title = g_path_get_basename (filename); + ext = strrchr (title, '.'); + + g_free (filename); + + if (ext && strlen (ext) <= 4) { + gchar *tmp = g_strndup (title, strlen (title) - strlen (ext)); + + g_free (title); + title = tmp; + } + } + } else if (strcmp (proto, "dvb") == 0) { + const gchar *channel = strrchr (uri, '/') + 1; + title = g_strdup (channel); + } + + g_free (proto); + + return title; +} diff --git a/src/lib/clapper/clapper-utils.h b/src/lib/clapper/clapper-utils.h new file mode 100644 index 00000000..669b8759 --- /dev/null +++ b/src/lib/clapper/clapper-utils.h @@ -0,0 +1,81 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + +G_BEGIN_DECLS + +/** + * CLAPPER_TIME_FORMAT: (skip): + * + * A string that can be used in printf-like format to display + * e.g. position or duration in `hh:mm:ss` format. Meant to be + * used together with [func@Clapper.TIME_ARGS]. + * + * Example: + * + * ```c + * gchar *str = g_strdup_printf ("%" CLAPPER_TIME_FORMAT, CLAPPER_TIME_ARGS (time)); + * ``` + */ +#define CLAPPER_TIME_FORMAT "02u:%02u:%02u" + +/** + * CLAPPER_TIME_ARGS: (skip): + * @t: time value in seconds + * + * Formats @t for the [const@Clapper.TIME_FORMAT] format string. + */ +#define CLAPPER_TIME_ARGS(t) \ + (guint) (((GstClockTime)(t)) / 3600), \ + (guint) ((((GstClockTime)(t)) / 60) % 60), \ + (guint) (((GstClockTime)(t)) % 60) + +/** + * CLAPPER_TIME_MS_FORMAT: (skip): + * + * Same as [const@Clapper.TIME_FORMAT], but also displays milliseconds. + * Meant to be used together with [func@Clapper.TIME_MS_ARGS]. + * + * Example: + * + * ```c + * gchar *str = g_strdup_printf ("%" CLAPPER_TIME_MS_FORMAT, CLAPPER_TIME_MS_ARGS (time)); + * ``` + */ +#define CLAPPER_TIME_MS_FORMAT "02u:%02u:%02u.%03u" + +/** + * CLAPPER_TIME_MS_ARGS: (skip): + * @t: time value in seconds + * + * Formats @t for the [const@Clapper.TIME_MS_FORMAT] format string. + */ +#define CLAPPER_TIME_MS_ARGS(t) \ + CLAPPER_TIME_ARGS(t), \ + (guint) (((GstClockTime)(t * 1000)) % 1000) + +G_END_DECLS diff --git a/src/lib/clapper/clapper-version.h.in b/src/lib/clapper/clapper-version.h.in new file mode 100644 index 00000000..a6db1f22 --- /dev/null +++ b/src/lib/clapper/clapper-version.h.in @@ -0,0 +1,76 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +/** + * CLAPPER_MAJOR_VERSION: + * + * Clapper major version component + */ +#define CLAPPER_MAJOR_VERSION (@CLAPPER_MAJOR_VERSION@) + +/** + * CLAPPER_MINOR_VERSION: + * + * Clapper minor version component + */ +#define CLAPPER_MINOR_VERSION (@CLAPPER_MINOR_VERSION@) + +/** + * CLAPPER_MICRO_VERSION: + * + * Clapper micro version component + */ +#define CLAPPER_MICRO_VERSION (@CLAPPER_MICRO_VERSION@) + +/** + * CLAPPER_VERSION: + * + * Clapper version + */ +#define CLAPPER_VERSION (@CLAPPER_VERSION@) + +/** + * CLAPPER_VERSION_S: + * + * Clapper version, encoded as a string + */ +#define CLAPPER_VERSION_S "@CLAPPER_VERSION@" + +#define CLAPPER_ENCODE_VERSION(major,minor,micro) \ + ((major) << 24 | (minor) << 16 | (micro) << 8) + +/** + * CLAPPER_VERSION_HEX: + * + * Clapper version, encoded as an hexadecimal number, useful for integer comparisons. + */ +#define CLAPPER_VERSION_HEX \ + (CLAPPER_ENCODE_VERSION (CLAPPER_MAJOR_VERSION, CLAPPER_MINOR_VERSION, CLAPPER_MICRO_VERSION)) + +#define CLAPPER_CHECK_VERSION(major, minor, micro) \ + (CLAPPER_MAJOR_VERSION > (major) || \ + (CLAPPER_MAJOR_VERSION == (major) && CLAPPER_MINOR_VERSION > (minor)) || \ + (CLAPPER_MAJOR_VERSION == (major) && CLAPPER_MINOR_VERSION == (minor) && \ + CLAPPER_MICRO_VERSION >= (micro))) diff --git a/src/lib/clapper/clapper-video-stream-private.h b/src/lib/clapper/clapper-video-stream-private.h new file mode 100644 index 00000000..7c8c928a --- /dev/null +++ b/src/lib/clapper/clapper-video-stream-private.h @@ -0,0 +1,32 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "clapper-video-stream.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +ClapperStream * clapper_video_stream_new (GstStream *gst_stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper-video-stream.c b/src/lib/clapper/clapper-video-stream.c new file mode 100644 index 00000000..fa0a8fed --- /dev/null +++ b/src/lib/clapper/clapper-video-stream.c @@ -0,0 +1,385 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperVideoStream: + * + * Represents a video stream within media. + */ + +#include "clapper-video-stream-private.h" +#include "clapper-stream-private.h" + +#define GST_CAT_DEFAULT clapper_video_stream_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperVideoStream +{ + ClapperStream parent; + + gchar *codec; + gint width; + gint height; + gdouble fps; + guint bitrate; + gchar *pixel_format; +}; + +#define parent_class clapper_video_stream_parent_class +G_DEFINE_TYPE (ClapperVideoStream, clapper_video_stream, CLAPPER_TYPE_STREAM); + +enum +{ + PROP_0, + PROP_CODEC, + PROP_WIDTH, + PROP_HEIGHT, + PROP_FPS, + PROP_BITRATE, + PROP_PIXEL_FORMAT, + PROP_LAST +}; + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static void +_update_using_caps (ClapperVideoStream *self, GstCaps *caps) +{ + ClapperStream *stream = CLAPPER_STREAM_CAST (self); + GstStructure *structure; + gint width = 0, height = 0, fps_n = 0, fps_d = 0; + + if (gst_caps_get_size (caps) == 0) + return; + + structure = gst_caps_get_structure (caps, 0); + + /* NOTE: We cannot use gst_structure_get() here, + * as it stops iterating on first not found key */ + + gst_structure_get_int (structure, "width", &width); + clapper_stream_set_int_prop (stream, param_specs[PROP_WIDTH], &self->width, width); + + gst_structure_get_int (structure, "height", &height); + clapper_stream_set_int_prop (stream, param_specs[PROP_HEIGHT], &self->height, height); + + gst_structure_get_fraction (structure, "framerate", &fps_n, &fps_d); + + if (G_UNLIKELY (fps_d == 0)) + fps_d = 1; + + clapper_stream_set_double_prop (stream, param_specs[PROP_FPS], &self->fps, (gdouble) fps_n / fps_d); + + clapper_stream_set_string_prop (stream, param_specs[PROP_PIXEL_FORMAT], &self->pixel_format, + gst_structure_get_string (structure, "format")); +} + +static void +_update_using_tags (ClapperVideoStream *self, GstTagList *tags) +{ + ClapperStream *stream = CLAPPER_STREAM_CAST (self); + gchar *codec = NULL; + guint bitrate = 0; + + gst_tag_list_get_string_index (tags, GST_TAG_VIDEO_CODEC, 0, &codec); + clapper_stream_take_string_prop (stream, param_specs[PROP_CODEC], &self->codec, codec); + + gst_tag_list_get_uint_index (tags, GST_TAG_BITRATE, 0, &bitrate); + clapper_stream_set_uint_prop (stream, param_specs[PROP_BITRATE], &self->bitrate, bitrate); +} + +ClapperStream * +clapper_video_stream_new (GstStream *gst_stream) +{ + ClapperVideoStream *video_stream; + + video_stream = g_object_new (CLAPPER_TYPE_VIDEO_STREAM, + "stream-type", CLAPPER_STREAM_TYPE_VIDEO, NULL); + gst_object_ref_sink (video_stream); + + clapper_stream_set_gst_stream (CLAPPER_STREAM_CAST (video_stream), gst_stream); + + return CLAPPER_STREAM_CAST (video_stream); +} + +/** + * clapper_video_stream_get_codec: + * @stream: a #ClapperVideoStream + * + * Get codec used to encode @stream. + * + * Returns: (transfer full) (nullable): the video codec of stream + * or %NULL if undetermined. + */ +gchar * +clapper_video_stream_get_codec (ClapperVideoStream *self) +{ + gchar *codec; + + g_return_val_if_fail (CLAPPER_IS_VIDEO_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + codec = g_strdup (self->codec); + GST_OBJECT_UNLOCK (self); + + return codec; +} + +/** + * clapper_video_stream_get_width: + * @stream: a #ClapperVideoStream + * + * Get width of video @stream. + * + * Returns: the width of video stream. + */ +gint +clapper_video_stream_get_width (ClapperVideoStream *self) +{ + gint width; + + g_return_val_if_fail (CLAPPER_IS_VIDEO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + width = self->width; + GST_OBJECT_UNLOCK (self); + + return width; +} + +/** + * clapper_video_stream_get_height: + * @stream: a #ClapperVideoStream + * + * Get height of video @stream. + * + * Returns: the height of video stream. + */ +gint +clapper_video_stream_get_height (ClapperVideoStream *self) +{ + gint height; + + g_return_val_if_fail (CLAPPER_IS_VIDEO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + height = self->height; + GST_OBJECT_UNLOCK (self); + + return height; +} + +/** + * clapper_video_stream_get_fps: + * @stream: a #ClapperVideoStream + * + * Get number of frames per second in video @stream. + * + * Returns: the FPS of video stream. + */ +gdouble +clapper_video_stream_get_fps (ClapperVideoStream *self) +{ + gdouble fps; + + g_return_val_if_fail (CLAPPER_IS_VIDEO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + fps = self->fps; + GST_OBJECT_UNLOCK (self); + + return fps; +} + +/** + * clapper_video_stream_get_bitrate: + * @stream: a #ClapperVideoStream + * + * Get bitrate of video @stream. + * + * Returns: the bitrate of video stream. + */ +guint +clapper_video_stream_get_bitrate (ClapperVideoStream *self) +{ + guint bitrate; + + g_return_val_if_fail (CLAPPER_IS_VIDEO_STREAM (self), 0); + + GST_OBJECT_LOCK (self); + bitrate = self->bitrate; + GST_OBJECT_UNLOCK (self); + + return bitrate; +} + +/** + * clapper_video_stream_get_pixel_format: + * @stream: a #ClapperVideoStream + * + * Get pixel format of video @stream. + * + * Returns: (transfer full) (nullable): the pixel format of stream + * or %NULL if undetermined. + */ +gchar * +clapper_video_stream_get_pixel_format (ClapperVideoStream *self) +{ + gchar *pixel_format; + + g_return_val_if_fail (CLAPPER_IS_VIDEO_STREAM (self), NULL); + + GST_OBJECT_LOCK (self); + pixel_format = g_strdup (self->pixel_format); + GST_OBJECT_UNLOCK (self); + + return pixel_format; +} + +static void +clapper_video_stream_init (ClapperVideoStream *self) +{ +} + +static void +clapper_video_stream_finalize (GObject *object) +{ + ClapperVideoStream *self = CLAPPER_VIDEO_STREAM_CAST (object); + + g_free (self->codec); + g_free (self->pixel_format); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_video_stream_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperVideoStream *self = CLAPPER_VIDEO_STREAM_CAST (object); + + switch (prop_id) { + case PROP_CODEC: + g_value_take_string (value, clapper_video_stream_get_codec (self)); + break; + case PROP_WIDTH: + g_value_set_int (value, clapper_video_stream_get_width (self)); + break; + case PROP_HEIGHT: + g_value_set_int (value, clapper_video_stream_get_height (self)); + break; + case PROP_FPS: + g_value_set_double (value, clapper_video_stream_get_fps (self)); + break; + case PROP_BITRATE: + g_value_set_uint (value, clapper_video_stream_get_bitrate (self)); + break; + case PROP_PIXEL_FORMAT: + g_value_take_string (value, clapper_video_stream_get_pixel_format (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_video_stream_internal_stream_updated (ClapperStream *stream, + GstCaps *caps, GstTagList *tags) +{ + ClapperVideoStream *self = CLAPPER_VIDEO_STREAM_CAST (stream); + + CLAPPER_STREAM_CLASS (parent_class)->internal_stream_updated (stream, caps, tags); + + if (caps) + _update_using_caps (self, caps); + if (tags) + _update_using_tags (self, tags); +} + +static void +clapper_video_stream_class_init (ClapperVideoStreamClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperStreamClass *stream_class = (ClapperStreamClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappervideostream", 0, + "Clapper Video Stream"); + + gobject_class->get_property = clapper_video_stream_get_property; + gobject_class->finalize = clapper_video_stream_finalize; + + stream_class->internal_stream_updated = clapper_video_stream_internal_stream_updated; + + /** + * ClapperVideoStream:codec: + * + * Stream codec. + */ + param_specs[PROP_CODEC] = g_param_spec_string ("codec", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperVideoStream:width: + * + * Stream width. + */ + param_specs[PROP_WIDTH] = g_param_spec_int ("width", + NULL, NULL, 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperVideoStream:height: + * + * Stream height. + */ + param_specs[PROP_HEIGHT] = g_param_spec_int ("height", + NULL, NULL, 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperVideoStream:fps: + * + * Stream FPS. + */ + param_specs[PROP_FPS] = g_param_spec_double ("fps", + NULL, NULL, 0, G_MAXDOUBLE, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperVideoStream:bitrate: + * + * Stream bitrate. + */ + param_specs[PROP_BITRATE] = g_param_spec_uint ("bitrate", + NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperVideoStream:pixel-format: + * + * Stream pixel format. + */ + param_specs[PROP_PIXEL_FORMAT] = g_param_spec_string ("pixel-format", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/clapper-video-stream.h b/src/lib/clapper/clapper-video-stream.h new file mode 100644 index 00000000..8fff48a1 --- /dev/null +++ b/src/lib/clapper/clapper-video-stream.h @@ -0,0 +1,49 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_VIDEO_STREAM (clapper_video_stream_get_type()) +#define CLAPPER_VIDEO_STREAM_CAST(obj) ((ClapperVideoStream *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperVideoStream, clapper_video_stream, CLAPPER, VIDEO_STREAM, ClapperStream) + +gchar * clapper_video_stream_get_codec (ClapperVideoStream *stream); + +gint clapper_video_stream_get_width (ClapperVideoStream *stream); + +gint clapper_video_stream_get_height (ClapperVideoStream *stream); + +gdouble clapper_video_stream_get_fps (ClapperVideoStream *stream); + +guint clapper_video_stream_get_bitrate (ClapperVideoStream *stream); + +gchar * clapper_video_stream_get_pixel_format (ClapperVideoStream *stream); + +G_END_DECLS diff --git a/src/lib/clapper/clapper.c b/src/lib/clapper/clapper.c new file mode 100644 index 00000000..4db668c7 --- /dev/null +++ b/src/lib/clapper/clapper.c @@ -0,0 +1,94 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "clapper.h" +#include "clapper-utils-private.h" +#include "clapper-playbin-bus-private.h" +#include "clapper-app-bus-private.h" +#include "clapper-features-bus-private.h" + +static gboolean is_initialized = FALSE; +static GMutex init_lock; + +static gboolean +clapper_init_check_internal (int *argc, char **argv[]) +{ + g_mutex_lock (&init_lock); + + if (is_initialized || !gst_init_check (argc, argv, NULL)) + goto finish; + + gst_pb_utils_init (); + + clapper_utils_initialize (); + clapper_playbin_bus_initialize (); + clapper_app_bus_initialize (); + clapper_features_bus_initialize (); + + is_initialized = TRUE; + +finish: + g_mutex_unlock (&init_lock); + + return is_initialized; +} + +/** + * clapper_init: + * @argc: (inout) (nullable) (optional): pointer to application's argc + * @argv: (inout) (array length=argc) (nullable) (optional): pointer to application's argv + * + * Initializes the Clapper library. Implementations must always call this + * before using Clapper API. + * + * Because Clapper uses GStreamer internally, this function will also initialize + * GStreamer before initializing Clapper itself for user convienience, so + * application does not have to do so anymore. + * + * WARNING: This function will terminate your program if it was unable to + * initialize for some reason. If you want to do some fallback logic, + * use [func@Clapper.init_check] instead. + */ +void +clapper_init (int *argc, char **argv[]) +{ + if (!clapper_init_check_internal (argc, argv)) { + g_printerr ("Could not initialize Clapper library\n"); + exit (1); + } +} + +/** + * clapper_init_check: + * @argc: (inout) (nullable) (optional): pointer to application's argc + * @argv: (inout) (array length=argc) (nullable) (optional): pointer to application's argv + * + * This function does the same thing as [func@Clapper.init], but instead of + * terminating on failure it returns %FALSE with @error set. + * + * Returns: %TRUE if Clapper could be initialized, %FALSE otherwise. + */ +gboolean +clapper_init_check (int *argc, char **argv[]) +{ + return clapper_init_check_internal (argc, argv); +} diff --git a/src/lib/clapper/clapper.h b/src/lib/clapper/clapper.h new file mode 100644 index 00000000..9f1dac71 --- /dev/null +++ b/src/lib/clapper/clapper.h @@ -0,0 +1,61 @@ +/* Clapper Playback Library + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#define __CLAPPER_INSIDE__ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if CLAPPER_HAVE_DISCOVERER +#include +#endif +#if CLAPPER_HAVE_MPRIS +#include +#endif +#if CLAPPER_HAVE_SERVER +#include +#endif + +G_BEGIN_DECLS + +void clapper_init (int *argc, char **argv[]); + +gboolean clapper_init_check (int *argc, char **argv[]); + +G_END_DECLS + +#undef __CLAPPER_INSIDE__ diff --git a/src/lib/clapper/features/clapper-features-availability.h.in b/src/lib/clapper/features/clapper-features-availability.h.in new file mode 100644 index 00000000..58df92a3 --- /dev/null +++ b/src/lib/clapper/features/clapper-features-availability.h.in @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +/** + * CLAPPER_HAVE_DISCOVERER: + * + * Check if Clapper was compiled with Discoverer feature. + */ +#define CLAPPER_HAVE_DISCOVERER (@CLAPPER_HAVE_DISCOVERER@) + +/** + * CLAPPER_HAVE_MPRIS: + * + * Check if Clapper was compiled with MPRIS feature. + */ +#define CLAPPER_HAVE_MPRIS (@CLAPPER_HAVE_MPRIS@) + +/** + * CLAPPER_HAVE_SERVER: + * + * Check if Clapper was compiled with Server feature. + */ +#define CLAPPER_HAVE_SERVER (@CLAPPER_HAVE_SERVER@) diff --git a/src/lib/clapper/features/discoverer/clapper-discoverer.c b/src/lib/clapper/features/discoverer/clapper-discoverer.c new file mode 100644 index 00000000..f55a78e5 --- /dev/null +++ b/src/lib/clapper/features/discoverer/clapper-discoverer.c @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperDiscoverer: + * + * An optional Discoverer feature to be added to the player. + * + * #ClapperDiscoverer is a feature that wraps around #GstDiscoverer API + * to automatically discover items within [class@Clapper.Queue]. Once media + * is scanned, all extra information of it will be filled within media item, + * this includes title, duration, chapters, etc. + * + * Please note that media items are also discovered during their playback + * by the player itself. #ClapperDiscoverer is useful in situations where + * one wants to present to the user an updated media item before its + * playback, such as an UI that displays playback queue. + * + * Depending on your application, you can select an optimal + * [enum@Clapper.DiscovererDiscoveryMode] that best suits your needs. + * + * Use [const@Clapper.HAVE_DISCOVERER] macro to check if Clapper API + * was compiled with this feature. + */ + +#include +#include + +#include "clapper-discoverer.h" +#include "clapper-queue.h" +#include "clapper-media-item-private.h" +#include "../shared/clapper-shared-utils-private.h" + +#define DEFAULT_DISCOVERY_MODE CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT + +#define GST_CAT_DEFAULT clapper_discoverer_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperDiscoverer +{ + ClapperFeature parent; + + GstDiscoverer *discoverer; + + GPtrArray *pending_items; + ClapperMediaItem *discovered_item; + + gboolean running; + GSource *timeout_source; + + ClapperDiscovererDiscoveryMode discovery_mode; +}; + +enum +{ + PROP_0, + PROP_DISCOVERY_MODE, + PROP_LAST +}; + +#define parent_class clapper_discoverer_parent_class +G_DEFINE_TYPE (ClapperDiscoverer, clapper_discoverer, CLAPPER_TYPE_FEATURE); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static inline void +_clear_timeout_source (ClapperDiscoverer *self) +{ + if (self->timeout_source) { + g_source_destroy (self->timeout_source); + g_clear_pointer (&self->timeout_source, g_source_unref); + } +} + +static inline void +_unqueue_discovery (ClapperDiscoverer *self, ClapperMediaItem *item) +{ + guint index = 0; + + /* Removing item that is being discovered */ + if (item == self->discovered_item) { + GST_DEBUG_OBJECT (self, "Ignoring discovery of current item %" GST_PTR_FORMAT, item); + gst_clear_object (&self->discovered_item); + } else if (g_ptr_array_find (self->pending_items, item, &index)) { + GST_DEBUG_OBJECT (self, "Removing discovery of pending item %" GST_PTR_FORMAT, item); + g_ptr_array_remove_index (self->pending_items, index); + } +} + +static inline void +_start_discovery (ClapperDiscoverer *self) +{ + if (!self->running) { + gst_discoverer_start (self->discoverer); + self->running = TRUE; + GST_INFO_OBJECT (self, "Discoverer started"); + } +} + +static inline void +_stop_discovery (ClapperDiscoverer *self) +{ + if (self->running) { + gst_discoverer_stop (self->discoverer); + self->running = FALSE; + GST_INFO_OBJECT (self, "Discoverer stopped"); + } +} + +static void +_run_discovery (ClapperDiscoverer *self) +{ + ClapperMediaItem *item; + ClapperQueue *queue; + ClapperDiscovererDiscoveryMode discovery_mode; + const gchar *uri; + gboolean success = FALSE; + + if (self->pending_items->len == 0) { + GST_DEBUG_OBJECT (self, "No more pending items"); + return; + } + + item = g_ptr_array_steal_index (self->pending_items, 0); + + GST_DEBUG_OBJECT (self, "Investigating discovery of %" GST_PTR_FORMAT, item); + + queue = CLAPPER_QUEUE_CAST (gst_object_get_parent (GST_OBJECT_CAST (item))); + + if (G_UNLIKELY (queue == NULL)) { + GST_DEBUG_OBJECT (self, "Queued item %" GST_PTR_FORMAT + " does not appear to be in queue anymore", item); + goto finish; + } + + discovery_mode = clapper_discoverer_get_discovery_mode (self); + + if (discovery_mode == CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT + && clapper_queue_item_is_current (queue, item)) { + GST_DEBUG_OBJECT (self, "Queued %" GST_PTR_FORMAT + " is current item, ignoring discovery", item); + goto finish; + } + + uri = clapper_media_item_get_uri (item); + GST_DEBUG_OBJECT (self, "Starting discovery of %" + GST_PTR_FORMAT "(%s)", item, uri); + + /* Need to start first, then append URI */ + _start_discovery (self); + + if ((success = gst_discoverer_discover_uri_async (self->discoverer, uri))) { + gst_object_replace ((GstObject **) &self->discovered_item, GST_OBJECT_CAST (item)); + GST_DEBUG_OBJECT (self, "Running discovery of %" + GST_PTR_FORMAT "(%s)", self->discovered_item, uri); + } else { + GST_ERROR_OBJECT (self, "Could not run discovery of %" + GST_PTR_FORMAT "(%s)", item, uri); + } + +finish: + gst_clear_object (&item); + gst_clear_object (&queue); + + /* Continue until we run out of pending items */ + if (!success) + _run_discovery (self); +} + +static gboolean +_run_discovery_delayed_cb (ClapperDiscoverer *self) +{ + GST_DEBUG_OBJECT (self, "Delayed discovery handler reached"); + + _clear_timeout_source (self); + _run_discovery (self); + + return G_SOURCE_REMOVE; +} + +static void +_discovered_cb (GstDiscoverer *discoverer G_GNUC_UNUSED, + GstDiscovererInfo *info, GError *error, ClapperDiscoverer *self) +{ + /* Can be NULL if removed while discovery of it was running */ + if (self->discovered_item) { + const gchar *uri = clapper_media_item_get_uri (self->discovered_item); + + if (G_LIKELY (error == NULL)) { + GST_DEBUG_OBJECT (self, "Finished discovery of %" + GST_PTR_FORMAT "(%s)", self->discovered_item, uri); + clapper_media_item_update_from_discoverer_info (self->discovered_item, info); + } else { + GST_ERROR_OBJECT (self, "Discovery of %" GST_PTR_FORMAT + "(%s) failed, reason: %s", self->discovered_item, uri, error->message); + } + + /* Clear so its NULL when replaced later */ + gst_clear_object (&self->discovered_item); + } + + /* Try to discover next item */ + _run_discovery (self); +} + +static void +_finished_cb (GstDiscoverer *discoverer G_GNUC_UNUSED, ClapperDiscoverer *self) +{ + if (G_LIKELY (self->pending_items->len == 0)) { + GST_DEBUG_OBJECT (self, "Finished discovery of all items"); + } else { + /* This should never happen, but if it does, then clear + * pending items array so we can somewhat recover */ + GST_ERROR_OBJECT (self, "Discovery stopped, but still had %u pending items!", + self->pending_items->len); + g_ptr_array_remove_range (self->pending_items, 0, self->pending_items->len); + } + + _stop_discovery (self); +} + +static void +clapper_discoverer_played_item_changed (ClapperFeature *feature, ClapperMediaItem *item) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Played item changed to: %" GST_PTR_FORMAT, item); + _unqueue_discovery (self, item); +} + +static void +clapper_discoverer_queue_item_added (ClapperFeature *feature, ClapperMediaItem *item, guint index) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Queue item added %" GST_PTR_FORMAT, item); + + g_ptr_array_add (self->pending_items, gst_object_ref (item)); + + /* Already running, nothing more to do */ + if (self->running) + return; + + /* Need to always clear timeout here, as mode may + * have changed between adding multiple items */ + _clear_timeout_source (self); + + switch (clapper_discoverer_get_discovery_mode (self)) { + case CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT: + /* We start running after small delay in this mode, so + * application can select item after adding it to queue first */ + self->timeout_source = clapper_shared_utils_context_timeout_add_full ( + g_main_context_get_thread_default (), + G_PRIORITY_DEFAULT_IDLE, 50, + (GSourceFunc) _run_discovery_delayed_cb, + self, NULL); + break; + case CLAPPER_DISCOVERER_DISCOVERY_ALWAYS: + _run_discovery (self); + break; + default: + g_assert_not_reached (); + break; + } +} + +static void +clapper_discoverer_queue_item_removed (ClapperFeature *feature, ClapperMediaItem *item, guint index) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Queue item removed %" GST_PTR_FORMAT, item); + _unqueue_discovery (self, item); +} + +static void +clapper_discoverer_queue_cleared (ClapperFeature *feature) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Discarding discovery of all pending items"); + + if (self->pending_items->len > 0) + g_ptr_array_remove_range (self->pending_items, 0, self->pending_items->len); + + gst_clear_object (&self->discovered_item); + + _stop_discovery (self); +} + +static gboolean +clapper_discoverer_prepare (ClapperFeature *feature) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (feature); + GError *error = NULL; + + GST_DEBUG_OBJECT (self, "Prepare"); + + self->discoverer = gst_discoverer_new (15 * GST_SECOND, &error); + + if (G_UNLIKELY (error != NULL)) { + GST_ERROR_OBJECT (self, "Could not prepare, reason: %s", error->message); + g_error_free (error); + + return FALSE; + } + + GST_TRACE_OBJECT (self, "Created new GstDiscoverer: %" GST_PTR_FORMAT, self->discoverer); + + /* FIXME: Caching in GStreamer is broken. Does not save container tags, such as media title. + * Disable it until completely fixed upsteam. Once fixed change to %TRUE. */ + g_object_set (self->discoverer, "use-cache", FALSE, NULL); + + g_signal_connect (self->discoverer, "discovered", + G_CALLBACK (_discovered_cb), self); + g_signal_connect (self->discoverer, "finished", + G_CALLBACK (_finished_cb), self); + + return TRUE; +} + +static gboolean +clapper_discoverer_unprepare (ClapperFeature *feature) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Unprepare"); + + _clear_timeout_source (self); + + /* Do what we also do when queue is cleared */ + clapper_discoverer_queue_cleared (feature); + + gst_clear_object (&self->discoverer); + + return TRUE; +} + +/** + * clapper_discoverer_new: + * + * Creates a new #ClapperDiscoverer instance. + * + * Returns: (transfer full): a new #ClapperDiscoverer instance. + */ +ClapperDiscoverer * +clapper_discoverer_new (void) +{ + ClapperDiscoverer *discoverer = g_object_new (CLAPPER_TYPE_DISCOVERER, NULL); + gst_object_ref_sink (discoverer); + + return discoverer; +} + +/** + * clapper_discoverer_set_discovery_mode: + * @discoverer: a #ClapperDiscoverer + * @mode: a #ClapperDiscovererDiscoveryMode + * + * Set the [enum@Clapper.DiscovererDiscoveryMode] of @discoverer. + */ +void +clapper_discoverer_set_discovery_mode (ClapperDiscoverer *self, ClapperDiscovererDiscoveryMode mode) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_DISCOVERER (self)); + + GST_OBJECT_LOCK (self); + if ((changed = self->discovery_mode != mode)) + self->discovery_mode = mode; + GST_OBJECT_UNLOCK (self); + + if (changed) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_DISCOVERY_MODE]); +} + +/** + * clapper_discoverer_get_discovery_mode: + * @discoverer: a #ClapperDiscoverer + * + * Get the [enum@Clapper.DiscovererDiscoveryMode] of @discoverer. + * + * Returns: a currently set #ClapperDiscovererDiscoveryMode. + */ +ClapperDiscovererDiscoveryMode +clapper_discoverer_get_discovery_mode (ClapperDiscoverer *self) +{ + ClapperDiscovererDiscoveryMode mode; + + g_return_val_if_fail (CLAPPER_IS_DISCOVERER (self), DEFAULT_DISCOVERY_MODE); + + GST_OBJECT_LOCK (self); + mode = self->discovery_mode; + GST_OBJECT_UNLOCK (self); + + return mode; +} + +static void +clapper_discoverer_init (ClapperDiscoverer *self) +{ + self->pending_items = g_ptr_array_new_with_free_func ((GDestroyNotify) gst_object_unref); + + self->discovery_mode = DEFAULT_DISCOVERY_MODE; +} + +static void +clapper_discoverer_finalize (GObject *object) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (object); + + g_ptr_array_unref (self->pending_items); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_discoverer_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (object); + + switch (prop_id) { + case PROP_DISCOVERY_MODE: + clapper_discoverer_set_discovery_mode (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_discoverer_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperDiscoverer *self = CLAPPER_DISCOVERER_CAST (object); + + switch (prop_id) { + case PROP_DISCOVERY_MODE: + g_value_set_enum (value, clapper_discoverer_get_discovery_mode (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_discoverer_class_init (ClapperDiscovererClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperFeatureClass *feature_class = (ClapperFeatureClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperdiscoverer", 0, + "Clapper Discoverer"); + + gobject_class->get_property = clapper_discoverer_get_property; + gobject_class->set_property = clapper_discoverer_set_property; + gobject_class->finalize = clapper_discoverer_finalize; + + feature_class->prepare = clapper_discoverer_prepare; + feature_class->unprepare = clapper_discoverer_unprepare; + feature_class->played_item_changed = clapper_discoverer_played_item_changed; + feature_class->queue_item_added = clapper_discoverer_queue_item_added; + feature_class->queue_item_removed = clapper_discoverer_queue_item_removed; + feature_class->queue_cleared = clapper_discoverer_queue_cleared; + + /** + * ClapperDiscoverer:discovery-mode: + * + * Discoverer discovery mode. + */ + param_specs[PROP_DISCOVERY_MODE] = g_param_spec_enum ("discovery-mode", + NULL, NULL, CLAPPER_TYPE_DISCOVERER_DISCOVERY_MODE, DEFAULT_DISCOVERY_MODE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/features/discoverer/clapper-discoverer.h b/src/lib/clapper/features/discoverer/clapper-discoverer.h new file mode 100644 index 00000000..7840455b --- /dev/null +++ b/src/lib/clapper/features/discoverer/clapper-discoverer.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_DISCOVERER (clapper_discoverer_get_type()) +#define CLAPPER_DISCOVERER_CAST(obj) ((ClapperDiscoverer *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperDiscoverer, clapper_discoverer, CLAPPER, DISCOVERER, ClapperFeature) + +ClapperDiscoverer * clapper_discoverer_new (void); + +void clapper_discoverer_set_discovery_mode (ClapperDiscoverer *discoverer, ClapperDiscovererDiscoveryMode mode); + +ClapperDiscovererDiscoveryMode clapper_discoverer_get_discovery_mode (ClapperDiscoverer *discoverer); + +G_END_DECLS diff --git a/src/lib/clapper/features/discoverer/meson.build b/src/lib/clapper/features/discoverer/meson.build new file mode 100644 index 00000000..f014afc5 --- /dev/null +++ b/src/lib/clapper/features/discoverer/meson.build @@ -0,0 +1,17 @@ +feature_option = get_option(feature_name) + +if feature_option.disabled() + subdir_done() +endif + +clapper_features_headers += [ + 'features/discoverer/clapper-discoverer.h', +] +clapper_features_sources += [ + 'features/discoverer/clapper-discoverer.c', +] + +install_headers('clapper-discoverer.h', + install_dir: join_paths(clapper_headers_dir, 'features', 'discoverer'), +) +clapper_available_features += feature_name diff --git a/src/lib/clapper/features/meson.build b/src/lib/clapper/features/meson.build new file mode 100644 index 00000000..72a9348e --- /dev/null +++ b/src/lib/clapper/features/meson.build @@ -0,0 +1,32 @@ +clapper_features_headers = [] +clapper_features_sources = [] +clapper_features_sources_internal = [] +clapper_features_deps = [] +clapper_available_features = [] + +features_availability_conf = configuration_data() + +clapper_possible_features = [ + 'discoverer', + 'mpris', + 'server', +] +foreach feature_name : clapper_possible_features + subdir(feature_name) + features_availability_conf.set( + 'CLAPPER_HAVE_@0@'.format(feature_name.to_upper()), + clapper_available_features.contains(feature_name) ? 'TRUE' : 'FALSE' + ) +endforeach + +clapper_features_availability_header = configure_file( + input: 'clapper-features-availability.h.in', + output: 'clapper-features-availability.h', + configuration: features_availability_conf, +) +install_headers(clapper_features_availability_header, + install_dir: join_paths(clapper_headers_dir, 'features'), +) +clapper_features_headers += [ + clapper_features_availability_header +] diff --git a/data/gstclapper-mpris-gdbus.xml b/src/lib/clapper/features/mpris/clapper-mpris-gdbus.xml similarity index 63% rename from data/gstclapper-mpris-gdbus.xml rename to src/lib/clapper/features/mpris/clapper-mpris-gdbus.xml index c0866feb..51e8fc1f 100644 --- a/data/gstclapper-mpris-gdbus.xml +++ b/src/lib/clapper/features/mpris/clapper-mpris-gdbus.xml @@ -31,7 +31,7 @@ - + @@ -49,4 +49,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/clapper/features/mpris/clapper-mpris.c b/src/lib/clapper/features/mpris/clapper-mpris.c new file mode 100644 index 00000000..d5e2cda8 --- /dev/null +++ b/src/lib/clapper/features/mpris/clapper-mpris.c @@ -0,0 +1,1659 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperMpris: + * + * An optional `MPRIS` feature to add to the player. + * + * Not every OS supports `MPRIS`. Use [const@Clapper.HAVE_MPRIS] macro + * to check if Clapper API was compiled with this feature. + */ + +#include "clapper-mpris.h" +#include "clapper-mpris-gdbus.h" +#include "clapper-player.h" +#include "clapper-utils-private.h" + +#define CLAPPER_MPRIS_DO_WITH_PLAYER(mpris, _player_dst, ...) { \ + *_player_dst = CLAPPER_PLAYER_CAST (gst_object_get_parent (GST_OBJECT_CAST (mpris))); \ + if (G_LIKELY (*_player_dst != NULL)) \ + __VA_ARGS__ \ + gst_clear_object (_player_dst); } + +#define CLAPPER_MPRIS_SECONDS_TO_USECONDS(seconds) ((gint64) (seconds * G_GINT64_CONSTANT (1000000))) +#define CLAPPER_MPRIS_USECONDS_TO_SECONDS(useconds) ((gdouble) useconds / G_GINT64_CONSTANT (1000000)) + +#define CLAPPER_MPRIS_COMPARE(a,b) (strcmp (a,b) == 0) + +#define CLAPPER_MPRIS_NO_TRACK "/org/mpris/MediaPlayer2/TrackList/NoTrack" + +#define CLAPPER_MPRIS_PLAYBACK_STATUS_PLAYING "Playing" +#define CLAPPER_MPRIS_PLAYBACK_STATUS_PAUSED "Paused" +#define CLAPPER_MPRIS_PLAYBACK_STATUS_STOPPED "Stopped" + +#define CLAPPER_MPRIS_LOOP_NONE "None" +#define CLAPPER_MPRIS_LOOP_TRACK "Track" +#define CLAPPER_MPRIS_LOOP_PLAYLIST "Playlist" + +#define DEFAULT_QUEUE_CONTROLLABLE FALSE + +#define GST_CAT_DEFAULT clapper_mpris_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +typedef struct +{ + gchar *id; + ClapperMediaItem *item; +} ClapperMprisTrack; + +struct _ClapperMpris +{ + ClapperFeature parent; + + ClapperMprisMediaPlayer2 *base_skeleton; + ClapperMprisMediaPlayer2Player *player_skeleton; + ClapperMprisMediaPlayer2TrackList *tracks_skeleton; + + gboolean base_exported; + gboolean player_exported; + gboolean tracks_exported; + + guint name_id; + gboolean registered; + + GMainLoop *loop; + + GPtrArray *tracks; + ClapperMprisTrack *current_track; + + ClapperQueueProgressionMode default_mode; + ClapperQueueProgressionMode non_shuffle_mode; + + gchar *own_name; + gchar *identity; + gchar *desktop_entry; + + gint queue_controllable; // atomic + gchar *fallback_art_url; +}; + +enum +{ + PROP_0, + PROP_OWN_NAME, + PROP_IDENTITY, + PROP_DESKTOP_ENTRY, + PROP_QUEUE_CONTROLLABLE, + PROP_FALLBACK_ART_URL, + PROP_LAST +}; + +#define parent_class clapper_mpris_parent_class +G_DEFINE_TYPE (ClapperMpris, clapper_mpris, CLAPPER_TYPE_FEATURE); + +static const gchar *const empty_tracklist[] = { NULL, }; +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; + +static ClapperMprisTrack * +clapper_mpris_track_new (ClapperMediaItem *item) +{ + ClapperMprisTrack *track = g_new (ClapperMprisTrack, 1); + + /* MPRIS docs: "Media players may not use any paths starting with /org/mpris + * unless explicitly allowed by this specification. Such paths are intended to + * have special meaning, such as /org/mpris/MediaPlayer2/TrackList/NoTrack" */ + track->id = g_strdup_printf ("/org/clapper/MediaItem%u", + clapper_media_item_get_id (item)); + + track->item = gst_object_ref (item); + + GST_TRACE ("Created track: %s", track->id); + + return track; +} + +static void +clapper_mpris_track_free (ClapperMprisTrack *track) +{ + GST_TRACE ("Freeing track: %s", track->id); + + g_free (track->id); + gst_object_unref (track->item); + + g_free (track); +} + +static inline void +_mpris_read_initial_tracks (ClapperMpris *self, ClapperQueue *queue) +{ + ClapperMediaItem *item, *current_item; + guint i = 0; + + current_item = clapper_queue_get_current_item (queue); + + while ((item = clapper_queue_get_item (queue, i))) { + ClapperMprisTrack *track = clapper_mpris_track_new (item); + + if (track->item == current_item) + self->current_track = track; + + g_ptr_array_add (self->tracks, track); + + gst_object_unref (item); + i++; + } + + gst_clear_object (¤t_item); +} + +static gboolean +_mpris_find_track_by_item (ClapperMpris *self, ClapperMediaItem *search_item, guint *index) +{ + guint i; + + for (i = 0; i < self->tracks->len; ++i) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, i); + + if (search_item == track->item) { + if (index) + *index = i; + + return TRUE; + } + } + + return FALSE; +} + +static gboolean +_mpris_find_track_by_id (ClapperMpris *self, const gchar *search_id, guint *index) +{ + guint i; + + for (i = 0; i < self->tracks->len; ++i) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, i); + + if (CLAPPER_MPRIS_COMPARE (track->id, search_id)) { + if (index) + *index = i; + + return TRUE; + } + } + + return FALSE; +} + +static GVariant * +_mpris_build_track_metadata (ClapperMpris *self, ClapperMprisTrack *track) +{ + GVariantBuilder builder; + GVariant *variant; + const gchar *uri; + gchar *title; + gint64 duration; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + uri = clapper_media_item_get_uri (track->item); + title = clapper_media_item_get_title (track->item); + duration = CLAPPER_MPRIS_SECONDS_TO_USECONDS ( + clapper_media_item_get_duration (track->item)); + + g_variant_builder_add (&builder, "{sv}", "mpris:trackid", + g_variant_new_string (track->id)); + g_variant_builder_add (&builder, "{sv}", "mpris:length", + g_variant_new_int64 (duration)); + g_variant_builder_add (&builder, "{sv}", "xesam:url", + g_variant_new_string (uri)); + if (title) { + g_variant_builder_add (&builder, "{sv}", "xesam:title", + g_variant_new_string (title)); + } + + /* TODO: Fill more xesam props from tags within media info */ + + GST_OBJECT_LOCK (self); + + /* TODO: Support image sample or per-item custom artwork */ + if (self->fallback_art_url) { + g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", + g_variant_new_string (self->fallback_art_url)); + } + + GST_OBJECT_UNLOCK (self); + + variant = g_variant_builder_end (&builder); + + g_free (title); + + return variant; +} + +static gchar ** +_filter_names (const gchar *const *all_names) +{ + GStrvBuilder *builder; + gchar **filtered_names; + guint i; + + builder = g_strv_builder_new (); + + for (i = 0; all_names[i]; ++i) { + const gchar *const *remaining_names = all_names + i + 1; + + if (*remaining_names && g_strv_contains (remaining_names, all_names[i])) + continue; + + GST_LOG ("Found: %s", all_names[i]); + g_strv_builder_add (builder, all_names[i]); + } + + filtered_names = g_strv_builder_end (builder); + g_strv_builder_unref (builder); + + return filtered_names; +} + +static gchar ** +clapper_mpris_get_supported_uri_schemes (ClapperMpris *self) +{ + GStrvBuilder *builder; + gchar **all_schemes, **filtered_schemes; + GList *elements, *el; + guint i; + + GST_DEBUG_OBJECT (self, "Checking supported URI schemes"); + + builder = g_strv_builder_new (); + elements = gst_element_factory_list_get_elements ( + GST_ELEMENT_FACTORY_TYPE_SRC, GST_RANK_NONE); + + for (el = elements; el != NULL; el = el->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY (el->data); + const gchar *const *protocols; + + if (gst_element_factory_get_uri_type (factory) != GST_URI_SRC) + continue; + + if (!(protocols = gst_element_factory_get_uri_protocols (factory))) + continue; + + for (i = 0; protocols[i]; ++i) + g_strv_builder_add (builder, protocols[i]); + } + + all_schemes = g_strv_builder_end (builder); + g_strv_builder_unref (builder); + gst_plugin_feature_list_free (elements); + + filtered_schemes = _filter_names ((const gchar *const *) all_schemes); + g_strfreev (all_schemes); + + return filtered_schemes; +} + +static gchar ** +clapper_mpris_get_supported_mime_types (ClapperMpris *self) +{ + GStrvBuilder *builder; + gchar **all_types, **filtered_types; + GList *elements, *el; + + GST_DEBUG_OBJECT (self, "Checking supported mime-types"); + + builder = g_strv_builder_new (); + elements = gst_element_factory_list_get_elements ( + GST_ELEMENT_FACTORY_TYPE_DEMUXER, GST_RANK_NONE); + + for (el = elements; el != NULL; el = el->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY (el->data); + const GList *pad_templates, *pt; + + pad_templates = gst_element_factory_get_static_pad_templates (factory); + + for (pt = pad_templates; pt != NULL; pt = pt->next) { + GstStaticPadTemplate *template = (GstStaticPadTemplate *) pt->data; + GstCaps *caps; + guint i, size; + + if (template->direction != GST_PAD_SINK) + continue; + + caps = gst_static_pad_template_get_caps (template); + size = gst_caps_get_size (caps); + + for (i = 0; i < size; ++i) { + GstStructure *structure = gst_caps_get_structure (caps, i); + const gchar *name = gst_structure_get_name (structure); + + /* Skip GStreamer internal mime types */ + if (g_str_has_prefix (name, "application/x-gst-")) + continue; + + /* GStreamer uses "video/quicktime" for MP4. If we can + * handle it, then also add more generic ones. */ + if (strcmp (name, "video/quicktime") == 0) { + g_strv_builder_add (builder, "video/mp4"); + g_strv_builder_add (builder, "audio/mp4"); + } + + g_strv_builder_add (builder, name); + } + + gst_caps_unref (caps); + } + } + + all_types = g_strv_builder_end (builder); + g_strv_builder_unref (builder); + gst_plugin_feature_list_free (elements); + + filtered_types = _filter_names ((const gchar *const *) all_types); + g_strfreev (all_types); + + return filtered_types; +} + +static void +clapper_mpris_unregister (ClapperMpris *self) +{ + GST_DEBUG_OBJECT (self, "Unregister"); + + if (self->base_exported) { + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->base_skeleton)); + self->base_exported = FALSE; + } + if (self->player_exported) { + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->player_skeleton)); + self->player_exported = FALSE; + } + if (self->tracks_exported) { + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->tracks_skeleton)); + self->tracks_exported = FALSE; + } + + self->registered = FALSE; +} + +static void +clapper_mpris_refresh_current_track (ClapperMpris *self, GVariant *variant) +{ + gboolean is_live = FALSE; + + GST_LOG_OBJECT (self, "Current track refresh"); + + /* Set or clear metadata */ + clapper_mpris_media_player2_player_set_metadata (self->player_skeleton, variant); + + /* Properties related to media item availablity, not current state */ + clapper_mpris_media_player2_player_set_can_play (self->player_skeleton, self->current_track != NULL); + clapper_mpris_media_player2_player_set_can_pause (self->player_skeleton, self->current_track != NULL); + + /* FIXME: Also disable for LIVE content */ + clapper_mpris_media_player2_player_set_can_seek (self->player_skeleton, self->current_track != NULL); + clapper_mpris_media_player2_player_set_minimum_rate (self->player_skeleton, (is_live) ? 1.0 : G_MINDOUBLE); + clapper_mpris_media_player2_player_set_maximum_rate (self->player_skeleton, (is_live) ? 1.0 : G_MAXDOUBLE); +} + +static void +clapper_mpris_refresh_track (ClapperMpris *self, ClapperMprisTrack *track) +{ + GVariant *variant = g_variant_take_ref (_mpris_build_track_metadata (self, track)); + + if (track == self->current_track) + clapper_mpris_refresh_current_track (self, variant); + + clapper_mpris_media_player2_track_list_emit_track_metadata_changed (self->tracks_skeleton, + track->id, variant); + + g_variant_unref (variant); +} + +static void +clapper_mpris_refresh_all_tracks (ClapperMpris *self) +{ + guint i; + + for (i = 0; i < self->tracks->len; ++i) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, i); + clapper_mpris_refresh_track (self, track); + } +} + +static void +clapper_mpris_refresh_track_list (ClapperMpris *self) +{ + GStrvBuilder *builder = NULL; + gchar **tracks_ids; + guint i; + + GST_LOG_OBJECT (self, "Track list refresh"); + + /* Track list is empty */ + if (self->tracks->len == 0) { + clapper_mpris_media_player2_track_list_set_tracks (self->tracks_skeleton, empty_tracklist); + return; + } + + builder = g_strv_builder_new (); + + for (i = 0; i < self->tracks->len; ++i) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, i); + g_strv_builder_add (builder, track->id); + } + + tracks_ids = g_strv_builder_end (builder); + g_strv_builder_unref (builder); + + clapper_mpris_media_player2_track_list_set_tracks (self->tracks_skeleton, (const gchar *const *) tracks_ids); + g_strfreev (tracks_ids); +} + +static void +clapper_mpris_refresh_can_go_next_previous (ClapperMpris *self) +{ + gboolean can_previous = FALSE, can_next = FALSE; + + GST_LOG_OBJECT (self, "Next/Previous availability refresh"); + + if (self->current_track && clapper_mpris_get_queue_controllable (self)) { + guint index = 0; + + if (_mpris_find_track_by_item (self, self->current_track->item, &index)) { + can_previous = (index > 0); + can_next = (index < self->tracks->len - 1); + } + } + + clapper_mpris_media_player2_player_set_can_go_previous (self->player_skeleton, can_previous); + clapper_mpris_media_player2_player_set_can_go_next (self->player_skeleton, can_next); +} + +static void +clapper_mpris_state_changed (ClapperFeature *feature, ClapperPlayerState state) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + const gchar *status_str = CLAPPER_MPRIS_PLAYBACK_STATUS_STOPPED; + + switch (state) { + case CLAPPER_PLAYER_STATE_PLAYING: + status_str = CLAPPER_MPRIS_PLAYBACK_STATUS_PLAYING; + break; + case CLAPPER_PLAYER_STATE_PAUSED: + case CLAPPER_PLAYER_STATE_BUFFERING: + status_str = CLAPPER_MPRIS_PLAYBACK_STATUS_PAUSED; + break; + default: + break; + } + + GST_DEBUG_OBJECT (self, "Playback status changed to: %s", status_str); + clapper_mpris_media_player2_player_set_playback_status (self->player_skeleton, status_str); +} + +static void +clapper_mpris_position_changed (ClapperFeature *feature, gdouble position) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + + GST_LOG_OBJECT (self, "Position changed to: %lf", position); + clapper_mpris_media_player2_player_set_position (self->player_skeleton, + CLAPPER_MPRIS_SECONDS_TO_USECONDS (position)); +} + +static void +clapper_mpris_speed_changed (ClapperFeature *feature, gdouble speed) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + gdouble mpris_speed; + + mpris_speed = clapper_mpris_media_player2_player_get_rate (self->player_skeleton); + + if (!G_APPROX_VALUE (speed, mpris_speed, FLT_EPSILON)) { + GST_LOG_OBJECT (self, "Speed changed to: %lf", speed); + clapper_mpris_media_player2_player_set_rate (self->player_skeleton, speed); + } +} + +static void +clapper_mpris_volume_changed (ClapperFeature *feature, gdouble volume) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + gdouble mpris_volume; + + if (G_UNLIKELY (volume < 0)) + volume = 0; + + mpris_volume = clapper_mpris_media_player2_player_get_volume (self->player_skeleton); + + if (!G_APPROX_VALUE (volume, mpris_volume, FLT_EPSILON)) { + GST_LOG_OBJECT (self, "Volume changed to: %lf", volume); + clapper_mpris_media_player2_player_set_volume (self->player_skeleton, volume); + } +} + +static void +clapper_mpris_played_item_changed (ClapperFeature *feature, ClapperMediaItem *item) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + GVariant *variant = NULL; + guint index = 0; + + GST_DEBUG_OBJECT (self, "Played item changed to: %" GST_PTR_FORMAT, item); + + if (G_LIKELY (_mpris_find_track_by_item (self, item, &index))) { + self->current_track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, index); + variant = _mpris_build_track_metadata (self, self->current_track); + } else { + self->current_track = NULL; + } + + clapper_mpris_refresh_current_track (self, variant); + clapper_mpris_refresh_can_go_next_previous (self); +} + +static void +clapper_mpris_item_updated (ClapperFeature *feature, ClapperMediaItem *item) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + guint index = 0; + + GST_LOG_OBJECT (self, "Item updated: %" GST_PTR_FORMAT, item); + + if (_mpris_find_track_by_item (self, item, &index)) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, index); + clapper_mpris_refresh_track (self, track); + } +} + +static void +clapper_mpris_queue_item_added (ClapperFeature *feature, ClapperMediaItem *item, guint index) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + ClapperMprisTrack *track, *prev_track = NULL; + GVariant *variant; + + /* Safety precaution for a case when someone adds MPRIS feature + * in middle of altering playlist from another thread, since we + * also read initial playlist after name is acquired. */ + if (G_UNLIKELY (_mpris_find_track_by_item (self, item, NULL))) + return; + + GST_DEBUG_OBJECT (self, "Queue item added at position: %u", index); + + track = clapper_mpris_track_new (item); + g_ptr_array_insert (self->tracks, index, track); + + clapper_mpris_refresh_track_list (self); + clapper_mpris_refresh_can_go_next_previous (self); + + variant = _mpris_build_track_metadata (self, track); + + /* NoTrack when item is added at first position in queue */ + clapper_mpris_media_player2_track_list_emit_track_added (self->tracks_skeleton, + variant, (prev_track != NULL) ? prev_track->id : CLAPPER_MPRIS_NO_TRACK); +} + +static void +clapper_mpris_queue_item_removed (ClapperFeature *feature, ClapperMediaItem *item, guint index) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + ClapperMprisTrack *track; + + GST_DEBUG_OBJECT (self, "Queue item removed"); + + track = (ClapperMprisTrack *) g_ptr_array_steal_index (self->tracks, index); + + if (track == self->current_track) { + self->current_track = NULL; + clapper_mpris_refresh_current_track (self, NULL); + } + + clapper_mpris_refresh_track_list (self); + clapper_mpris_refresh_can_go_next_previous (self); + clapper_mpris_media_player2_track_list_emit_track_removed (self->tracks_skeleton, track->id); + + clapper_mpris_track_free (track); +} + +static void +clapper_mpris_queue_item_repositioned (ClapperFeature *feature, guint before, guint after) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + ClapperMprisTrack *track; + + GST_DEBUG_OBJECT (self, "Queue item repositioned: %u -> %u", before, after); + + track = (ClapperMprisTrack *) g_ptr_array_steal_index (self->tracks, before); + g_ptr_array_insert (self->tracks, after, track); + + clapper_mpris_refresh_track_list (self); + clapper_mpris_refresh_can_go_next_previous (self); +} + +static void +clapper_mpris_queue_cleared (ClapperFeature *feature) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + guint n_items = self->tracks->len; + + if (n_items > 0) + g_ptr_array_remove_range (self->tracks, 0, n_items); + + self->current_track = NULL; + clapper_mpris_refresh_current_track (self, NULL); + clapper_mpris_refresh_can_go_next_previous (self); + clapper_mpris_refresh_track_list (self); + + clapper_mpris_media_player2_track_list_emit_track_list_replaced (self->tracks_skeleton, + empty_tracklist, CLAPPER_MPRIS_NO_TRACK); +} + +static void +clapper_mpris_queue_progression_changed (ClapperFeature *feature, ClapperQueueProgressionMode mode) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + const gchar *loop_status = CLAPPER_MPRIS_LOOP_NONE; + gboolean shuffle = FALSE; + + GST_DEBUG_OBJECT (self, "Queue progression changed to: %i", mode); + + switch (mode) { + case CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM: + loop_status = CLAPPER_MPRIS_LOOP_TRACK; + break; + case CLAPPER_QUEUE_PROGRESSION_CAROUSEL: + loop_status = CLAPPER_MPRIS_LOOP_PLAYLIST; + break; + case CLAPPER_QUEUE_PROGRESSION_SHUFFLE: + shuffle = TRUE; + break; + case CLAPPER_QUEUE_PROGRESSION_NONE: + case CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE: + self->default_mode = mode; + break; + default: + break; + } + + if (mode != CLAPPER_QUEUE_PROGRESSION_SHUFFLE) + self->non_shuffle_mode = mode; + + clapper_mpris_media_player2_player_set_loop_status (self->player_skeleton, loop_status); + clapper_mpris_media_player2_player_set_shuffle (self->player_skeleton, shuffle); +} + +static gboolean +_handle_open_uri_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, const gchar *uri, ClapperMpris *self) +{ + ClapperPlayer *player; + + if (!clapper_mpris_get_queue_controllable (self)) + return G_DBUS_METHOD_INVOCATION_UNHANDLED; + + GST_DEBUG_OBJECT (self, "Handle open URI: %s", uri); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + ClapperMediaItem *item = clapper_media_item_new (uri); + + /* We can only alter ClapperQueue from main thread. + * Adding items to it will trigger clapper_mpris_queue_item_added(), + * then we will add this new item to our track list */ + clapper_utils_queue_append_on_main_sync (queue, item); + + if (clapper_queue_select_item (queue, item)) + clapper_player_play (player); + + gst_object_unref (item); + }); + + clapper_mpris_media_player2_player_complete_open_uri (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_play_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle play"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + clapper_player_play (player); + }); + + clapper_mpris_media_player2_player_complete_play (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_pause_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle pause"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + clapper_player_pause (player); + }); + + clapper_mpris_media_player2_player_complete_pause (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_play_pause_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle play/pause"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperPlayerState state = clapper_player_get_state (player); + + switch (state) { + case CLAPPER_PLAYER_STATE_PLAYING: + clapper_player_pause (player); + break; + case CLAPPER_PLAYER_STATE_PAUSED: + case CLAPPER_PLAYER_STATE_STOPPED: + clapper_player_play (player); + break; + default: + break; + } + }); + + clapper_mpris_media_player2_player_complete_play_pause (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_stop_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle stop"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + clapper_player_stop (player); + }); + + clapper_mpris_media_player2_player_complete_stop (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_next_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, ClapperMpris *self) +{ + ClapperPlayer *player; + + if (!clapper_mpris_get_queue_controllable (self)) + return G_DBUS_METHOD_INVOCATION_UNHANDLED; + + GST_DEBUG_OBJECT (self, "Handle next"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + clapper_queue_select_next_item (queue); + }); + + clapper_mpris_media_player2_player_complete_next (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_previous_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, ClapperMpris *self) +{ + ClapperPlayer *player; + + if (!clapper_mpris_get_queue_controllable (self)) + return G_DBUS_METHOD_INVOCATION_UNHANDLED; + + GST_DEBUG_OBJECT (self, "Handle previous"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + clapper_queue_select_previous_item (queue); + }); + + clapper_mpris_media_player2_player_complete_previous (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_seek_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, gint64 offset, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle seek"); + + if (!self->current_track) + goto finish; + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + gdouble position, seek_position; + + position = clapper_player_get_position (player); + seek_position = position + CLAPPER_MPRIS_USECONDS_TO_SECONDS (offset); + + if (seek_position <= 0) { + clapper_player_seek (player, 0); + } else { + gdouble duration = clapper_media_item_get_duration (self->current_track->item); + + if (seek_position > duration) { + ClapperQueue *queue = clapper_player_get_queue (player); + clapper_queue_select_next_item (queue); + } else { + clapper_player_seek (player, seek_position); + } + } + }); + +finish: + clapper_mpris_media_player2_player_complete_seek (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_set_position_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GDBusMethodInvocation *invocation, const gchar *track_id, + gint64 position, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle set position"); + + if (G_UNLIKELY (position < 0) || !self->current_track) + goto finish; + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + gdouble duration, position_dbl; + + duration = clapper_media_item_get_duration (self->current_track->item); + position_dbl = CLAPPER_MPRIS_USECONDS_TO_SECONDS (position); + + if (position_dbl <= duration) + clapper_player_seek (player, position_dbl); + }); + +finish: + clapper_mpris_media_player2_player_complete_set_position (player_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +_handle_rate_notify_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GParamSpec *pspec G_GNUC_UNUSED, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle rate notify"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + gdouble speed, player_speed; + + speed = clapper_mpris_media_player2_player_get_rate (player_skeleton); + player_speed = clapper_player_get_speed (player); + + if (!G_APPROX_VALUE (speed, player_speed, FLT_EPSILON)) + clapper_player_set_speed (player, speed); + }); +} + +static void +_handle_volume_notify_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GParamSpec *pspec G_GNUC_UNUSED, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle volume notify"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + gdouble volume, player_volume; + + volume = clapper_mpris_media_player2_player_get_volume (player_skeleton); + player_volume = clapper_player_get_volume (player); + + if (!G_APPROX_VALUE (volume, player_volume, FLT_EPSILON)) + clapper_player_set_volume (player, volume); + }); +} + +static void +_handle_loop_status_notify_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GParamSpec *pspec G_GNUC_UNUSED, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle loop status notify"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + ClapperQueueProgressionMode mode, player_mode; + const gchar *loop_status; + + loop_status = clapper_mpris_media_player2_player_get_loop_status (player_skeleton); + player_mode = clapper_queue_get_progression_mode (queue); + + /* When in shuffle and no loop, assume default mode (none or consecutive). + * This prevents us from getting stuck constantly changing loop and shuffle. */ + if (player_mode == CLAPPER_QUEUE_PROGRESSION_SHUFFLE) + player_mode = self->default_mode; + + mode = CLAPPER_MPRIS_COMPARE (loop_status, CLAPPER_MPRIS_LOOP_TRACK) + ? CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM + : CLAPPER_MPRIS_COMPARE (loop_status, CLAPPER_MPRIS_LOOP_PLAYLIST) + ? CLAPPER_QUEUE_PROGRESSION_CAROUSEL + : self->default_mode; + + if (mode != player_mode) + clapper_queue_set_progression_mode (queue, mode); + }); +} + +static void +_handle_shuffle_notify_cb (ClapperMprisMediaPlayer2Player *player_skeleton, + GParamSpec *pspec G_GNUC_UNUSED, ClapperMpris *self) +{ + ClapperPlayer *player; + + GST_DEBUG_OBJECT (self, "Handle shuffle notify"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + ClapperQueueProgressionMode player_mode; + gboolean shuffle, player_shuffle; + + player_mode = clapper_queue_get_progression_mode (queue); + + shuffle = clapper_mpris_media_player2_player_get_shuffle (player_skeleton); + player_shuffle = (player_mode == CLAPPER_QUEUE_PROGRESSION_SHUFFLE); + + if (shuffle != player_shuffle) { + clapper_queue_set_progression_mode (queue, + (shuffle) ? CLAPPER_QUEUE_PROGRESSION_SHUFFLE : self->non_shuffle_mode); + } + }); +} + +static gboolean +_handle_get_tracks_metadata_cb (ClapperMprisMediaPlayer2TrackList *tracks_skeleton, + GDBusMethodInvocation *invocation, const gchar *const *tracks_ids, ClapperMpris *self) +{ + GVariantBuilder builder; + GVariant *tracks_variant = NULL; + gboolean initialized = FALSE; + guint i; + + GST_DEBUG_OBJECT (self, "Handle get tracks metadata"); + + for (i = 0; tracks_ids[i]; ++i) { + guint index = 0; + + if (_mpris_find_track_by_id (self, tracks_ids[i], &index)) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, index); + GVariant *variant = _mpris_build_track_metadata (self, track); + + if (!initialized) { + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + initialized = TRUE; + } + + g_variant_builder_add_value (&builder, variant); + } + } + + if (initialized) + tracks_variant = g_variant_builder_end (&builder); + + clapper_mpris_media_player2_track_list_complete_get_tracks_metadata (tracks_skeleton, + invocation, tracks_variant); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_add_track_cb (ClapperMprisMediaPlayer2TrackList *tracks_skeleton, + GDBusMethodInvocation *invocation, const gchar *uri, + const gchar *after_track, gboolean set_current, ClapperMpris *self) +{ + ClapperPlayer *player; + + if (!clapper_mpris_get_queue_controllable (self)) + return G_DBUS_METHOD_INVOCATION_UNHANDLED; + + GST_DEBUG_OBJECT (self, "Handle add track, URI: %s, after_track: %s," + " set_current: %s", uri, after_track, set_current ? "yes" : "no"); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperMediaItem *after_item = NULL; + gboolean add; + + if ((add = CLAPPER_MPRIS_COMPARE (after_track, CLAPPER_MPRIS_NO_TRACK))) { + GST_DEBUG_OBJECT (self, "Prepend, since requested after \"NoTrack\""); + } else { + guint index = 0; + + if ((add = _mpris_find_track_by_id (self, after_track, &index))) { + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, index); + + GST_DEBUG_OBJECT (self, "Append after: %s", track->id); + after_item = track->item; + } + } + + if (add) { + ClapperQueue *queue = clapper_player_get_queue (player); + ClapperMediaItem *item = clapper_media_item_new (uri); + + clapper_utils_queue_insert_on_main_sync (queue, item, after_item); + + if (set_current && clapper_queue_select_item (queue, item)) + clapper_player_play (player); + + gst_object_unref (item); + } + }); + + clapper_mpris_media_player2_track_list_complete_add_track (tracks_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_remove_track_cb (ClapperMprisMediaPlayer2TrackList *tracks_skeleton, + GDBusMethodInvocation *invocation, const gchar *track_id, ClapperMpris *self) +{ + ClapperPlayer *player; + guint index = 0; + + if (!clapper_mpris_get_queue_controllable (self)) + return G_DBUS_METHOD_INVOCATION_UNHANDLED; + + GST_DEBUG_OBJECT (self, "Handle remove track"); + + if (!_mpris_find_track_by_id (self, track_id, &index)) + goto finish; + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, index); + + clapper_utils_queue_remove_on_main_sync (queue, track->item); + }); + +finish: + clapper_mpris_media_player2_track_list_complete_remove_track (tracks_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +_handle_go_to_cb (ClapperMprisMediaPlayer2TrackList *tracks_skeleton, + GDBusMethodInvocation *invocation, const gchar *track_id, ClapperMpris *self) +{ + ClapperPlayer *player; + guint index = 0; + + if (!clapper_mpris_get_queue_controllable (self)) + return G_DBUS_METHOD_INVOCATION_UNHANDLED; + + if (!_mpris_find_track_by_id (self, track_id, &index)) + goto finish; + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + ClapperMprisTrack *track = (ClapperMprisTrack *) g_ptr_array_index (self->tracks, index); + + if (clapper_queue_select_item (queue, track->item)) + clapper_player_play (player); + }); + +finish: + clapper_mpris_media_player2_track_list_complete_go_to (tracks_skeleton, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +_name_acquired_cb (GDBusConnection *connection, const gchar *name, ClapperMpris *self) +{ + ClapperPlayer *player; + GError *error = NULL; + gchar **uri_schemes, **mime_types; + + GST_DEBUG_OBJECT (self, "Name acquired: %s", name); + + if (!(self->base_exported = g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (self->base_skeleton), + connection, "/org/mpris/MediaPlayer2", &error))) { + goto finish; + } + if (!(self->player_exported = g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (self->player_skeleton), + connection, "/org/mpris/MediaPlayer2", &error))) { + goto finish; + } + if (!(self->tracks_exported = g_dbus_interface_skeleton_export ( + G_DBUS_INTERFACE_SKELETON (self->tracks_skeleton), + connection, "/org/mpris/MediaPlayer2", &error))) { + goto finish; + } + + self->registered = TRUE; + + clapper_mpris_media_player2_set_identity (self->base_skeleton, self->identity); + clapper_mpris_media_player2_set_desktop_entry (self->base_skeleton, self->desktop_entry); + + uri_schemes = clapper_mpris_get_supported_uri_schemes (self); + clapper_mpris_media_player2_set_supported_uri_schemes (self->base_skeleton, + (const gchar *const *) uri_schemes); + g_strfreev (uri_schemes); + + mime_types = clapper_mpris_get_supported_mime_types (self); + clapper_mpris_media_player2_set_supported_mime_types (self->base_skeleton, + (const gchar *const *) mime_types); + g_strfreev (mime_types); + + /* As stated in MPRIS docs: "This property is not expected to change, + * as it describes an intrinsic capability of the implementation." */ + clapper_mpris_media_player2_player_set_can_control (self->player_skeleton, TRUE); + clapper_mpris_media_player2_set_has_track_list (self->base_skeleton, TRUE); + clapper_mpris_media_player2_track_list_set_can_edit_tracks (self->tracks_skeleton, + clapper_mpris_get_queue_controllable (self)); + + CLAPPER_MPRIS_DO_WITH_PLAYER (self, &player, { + ClapperQueue *queue = clapper_player_get_queue (player); + GVariant *variant = NULL; + + _mpris_read_initial_tracks (self, queue); + + /* Update tracks IDs after reading initial tracks from queue */ + clapper_mpris_refresh_track_list (self); + + if (self->current_track) + variant = _mpris_build_track_metadata (self, self->current_track); + + clapper_mpris_refresh_current_track (self, variant); + clapper_mpris_refresh_can_go_next_previous (self); + + /* Set some initial default progressions to revert to and + * try to update them in progression_changed call below */ + self->default_mode = CLAPPER_QUEUE_PROGRESSION_NONE; + self->non_shuffle_mode = CLAPPER_QUEUE_PROGRESSION_NONE; + + /* Trigger update with current values */ + clapper_mpris_state_changed (CLAPPER_FEATURE (self), clapper_player_get_state (player)); + clapper_mpris_position_changed (CLAPPER_FEATURE (self), clapper_player_get_position (player)); + clapper_mpris_speed_changed (CLAPPER_FEATURE (self), clapper_player_get_speed (player)); + clapper_mpris_volume_changed (CLAPPER_FEATURE (self), clapper_player_get_volume (player)); + clapper_mpris_queue_progression_changed (CLAPPER_FEATURE (self), clapper_queue_get_progression_mode (queue)); + }); + +finish: + if (error) { + GST_ERROR_OBJECT (self, "Error: %s", (error && error->message) + ? error->message : "Unknown DBUS error occured"); + g_error_free (error); + + clapper_mpris_unregister (self); + } + + if (self->loop && g_main_loop_is_running (self->loop)) + g_main_loop_quit (self->loop); +} + +static void +_name_lost_cb (GDBusConnection *connection, const gchar *name, ClapperMpris *self) +{ + GST_DEBUG_OBJECT (self, "Name lost: %s", name); + + if (self->loop && g_main_loop_is_running (self->loop)) + g_main_loop_quit (self->loop); + + clapper_mpris_unregister (self); +} + +static gboolean +clapper_mpris_prepare (ClapperFeature *feature) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + GDBusConnection *connection; + gchar *address; + + GST_DEBUG_OBJECT (self, "Prepare"); + + if (!(address = g_dbus_address_get_for_bus_sync (G_BUS_TYPE_SESSION, NULL, NULL))) { + GST_WARNING_OBJECT (self, "No MPRIS bus address"); + return FALSE; + } + + GST_INFO_OBJECT (self, "Obtained MPRIS DBus address: %s", address); + + connection = g_dbus_connection_new_for_address_sync (address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT + | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, NULL, NULL, NULL); + g_free (address); + + if (!connection) { + GST_WARNING_OBJECT (self, "No MPRIS bus connection"); + return FALSE; + } + + GST_INFO_OBJECT (self, "Obtained MPRIS DBus connection"); + + self->loop = g_main_loop_new (g_main_context_get_thread_default (), FALSE); + + self->name_id = g_bus_own_name_on_connection (connection, self->own_name, + G_BUS_NAME_OWNER_FLAGS_NONE, + (GBusNameAcquiredCallback) _name_acquired_cb, + (GBusNameLostCallback) _name_lost_cb, + self, NULL); + g_object_unref (connection); + + /* Wait until connection is established */ + g_main_loop_run (self->loop); + g_clear_pointer (&self->loop, g_main_loop_unref); + + if (self->registered) { + GST_DEBUG_OBJECT (self, "Own name ID: %u", self->name_id); + } else if (self->name_id > 0) { + GST_ERROR_OBJECT (self, "Could not register MPRIS connection"); + g_bus_unown_name (self->name_id); + self->name_id = 0; + } + + return self->registered; +} + +static gboolean +clapper_mpris_unprepare (ClapperFeature *feature) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + + GST_DEBUG_OBJECT (self, "Unprepare"); + + clapper_mpris_unregister (self); + + if (self->name_id > 0) { + g_bus_unown_name (self->name_id); + self->name_id = 0; + } + + return TRUE; +} + +static void +clapper_mpris_property_changed (ClapperFeature *feature, GParamSpec *pspec) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (feature); + + GST_DEBUG_OBJECT (self, "Property changed: \"%s\"", + g_param_spec_get_name (pspec)); + + if (pspec == param_specs[PROP_FALLBACK_ART_URL]) { + clapper_mpris_refresh_all_tracks (self); + } else if (pspec == param_specs[PROP_QUEUE_CONTROLLABLE]) { + clapper_mpris_media_player2_track_list_set_can_edit_tracks (self->tracks_skeleton, + clapper_mpris_get_queue_controllable (self)); + clapper_mpris_refresh_can_go_next_previous (self); + } +} + +/** + * clapper_mpris_new: + * @own_name: an unique DBus name with "org.mpris.MediaPlayer2." prefix + * @identity: a media player friendly name + * @desktop_entry: (nullable): desktop entry filename + * + * Creates a new #ClapperMpris instance. + * + * Returns: (transfer full): a new #ClapperMpris instance. + */ +ClapperMpris * +clapper_mpris_new (const gchar *own_name, const gchar *identity, + const gchar *desktop_entry) +{ + ClapperMpris *mpris; + + mpris = g_object_new (CLAPPER_TYPE_MPRIS, + "own-name", own_name, + "identity", identity, + "desktop-entry", desktop_entry, + NULL); + gst_object_ref_sink (mpris); + + return mpris; +} + +/** + * clapper_mpris_set_queue_controllable: + * @mpris: a #ClapperMpris + * @controllable: if #ClapperQueue should be controllable + * + * Set whether remote MPRIS clients can control #ClapperQueue. + * + * This includes ability to open new URIs, adding/removing + * items from the queue and selecting current item for + * playback remotely using MPRIS interface. + * + * You probably want to keep this disabled if your application + * is supposed to manage what is played now and not MPRIS client. + */ +void +clapper_mpris_set_queue_controllable (ClapperMpris *self, gboolean controllable) +{ + gboolean prev_controllable; + + g_return_if_fail (CLAPPER_IS_MPRIS (self)); + + prev_controllable = (gboolean) g_atomic_int_exchange (&self->queue_controllable, (gint) controllable); + + if (prev_controllable != controllable) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_QUEUE_CONTROLLABLE]); +} + +/** + * clapper_mpris_get_queue_controllable: + * @mpris: a #ClapperMpris + * + * Get whether remote `MPRIS` clients can control [class@Clapper.Queue]. + * + * Returns: %TRUE if control over #ClapperQueue is allowed, %FALSE otherwise. + */ +gboolean +clapper_mpris_get_queue_controllable (ClapperMpris *self) +{ + g_return_val_if_fail (CLAPPER_IS_MPRIS (self), FALSE); + + return (gboolean) g_atomic_int_get (&self->queue_controllable); +} + +/** + * clapper_mpris_set_fallback_art_url: + * @mpris: a #ClapperMpris + * @art_url: (nullable): an art URL + * + * Set fallback artwork to show when media does not provide one. + */ +void +clapper_mpris_set_fallback_art_url (ClapperMpris *self, const gchar *art_url) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_MPRIS (self)); + + GST_OBJECT_LOCK (self); + changed = g_set_str (&self->fallback_art_url, art_url); + GST_OBJECT_UNLOCK (self); + + if (changed) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_FALLBACK_ART_URL]); +} + +/** + * clapper_mpris_get_fallback_art_url: + * @mpris: a #ClapperMpris + * + * Get fallback art URL earlier set by user. + * + * Returns: (transfer full) (nullable): fallback art URL. + */ +gchar * +clapper_mpris_get_fallback_art_url (ClapperMpris *self) +{ + gchar *art_url; + + g_return_val_if_fail (CLAPPER_IS_MPRIS (self), NULL); + + GST_OBJECT_LOCK (self); + art_url = g_strdup (self->fallback_art_url); + GST_OBJECT_UNLOCK (self); + + return art_url; +} + +static void +clapper_mpris_init (ClapperMpris *self) +{ + self->base_skeleton = clapper_mpris_media_player2_skeleton_new (); + self->player_skeleton = clapper_mpris_media_player2_player_skeleton_new (); + self->tracks_skeleton = clapper_mpris_media_player2_track_list_skeleton_new (); + + self->tracks = g_ptr_array_new_with_free_func ((GDestroyNotify) clapper_mpris_track_free); + + g_atomic_int_set (&self->queue_controllable, (gint) DEFAULT_QUEUE_CONTROLLABLE); + + g_signal_connect (self->player_skeleton, "handle-open-uri", + G_CALLBACK (_handle_open_uri_cb), self); + g_signal_connect (self->player_skeleton, "handle-play", + G_CALLBACK (_handle_play_cb), self); + g_signal_connect (self->player_skeleton, "handle-pause", + G_CALLBACK (_handle_pause_cb), self); + g_signal_connect (self->player_skeleton, "handle-play-pause", + G_CALLBACK (_handle_play_pause_cb), self); + g_signal_connect (self->player_skeleton, "handle-stop", + G_CALLBACK (_handle_stop_cb), self); + g_signal_connect (self->player_skeleton, "handle-next", + G_CALLBACK (_handle_next_cb), self); + g_signal_connect (self->player_skeleton, "handle-previous", + G_CALLBACK (_handle_previous_cb), self); + g_signal_connect (self->player_skeleton, "handle-seek", + G_CALLBACK (_handle_seek_cb), self); + g_signal_connect (self->player_skeleton, "handle-set-position", + G_CALLBACK (_handle_set_position_cb), self); + g_signal_connect (self->player_skeleton, "notify::rate", + G_CALLBACK (_handle_rate_notify_cb), self); + g_signal_connect (self->player_skeleton, "notify::volume", + G_CALLBACK (_handle_volume_notify_cb), self); + g_signal_connect (self->player_skeleton, "notify::loop-status", + G_CALLBACK (_handle_loop_status_notify_cb), self); + g_signal_connect (self->player_skeleton, "notify::shuffle", + G_CALLBACK (_handle_shuffle_notify_cb), self); + + g_signal_connect (self->tracks_skeleton, "handle-get-tracks-metadata", + G_CALLBACK (_handle_get_tracks_metadata_cb), self); + g_signal_connect (self->tracks_skeleton, "handle-add-track", + G_CALLBACK (_handle_add_track_cb), self); + g_signal_connect (self->tracks_skeleton, "handle-remove-track", + G_CALLBACK (_handle_remove_track_cb), self); + g_signal_connect (self->tracks_skeleton, "handle-go-to", + G_CALLBACK (_handle_go_to_cb), self); +} + +static void +clapper_mpris_finalize (GObject *object) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (object); + + g_object_unref (self->base_skeleton); + g_object_unref (self->player_skeleton); + g_object_unref (self->tracks_skeleton); + + self->current_track = NULL; + g_ptr_array_unref (self->tracks); + + g_free (self->own_name); + g_free (self->identity); + g_free (self->desktop_entry); + g_free (self->fallback_art_url); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_mpris_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (object); + + switch (prop_id) { + case PROP_OWN_NAME: + self->own_name = g_value_dup_string (value); + break; + case PROP_IDENTITY: + self->identity = g_value_dup_string (value); + break; + case PROP_DESKTOP_ENTRY: + self->desktop_entry = g_value_dup_string (value); + break; + case PROP_QUEUE_CONTROLLABLE: + clapper_mpris_set_queue_controllable (self, g_value_get_boolean (value)); + break; + case PROP_FALLBACK_ART_URL: + clapper_mpris_set_fallback_art_url (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_mpris_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperMpris *self = CLAPPER_MPRIS_CAST (object); + + switch (prop_id) { + case PROP_OWN_NAME: + g_value_set_string (value, self->own_name); + break; + case PROP_IDENTITY: + g_value_set_string (value, self->identity); + break; + case PROP_DESKTOP_ENTRY: + g_value_set_string (value, self->desktop_entry); + break; + case PROP_QUEUE_CONTROLLABLE: + g_value_set_boolean (value, clapper_mpris_get_queue_controllable (self)); + break; + case PROP_FALLBACK_ART_URL: + g_value_take_string (value, clapper_mpris_get_fallback_art_url (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_mpris_class_init (ClapperMprisClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperFeatureClass *feature_class = (ClapperFeatureClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappermpris", 0, + "Clapper Mpris"); + + gobject_class->get_property = clapper_mpris_get_property; + gobject_class->set_property = clapper_mpris_set_property; + gobject_class->finalize = clapper_mpris_finalize; + + feature_class->prepare = clapper_mpris_prepare; + feature_class->unprepare = clapper_mpris_unprepare; + feature_class->property_changed = clapper_mpris_property_changed; + feature_class->state_changed = clapper_mpris_state_changed; + feature_class->position_changed = clapper_mpris_position_changed; + feature_class->speed_changed = clapper_mpris_speed_changed; + feature_class->volume_changed = clapper_mpris_volume_changed; + feature_class->played_item_changed = clapper_mpris_played_item_changed; + feature_class->item_updated = clapper_mpris_item_updated; + feature_class->queue_item_added = clapper_mpris_queue_item_added; + feature_class->queue_item_removed = clapper_mpris_queue_item_removed; + feature_class->queue_item_repositioned = clapper_mpris_queue_item_repositioned; + feature_class->queue_cleared = clapper_mpris_queue_cleared; + feature_class->queue_progression_changed = clapper_mpris_queue_progression_changed; + + /** + * ClapperMpris:own-name: + * + * DBus name to own on connection. + * + * Must be written as a reverse DNS format starting with "org.mpris.MediaPlayer2." prefix. + * Each #ClapperMpris instance running on the same system must have an unique. + * + * Example: "org.mpris.MediaPlayer2.MyPlayer1" + */ + param_specs[PROP_OWN_NAME] = g_param_spec_string ("own-name", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMpris:identity: + * + * A friendly name to identify the media player. + * + * Example: "My Player" + */ + param_specs[PROP_IDENTITY] = g_param_spec_string ("identity", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMpris:desktop-entry: + * + * The basename of an installed .desktop file with the ".desktop" extension stripped. + */ + param_specs[PROP_DESKTOP_ENTRY] = g_param_spec_string ("desktop-entry", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMpris:queue-controllable: + * + * Whether remote MPRIS clients can control #ClapperQueue. + */ + param_specs[PROP_QUEUE_CONTROLLABLE] = g_param_spec_boolean ("queue-controllable", + NULL, NULL, DEFAULT_QUEUE_CONTROLLABLE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperMpris:fallback-art-url: + * + * Fallback artwork to show when media does not provide one. + */ + param_specs[PROP_FALLBACK_ART_URL] = g_param_spec_string ("fallback-art-url", + NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/features/mpris/clapper-mpris.h b/src/lib/clapper/features/mpris/clapper-mpris.h new file mode 100644 index 00000000..1a34f2cb --- /dev/null +++ b/src/lib/clapper/features/mpris/clapper-mpris.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_MPRIS (clapper_mpris_get_type()) +#define CLAPPER_MPRIS_CAST(obj) ((ClapperMpris *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperMpris, clapper_mpris, CLAPPER, MPRIS, ClapperFeature) + +ClapperMpris * clapper_mpris_new (const gchar *own_name, const gchar *identity, const gchar *desktop_entry); + +void clapper_mpris_set_queue_controllable (ClapperMpris *mpris, gboolean controllable); + +gboolean clapper_mpris_get_queue_controllable (ClapperMpris *mpris); + +void clapper_mpris_set_fallback_art_url (ClapperMpris *mpris, const gchar *art_url); + +gchar * clapper_mpris_get_fallback_art_url (ClapperMpris *mpris); + +G_END_DECLS diff --git a/src/lib/clapper/features/mpris/meson.build b/src/lib/clapper/features/mpris/meson.build new file mode 100644 index 00000000..383b8e1f --- /dev/null +++ b/src/lib/clapper/features/mpris/meson.build @@ -0,0 +1,48 @@ +feature_option = get_option(feature_name) + +if feature_option.disabled() + subdir_done() +endif + +# Known OSes that can support our MPRIS implementation +os_supported = ['linux'].contains(host_machine.system()) + +if not os_supported + if feature_option.enabled() + error('@0@ feature was enabled, but OS is not supported by it'.format(feature_name)) + endif + subdir_done() +endif + +feature_deps = [ + dependency('gio-unix-2.0', version: glib_req, required: false), +] +foreach dep : feature_deps + if not dep.found() + if feature_option.enabled() + error('@0@ feature was enabled, but required dependencies were not found'.format(feature_name)) + endif + subdir_done() + endif +endforeach + +clapper_mpris_gdbus = gnome.gdbus_codegen('clapper-mpris-gdbus', + sources: 'clapper-mpris-gdbus.xml', + interface_prefix: 'org.mpris.', + namespace: 'ClapperMpris', +) +clapper_features_headers += [ + 'features/mpris/clapper-mpris.h', +] +clapper_features_sources += [ + 'features/mpris/clapper-mpris.c', +] +clapper_features_sources_internal += [ + clapper_mpris_gdbus, +] +clapper_features_deps += feature_deps + +install_headers('clapper-mpris.h', + install_dir: join_paths(clapper_headers_dir, 'features', 'mpris'), +) +clapper_available_features += feature_name diff --git a/src/lib/clapper/features/server/clapper-server-actions-private.h b/src/lib/clapper/features/server/clapper-server-actions-private.h new file mode 100644 index 00000000..ebf61077 --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-actions-private.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "clapper-enums.h" + +G_BEGIN_DECLS + +typedef enum +{ + CLAPPER_SERVER_ACTION_INVALID = 0, + CLAPPER_SERVER_ACTION_TOGGLE_PLAY, + CLAPPER_SERVER_ACTION_PLAY, + CLAPPER_SERVER_ACTION_PAUSE, + CLAPPER_SERVER_ACTION_STOP, + CLAPPER_SERVER_ACTION_SEEK, + CLAPPER_SERVER_ACTION_SET_SPEED, + CLAPPER_SERVER_ACTION_SET_VOLUME, + CLAPPER_SERVER_ACTION_SET_MUTE, + CLAPPER_SERVER_ACTION_SET_PROGRESSION, + CLAPPER_SERVER_ACTION_ADD, + CLAPPER_SERVER_ACTION_INSERT, + CLAPPER_SERVER_ACTION_SELECT, + CLAPPER_SERVER_ACTION_REMOVE, + CLAPPER_SERVER_ACTION_CLEAR +} ClapperServerAction; + +G_GNUC_INTERNAL +ClapperServerAction clapper_server_actions_get_action (const gchar *text); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_seek (const gchar *text, gdouble *position); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_set_speed (const gchar *text, gdouble *speed); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_set_volume (const gchar *text, gdouble *volume); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_set_mute (const gchar *text, gboolean *mute); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_set_progression (const gchar *text, ClapperQueueProgressionMode *mode); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_add (const gchar *text, const gchar **uri); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_insert (const gchar *text, gchar **uri, guint *after_id); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_select (const gchar *text, guint *id); + +G_GNUC_INTERNAL +gboolean clapper_server_actions_parse_remove (const gchar *text, guint *id); + +G_END_DECLS diff --git a/src/lib/clapper/features/server/clapper-server-actions.c b/src/lib/clapper/features/server/clapper-server-actions.c new file mode 100644 index 00000000..927a7280 --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-actions.c @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include + +#include "clapper-server-actions-private.h" +#include "clapper-server-names-private.h" +#include "clapper-enums.h" + +inline ClapperServerAction +clapper_server_actions_get_action (const gchar *text) +{ + /* Actions without arg(s) */ + if (strcmp (text, "toggle_play") == 0) + return CLAPPER_SERVER_ACTION_TOGGLE_PLAY; + if (strcmp (text, "play") == 0) + return CLAPPER_SERVER_ACTION_PLAY; + if (strcmp (text, "pause") == 0) + return CLAPPER_SERVER_ACTION_PAUSE; + if (strcmp (text, "stop") == 0) + return CLAPPER_SERVER_ACTION_STOP; + if (strcmp (text, "clear") == 0) + return CLAPPER_SERVER_ACTION_CLEAR; + + /* Actions followed by space and arg(s) */ + if (g_str_has_prefix (text, "seek ")) + return CLAPPER_SERVER_ACTION_SEEK; + if (g_str_has_prefix (text, "set_speed ")) + return CLAPPER_SERVER_ACTION_SET_SPEED; + if (g_str_has_prefix (text, "set_volume ")) + return CLAPPER_SERVER_ACTION_SET_VOLUME; + if (g_str_has_prefix (text, "set_mute ")) + return CLAPPER_SERVER_ACTION_SET_MUTE; + if (g_str_has_prefix (text, "set_progression ")) + return CLAPPER_SERVER_ACTION_SET_PROGRESSION; + if (g_str_has_prefix (text, "add ")) + return CLAPPER_SERVER_ACTION_ADD; + if (g_str_has_prefix (text, "insert ")) + return CLAPPER_SERVER_ACTION_INSERT; + if (g_str_has_prefix (text, "select ")) + return CLAPPER_SERVER_ACTION_SELECT; + if (g_str_has_prefix (text, "remove ")) + return CLAPPER_SERVER_ACTION_REMOVE; + + return CLAPPER_SERVER_ACTION_INVALID; +} + +static inline gboolean +_string_is_number (const gchar *string, gboolean decimal) +{ + guint i; + + for (i = 0; string[i] != '\0'; ++i) { + if (!g_ascii_isdigit (string[i])) { + if (decimal && string[i] == '.') + continue; + + return FALSE; + } + } + + return (i > 0); +} + +static gboolean +_parse_uint (const gchar *text, guint *val) +{ + gint64 tmp_val; + + if (!_string_is_number (text, FALSE)) + return FALSE; + + tmp_val = g_ascii_strtoll (text, NULL, 10); + + /* guint overflow check */ + if (tmp_val < 0 || tmp_val > G_MAXUINT) + return FALSE; + + *val = (guint) tmp_val; + + return TRUE; +} + +static gboolean +_parse_double (const gchar *text, gdouble *val) +{ + if (!_string_is_number (text, TRUE)) + return FALSE; + + *val = g_ascii_strtod (text, NULL); + + return TRUE; +} + +static gboolean +_parse_boolean (const gchar *text, gboolean *val) +{ + gboolean res; + + if ((res = (strcmp (text, "true") == 0))) + *val = TRUE; + else if ((res = (strcmp (text, "false") == 0))) + *val = FALSE; + + return res; +} + +inline gboolean +clapper_server_actions_parse_seek (const gchar *text, gdouble *position) +{ + /* "seek" + whitespace = 5 */ + if (!_parse_double (text + 5, position)) + return FALSE; + + return (*position >= 0); +} + +inline gboolean +clapper_server_actions_parse_set_speed (const gchar *text, gdouble *speed) +{ + /* "set_speed" + whitespace = 10 */ + return _parse_double (text + 10, speed); +} + +inline gboolean +clapper_server_actions_parse_set_volume (const gchar *text, gdouble *volume) +{ + /* "set_volume" + whitespace = 11 */ + if (!_parse_double (text + 11, volume)) + return FALSE; + + if (*volume <= 0 || *volume > 2.0) + return FALSE; + + return TRUE; +} + +inline gboolean +clapper_server_actions_parse_set_mute (const gchar *text, gboolean *mute) +{ + /* "set_mute" + whitespace = 9 */ + return _parse_boolean (text + 9, mute); +} + +inline gboolean +clapper_server_actions_parse_set_progression (const gchar *text, ClapperQueueProgressionMode *mode) +{ + gboolean res; + + /* "set_progression" + whitespace = 16 */ + text += 16; + + if ((res = (strcmp (text, CLAPPER_SERVER_QUEUE_PROGRESSION_NONE) == 0))) + *mode = CLAPPER_QUEUE_PROGRESSION_NONE; + else if ((res = (strcmp (text, CLAPPER_SERVER_QUEUE_PROGRESSION_CONSECUTIVE) == 0))) + *mode = CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE; + else if ((res = (strcmp (text, CLAPPER_SERVER_QUEUE_PROGRESSION_REPEAT_ITEM) == 0))) + *mode = CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM; + else if ((res = (strcmp (text, CLAPPER_SERVER_QUEUE_PROGRESSION_CAROUSEL) == 0))) + *mode = CLAPPER_QUEUE_PROGRESSION_CAROUSEL; + else if ((res = (strcmp (text, CLAPPER_SERVER_QUEUE_PROGRESSION_SHUFFLE) == 0))) + *mode = CLAPPER_QUEUE_PROGRESSION_SHUFFLE; + + return res; +} + +inline gboolean +clapper_server_actions_parse_add (const gchar *text, const gchar **uri) +{ + /* "add" + whitespace = 4 */ + text += 4; + + /* No more spaces allowed */ + if (strchr (text, ' ') != NULL) + return FALSE; + + if (!gst_uri_is_valid (text)) + return FALSE; + + *uri = text; + + return TRUE; +} + +inline gboolean +clapper_server_actions_parse_insert (const gchar *text, gchar **uri, guint *after_id) +{ + gchar **data; + gboolean res; + + /* "insert" + whitespace = 7 */ + text += 7; + data = g_strsplit (text, " ", 2); + + if ((res = (g_strv_length (data) == 2 + && gst_uri_is_valid (data[0]) + && _parse_uint (data[1], after_id)))) { + *uri = g_strdup (data[0]); + } + + g_strfreev (data); + + return res; +} + +inline gboolean +clapper_server_actions_parse_select (const gchar *text, guint *id) +{ + /* "select" + whitespace = 7 */ + return _parse_uint (text + 7, id); +} + +inline gboolean +clapper_server_actions_parse_remove (const gchar *text, guint *id) +{ + /* "remove" + whitespace = 7 */ + return _parse_uint (text + 7, id); +} diff --git a/src/lib/clapper/features/server/clapper-server-json-private.h b/src/lib/clapper/features/server/clapper-server-json-private.h new file mode 100644 index 00000000..9cfc98e5 --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-json-private.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "clapper-server.h" +#include "clapper-media-item.h" + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +gchar * clapper_server_json_build_complete (ClapperServer *server, ClapperMediaItem *played_item, guint played_index, GPtrArray *items); + +G_END_DECLS diff --git a/src/lib/clapper/features/server/clapper-server-json.c b/src/lib/clapper/features/server/clapper-server-json.c new file mode 100644 index 00000000..0f144296 --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-json.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "clapper-server-json-private.h" +#include "clapper-server-names-private.h" +#include "clapper-player.h" +#include "clapper-queue.h" + +#define CLAPPER_SERVER_JSON_BUILD(dest, ...) { \ + GString *_json = g_string_new ("{"); \ + __VA_ARGS__ \ + g_string_append (_json, "}"); \ + *dest = g_string_free (_json, FALSE); } + +#define _JSON_AUTO_COMMA \ + if (_json->str[_json->len - 1] != '{' \ + && _json->str[_json->len - 1] != '[') \ + g_string_append (_json, ","); + +#define _ADD_KEY_VAL_BOOLEAN(key, val) \ + _JSON_AUTO_COMMA \ + g_string_append_printf (_json, "\"%s\":%s", key, (val) ? "true" : "false"); + +#define _ADD_KEY_VAL_UINT(key, val) \ + _JSON_AUTO_COMMA \ + g_string_append_printf (_json, "\"%s\":%" G_GUINT64_FORMAT, key, (guint64) val); + +#define _ADD_KEY_VAL_DOUBLE(key, val) \ + _JSON_AUTO_COMMA \ + g_string_append_printf (_json, "\"%s\":%.2lf", key, (gdouble) val); + +#define _ADD_KEY_VAL_STRING(key, val) \ + _JSON_AUTO_COMMA \ + if (G_UNLIKELY (val == NULL)) \ + g_string_append_printf (_json, "\"%s\":null", key); \ + else \ + g_string_append_printf (_json, "\"%s\":\"%s\"", key, val); + +#define _ADD_KEY_VAL_STRING_TAKE(key, val) \ + _ADD_KEY_VAL_STRING(key, val) \ + g_free (val); + +#define _ADD_VAL_STRING(val) \ + _JSON_AUTO_COMMA \ + if (G_UNLIKELY (val == NULL)) \ + g_string_append (_json, "null"); \ + else \ + g_string_append_printf (_json, "\"%s\"", val); + +#define _ADD_VAL_STRING_TAKE(val) \ + _ADD_VAL_STRING(val) \ + g_free (val); + +#define _ADD_OBJECT(...) \ + _JSON_AUTO_COMMA \ + g_string_append (_json, "{"); \ + __VA_ARGS__ \ + g_string_append (_json, "}"); + +#define _ADD_NAMED_OBJECT(name, ...) \ + _JSON_AUTO_COMMA \ + g_string_append_printf (_json, "\"%s\":{", name); \ + __VA_ARGS__ \ + g_string_append (_json, "}"); + +#define _ADD_NAMED_ARRAY(name, ...) \ + _JSON_AUTO_COMMA \ + g_string_append_printf (_json, "\"%s\":[", name); \ + __VA_ARGS__ \ + g_string_append (_json, "]"); + +static inline void +clapper_server_json_escape_string (gchar **string) +{ + gchar *dest, *src = *string; + guint i, offset = 0; + + for (i = 0; src[i] != '\0'; ++i) { + switch (src[i]) { + case '\"': + offset++; + default: + break; + } + } + + /* Nothing to escape, leave string unchaged */ + if (offset == 0) + return; + + /* Previous length + n_escapes + term */ + dest = g_new (gchar, i + offset + 1); + offset = 0; + + for (i = 0; src[i] != '\0'; ++i) { + switch (src[i]) { + case '\"': + dest[i + offset] = '\\'; + dest[i + offset + 1] = '\"'; + offset++; + break; + default: + dest[i + offset] = src[i]; + break; + } + } + dest[i + offset] = '\0'; + + g_free (*string); + *string = dest; +} + +gchar * +clapper_server_json_build_complete (ClapperServer *server, ClapperMediaItem *played_item, + guint played_index, GPtrArray *items) +{ + ClapperPlayer *player; + gchar *data = NULL; + + player = CLAPPER_PLAYER_CAST (gst_object_get_parent (GST_OBJECT_CAST (server))); + + if (G_UNLIKELY (player == NULL)) + return NULL; + + CLAPPER_SERVER_JSON_BUILD (&data, { + switch (clapper_player_get_state (player)) { + case CLAPPER_PLAYER_STATE_PLAYING: + _ADD_KEY_VAL_STRING ("state", CLAPPER_SERVER_PLAYER_STATE_PLAYING); + break; + case CLAPPER_PLAYER_STATE_PAUSED: + _ADD_KEY_VAL_STRING ("state", CLAPPER_SERVER_PLAYER_STATE_PAUSED); + break; + case CLAPPER_PLAYER_STATE_BUFFERING: + _ADD_KEY_VAL_STRING ("state", CLAPPER_SERVER_PLAYER_STATE_BUFFERING); + break; + case CLAPPER_PLAYER_STATE_STOPPED: + _ADD_KEY_VAL_STRING ("state", CLAPPER_SERVER_PLAYER_STATE_STOPPED); + break; + default: + g_assert_not_reached (); + break; + } + + _ADD_KEY_VAL_UINT ("position", clapper_player_get_position (player)); + _ADD_KEY_VAL_DOUBLE ("speed", clapper_player_get_speed (player)); + _ADD_KEY_VAL_DOUBLE ("volume", clapper_player_get_volume (player)); + _ADD_KEY_VAL_BOOLEAN ("mute", clapper_player_get_mute (player)); + + _ADD_NAMED_OBJECT ("queue", { + ClapperQueue *queue = clapper_player_get_queue (player); + + _ADD_KEY_VAL_BOOLEAN ("controllable", clapper_server_get_queue_controllable (server)); + _ADD_KEY_VAL_UINT ("played_index", played_index); + _ADD_KEY_VAL_UINT ("n_items", items->len); + + switch (clapper_queue_get_progression_mode (queue)) { + case CLAPPER_QUEUE_PROGRESSION_NONE: + _ADD_KEY_VAL_STRING ("progression", CLAPPER_SERVER_QUEUE_PROGRESSION_NONE); + break; + case CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE: + _ADD_KEY_VAL_STRING ("progression", CLAPPER_SERVER_QUEUE_PROGRESSION_CONSECUTIVE); + break; + case CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM: + _ADD_KEY_VAL_STRING ("progression", CLAPPER_SERVER_QUEUE_PROGRESSION_REPEAT_ITEM); + break; + case CLAPPER_QUEUE_PROGRESSION_CAROUSEL: + _ADD_KEY_VAL_STRING ("progression", CLAPPER_SERVER_QUEUE_PROGRESSION_CAROUSEL); + break; + case CLAPPER_QUEUE_PROGRESSION_SHUFFLE: + _ADD_KEY_VAL_STRING ("progression", CLAPPER_SERVER_QUEUE_PROGRESSION_SHUFFLE); + break; + default: + g_assert_not_reached (); + break; + } + + _ADD_NAMED_ARRAY ("items", { + guint i; + + for (i = 0; i < items->len; ++i) { + _ADD_OBJECT ({ + ClapperMediaItem *item = (ClapperMediaItem *) g_ptr_array_index (items, i); + gchar *title = clapper_media_item_get_title (item); + + if (title) + clapper_server_json_escape_string (&title); + + _ADD_KEY_VAL_UINT ("id", clapper_media_item_get_id (item)); + _ADD_KEY_VAL_STRING_TAKE ("title", title); + _ADD_KEY_VAL_UINT ("duration", clapper_media_item_get_duration (item)); + + /* TODO: Add more info per item (including timeline markers) */ + }); + } + }); + }); + }); + + gst_object_unref (player); + + return data; +} diff --git a/src/lib/clapper/features/server/clapper-server-mdns-private.h b/src/lib/clapper/features/server/clapper-server-mdns-private.h new file mode 100644 index 00000000..db04145c --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-mdns-private.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +void clapper_server_mdns_debug_init (void); + +G_GNUC_INTERNAL +void clapper_server_mdns_serve (gchar *name, guint port); + +G_GNUC_INTERNAL +void clapper_server_mdns_remove (guint port); + +G_END_DECLS diff --git a/src/lib/clapper/features/server/clapper-server-mdns.c b/src/lib/clapper/features/server/clapper-server-mdns.c new file mode 100644 index 00000000..c4137c6e --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-mdns.c @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include + +#include "clapper-server-mdns-private.h" + +#define CLAPPER_SERVER_MDNS_SERVICE "_clapper._tcp.local" + +#define N_RESP 4 + +#define PTR_INDEX(i) (i * N_RESP) +#define TXT_INDEX(i) (PTR_INDEX(i) + 1) +#define SRV_INDEX(i) (PTR_INDEX(i) + 2) +#define A_AAAA_INDEX(i) (PTR_INDEX(i) + 3) + +typedef struct +{ + gchar *name; + gchar *service_link; + guint port; +} ClapperServerMdnsEntry; + +typedef struct +{ + GPtrArray *entries; + GPtrArray *pending_entries; +} ClapperServerMdns; + +#define GST_CAT_DEFAULT clapper_server_mdns_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +static ClapperServerMdns *mdns = NULL; +static GCond mdns_cond; +static GMutex mdns_lock; + +void +clapper_server_mdns_debug_init (void) +{ + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperservermdns", + GST_DEBUG_FG_RED, "Clapper Server MDNS"); +} + +static void +clapper_server_mdns_entry_free (ClapperServerMdnsEntry *entry) +{ + GST_TRACE ("Freeing MDNS entry: %p", entry); + + g_free (entry->name); + g_free (entry->service_link); + g_free (entry); +} + +static void +clapper_server_mdns_remove_port (GPtrArray *entries, guint port) +{ + guint i; + + for (i = 0; i < entries->len; ++i) { + ClapperServerMdnsEntry *entry = g_ptr_array_index (entries, i); + + if (entry->port == port) { + GST_TRACE ("Removing entry with port: %u", port); + g_ptr_array_remove_index (entries, i); + break; + } + } +} + +static inline void +_send_entries (struct mdns_ctx *ctx, const struct sockaddr *addr, + enum mdns_announce_type type, GPtrArray *entries) +{ + const guint n_answers = N_RESP * entries->len; + gchar domain_name[32]; + guint i; + + struct rr_entry *answers = g_alloca0 (sizeof (struct rr_entry) * n_answers); + struct mdns_hdr hdr = { 0, }; + + hdr.flags |= FLAG_QR; + hdr.flags |= FLAG_AA; + hdr.num_ans_rr = n_answers; + + g_snprintf (domain_name, sizeof (domain_name), "%s.local", g_get_host_name ()); + + for (i = 0; i < entries->len; ++i) { + ClapperServerMdnsEntry *entry = g_ptr_array_index (entries, i); + + GST_LOG ("Preparing answers for MDNS query, service: \"%s\"" + ", domain: \"%s\", link: \"%s\"", + CLAPPER_SERVER_MDNS_SERVICE, domain_name, entry->service_link); + + answers[PTR_INDEX(i)] = (struct rr_entry) { + .type = RR_PTR, + .name = (char *) CLAPPER_SERVER_MDNS_SERVICE, + .data.PTR.domain = entry->service_link, + .rr_class = RR_IN, + .msbit = 1, + .ttl = (type == MDNS_ANNOUNCE_GOODBYE) ? 0 : 120, + .next = &answers[TXT_INDEX(i)] + }; + + answers[TXT_INDEX(i)] = (struct rr_entry) { + .type = RR_TXT, + .name = entry->service_link, + .rr_class = RR_IN, + .msbit = 1, + .ttl = (type == MDNS_ANNOUNCE_GOODBYE) ? 0 : 120, + .next = &answers[SRV_INDEX(i)] + }; + + answers[SRV_INDEX(i)] = (struct rr_entry) { + .type = RR_SRV, + .name = entry->service_link, + .data.SRV.port = entry->port, + .data.SRV.priority = 0, + .data.SRV.weight = 0, + .data.SRV.target = domain_name, + .rr_class = RR_IN, + .msbit = 1, + .ttl = (type == MDNS_ANNOUNCE_GOODBYE) ? 0 : 120, + .next = &answers[A_AAAA_INDEX(i)] + }; + + answers[A_AAAA_INDEX(i)] = (struct rr_entry) { + .name = domain_name, + .rr_class = RR_IN, + .msbit = 1, + .ttl = (type == MDNS_ANNOUNCE_GOODBYE) ? 0 : 120, + .next = (i + 1 < entries->len) ? &answers[PTR_INDEX(i + 1)] : NULL + }; + + if (addr->sa_family == AF_INET) { + answers[A_AAAA_INDEX(i)].type = RR_A; + memcpy (&answers[A_AAAA_INDEX(i)].data.A.addr, + &((struct sockaddr_in *) addr)->sin_addr, + sizeof (answers[A_AAAA_INDEX(i)].data.A.addr)); + } else { + answers[A_AAAA_INDEX(i)].type = RR_AAAA; + memcpy(&answers[A_AAAA_INDEX(i)].data.AAAA.addr, + &((struct sockaddr_in6 *) addr)->sin6_addr, + sizeof (answers[A_AAAA_INDEX(i)].data.AAAA.addr)); + } + + GST_LOG ("Prepared %u/%u bunches of answers", i + 1, entries->len); + } + + /* Needs to still have lock here, as pointers + * in answers are simply assigned from entries */ + GST_LOG ("Sending all answers"); + mdns_entries_send (ctx, &hdr, answers); +} + +static void +_mdns_cb (struct mdns_ctx *ctx, const struct sockaddr *addr, + const char* service, enum mdns_announce_type type) +{ + if (service && strcmp (service, CLAPPER_SERVER_MDNS_SERVICE) != 0) + return; + + g_mutex_lock (&mdns_lock); + + switch (type) { + case MDNS_ANNOUNCE_INITIAL: + if (mdns->pending_entries->len > 0) { + GST_LOG ("Handling announcement type: INITIAL"); + _send_entries (ctx, addr, type, mdns->pending_entries); + + /* Move to entries after initial announcement */ + while (mdns->pending_entries->len > 0) { + ClapperServerMdnsEntry *entry; + + /* MDNS advertises entries in reverse order */ + entry = g_ptr_array_steal_index (mdns->pending_entries, 0); + g_ptr_array_insert (mdns->entries, 0, entry); + } + } + break; + case MDNS_ANNOUNCE_RESPONSE: + case MDNS_ANNOUNCE_GOODBYE: + if (mdns->entries->len > 0) { + GST_LOG ("Handling announcement type: %s", + (type == MDNS_ANNOUNCE_RESPONSE) ? "RESPONSE" : "GOODBYE"); + _send_entries (ctx, addr, type, mdns->entries); + } + break; + default: + break; + } + + g_mutex_unlock (&mdns_lock); +} + +static gboolean +mdns_stop_cb (struct mdns_ctx *ctx) +{ + gboolean announce; + + g_mutex_lock (&mdns_lock); + + if (mdns->entries->len == 0 + && mdns->pending_entries->len == 0) { + g_mutex_unlock (&mdns_lock); + return TRUE; + } + + announce = (mdns->pending_entries->len > 0); + g_mutex_unlock (&mdns_lock); + + if (announce) + mdns_request_initial_announce (ctx, NULL); + + return FALSE; +} + +static gpointer +mdns_thread_func (gpointer user_data) +{ + struct mdns_ctx *ctx = NULL; + int resp; + char err_str[128]; + + GST_TRACE ("MDNS init"); + + if ((resp = mdns_init (&ctx, MDNS_ADDR_IPV4, MDNS_PORT)) < 0) { + mdns_strerror(resp, err_str, sizeof (err_str)); + GST_ERROR ("Could not initialize MDNS, reason: %s", err_str); + return NULL; + } + + mdns_announce (ctx, RR_PTR, (mdns_announce_callback) _mdns_cb, ctx); + + GST_DEBUG ("MDNS start"); +serve: + if ((resp = mdns_serve (ctx, (mdns_stop_func) mdns_stop_cb, ctx)) < 0) { + mdns_strerror(resp, err_str, sizeof (err_str)); + GST_ERROR ("Could start MDNS, reason: %s", err_str); + } + + g_mutex_lock (&mdns_lock); + + /* Can happen when stopped due to lack of entries, + * but entries were added afterwards */ + if (resp >= 0 && (mdns->entries->len > 0 + || mdns->pending_entries->len > 0)) { + g_mutex_unlock (&mdns_lock); + goto serve; + } + + /* No more going back now */ + GST_DEBUG ("MDNS stop"); + + /* Destroy with a lock, this ensures unbind + * of MDNS_PORT before doing "mdns_init" again */ + GST_TRACE ("MDNS destroy"); + mdns_destroy (ctx); + + GST_TRACE ("Freeing MDNS entries storage: %p", mdns); + g_ptr_array_unref (mdns->entries); + g_ptr_array_unref (mdns->pending_entries); + g_clear_pointer (&mdns, g_free); + + g_cond_broadcast (&mdns_cond); + + g_mutex_unlock (&mdns_lock); + + return NULL; +} + +void +clapper_server_mdns_serve (gchar *name, guint port) +{ + ClapperServerMdnsEntry *entry; + const gchar *prgname = g_get_prgname (); + gboolean stopped; + + entry = g_new (ClapperServerMdnsEntry, 1); + entry->name = name; + entry->service_link = g_strdup_printf ("%s %s %s.%s", + g_get_host_name (), (prgname) ? prgname : "clapperplayer", + entry->name, CLAPPER_SERVER_MDNS_SERVICE); + entry->port = port; + GST_TRACE ("Created MDNS entry: %p", entry); + + g_mutex_lock (&mdns_lock); + + if ((stopped = mdns == NULL)) { + mdns = g_new (ClapperServerMdns, 1); + mdns->entries = g_ptr_array_new_with_free_func ( + (GDestroyNotify) clapper_server_mdns_entry_free); + mdns->pending_entries = g_ptr_array_new_with_free_func ( + (GDestroyNotify) clapper_server_mdns_entry_free); + GST_TRACE ("Created MDNS entries storage: %p", mdns); + } + g_ptr_array_add (mdns->pending_entries, entry); + + g_mutex_unlock (&mdns_lock); + + if (stopped) { + GThread *thread; + GError *error = NULL; + + GST_DEBUG ("Starting MDNS service"); + thread = g_thread_try_new ("clapper-server-mdns", + (GThreadFunc) mdns_thread_func, NULL, &error); + + if (error) { + GST_ERROR ("Could not create MDNS thread, reason: %s", error->message); + g_error_free (error); + } else { + g_thread_unref (thread); + } + } +} + +void +clapper_server_mdns_remove (guint port) +{ + g_mutex_lock (&mdns_lock); + + clapper_server_mdns_remove_port (mdns->entries, port); + clapper_server_mdns_remove_port (mdns->pending_entries, port); + + if (mdns + && mdns->entries->len == 0 + && mdns->pending_entries->len == 0) { + GST_DEBUG ("MDNS is going to stop"); + + while (mdns != NULL) + g_cond_wait (&mdns_cond, &mdns_lock); + } + + g_mutex_unlock (&mdns_lock); +} diff --git a/src/lib/clapper/features/server/clapper-server-names-private.h b/src/lib/clapper/features/server/clapper-server-names-private.h new file mode 100644 index 00000000..287cfbb1 --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server-names-private.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +G_BEGIN_DECLS + +#define CLAPPER_SERVER_WS_EVENT_STATE "state" +#define CLAPPER_SERVER_WS_EVENT_POSITION "position" +#define CLAPPER_SERVER_WS_EVENT_SPEED "speed" +#define CLAPPER_SERVER_WS_EVENT_VOLUME "volume" +#define CLAPPER_SERVER_WS_EVENT_MUTED "muted" +#define CLAPPER_SERVER_WS_EVENT_UNMUTED "unmuted" +#define CLAPPER_SERVER_WS_EVENT_PLAYED_INDEX "played_index" +#define CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED "queue_changed" +#define CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION "queue_progression" + +#define CLAPPER_SERVER_PLAYER_STATE_STOPPED "stopped" +#define CLAPPER_SERVER_PLAYER_STATE_BUFFERING "buffering" +#define CLAPPER_SERVER_PLAYER_STATE_PAUSED "paused" +#define CLAPPER_SERVER_PLAYER_STATE_PLAYING "playing" + +#define CLAPPER_SERVER_QUEUE_PROGRESSION_NONE "none" +#define CLAPPER_SERVER_QUEUE_PROGRESSION_CONSECUTIVE "consecutive" +#define CLAPPER_SERVER_QUEUE_PROGRESSION_REPEAT_ITEM "repeat_item" +#define CLAPPER_SERVER_QUEUE_PROGRESSION_CAROUSEL "carousel" +#define CLAPPER_SERVER_QUEUE_PROGRESSION_SHUFFLE "shuffle" + +G_END_DECLS diff --git a/src/lib/clapper/features/server/clapper-server.c b/src/lib/clapper/features/server/clapper-server.c new file mode 100644 index 00000000..5eed0fa3 --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server.c @@ -0,0 +1,1182 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * ClapperServer: + * + * An optional Server feature to add to the player. + * + * #ClapperServer is a feature that hosts a local server + * providing an ability to both monitor and control playback + * through WebSocket messages and HTTP requests. + * + * Use [const@Clapper.HAVE_SERVER] macro to check if Clapper API + * was compiled with this feature. + */ + +#include +#include + +#include "clapper-server.h" +#include "clapper-server-names-private.h" +#include "clapper-server-json-private.h" +#include "clapper-server-actions-private.h" +#include "clapper-server-mdns-private.h" + +#include "clapper-enums.h" +#include "clapper-player.h" +#include "clapper-queue.h" +#include "clapper-media-item.h" +#include "clapper-utils-private.h" +#include "../shared/clapper-shared-utils-private.h" + +#define CLAPPER_SERVER_WS_EVENT_STATE_MAKE(s) (CLAPPER_SERVER_WS_EVENT_STATE " " s) +#define CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION_MAKE(s) (CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION " " s) + +#define DOUBLE_EVENT_MAX_SIZE 12 +#define UINT_EVENT_MAX_SIZE 24 + +#define PORT_MAX 65535 + +#define DEFAULT_ENABLED FALSE +#define DEFAULT_QUEUE_CONTROLLABLE FALSE + +#define GST_CAT_DEFAULT clapper_server_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +struct _ClapperServer +{ + ClapperFeature parent; + + SoupServer *server; + + GPtrArray *ws_connections; + + GPtrArray *items; + ClapperMediaItem *played_item; + guint played_index; + + guint position_uint; + + guint error_id; + guint running_id; + GSource *timeout_source; + + gint enabled; // atomic + gboolean running; // only changed from features thread + guint port; + guint current_port; + gint queue_controllable; // atomic +}; + +typedef struct +{ + ClapperServer *server; + GError *error; +} ClapperServerErrorData; + +enum +{ + PROP_0, + PROP_ENABLED, + PROP_RUNNING, + PROP_PORT, + PROP_CURRENT_PORT, + PROP_QUEUE_CONTROLLABLE, + PROP_LAST +}; + +enum +{ + SIGNAL_ERROR, + SIGNAL_LAST +}; + +#define parent_class clapper_server_parent_class +G_DEFINE_TYPE (ClapperServer, clapper_server, CLAPPER_TYPE_FEATURE); + +static GParamSpec *param_specs[PROP_LAST] = { NULL, }; +static guint signals[SIGNAL_LAST] = { 0, }; + +static ClapperServerErrorData * +clapper_server_error_data_new (ClapperServer *self, GError *error) +{ + ClapperServerErrorData *data = g_new (ClapperServerErrorData, 1); + + GST_TRACE ("Created server error data: %p", data); + + data->server = gst_object_ref (self); + data->error = error; + + return data; +} + +static void +clapper_server_error_data_free (ClapperServerErrorData *data) +{ + GST_TRACE ("Freeing server error data: %p", data); + + gst_object_unref (data->server); + g_clear_error (&data->error); + + g_free (data); +} + +static void +clapper_server_notify_port_and_running_on_main_idle (ClapperServer *self) +{ + GST_OBJECT_LOCK (self); + self->running_id = 0; + GST_OBJECT_UNLOCK (self); + + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_CURRENT_PORT]); + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_RUNNING]); +} + +static void +clapper_server_send_error_on_main_idle (ClapperServerErrorData *data) +{ + ClapperServer *self = data->server; + + GST_OBJECT_LOCK (self); + self->error_id = 0; + GST_OBJECT_UNLOCK (self); + + g_signal_emit (G_OBJECT (self), signals[SIGNAL_ERROR], 0, data->error); +} + +static inline void +_clear_delayed_queue_changed_timeout (ClapperServer *self) +{ + if (self->timeout_source) { + g_source_destroy (self->timeout_source); + g_clear_pointer (&self->timeout_source, g_source_unref); + } +} + +static inline guint +_find_current_port (ClapperServer *self) +{ + GSList *uris_list, *list; + guint found_port = 0; + + uris_list = soup_server_get_uris (self->server); + + for (list = uris_list; list != NULL; list = g_slist_next (list)) { + GUri *uri = list->data; + gint current_port = g_uri_get_port (uri); + + if (current_port > 0) { + found_port = current_port; + break; + } + } + + g_slist_free_full (uris_list, (GDestroyNotify) g_uri_unref); + + if (G_UNLIKELY (found_port == 0)) + GST_ERROR_OBJECT (self, "Could not determine server current port"); + + return found_port; +} + +static inline void +_start_server (ClapperServer *self) +{ + GError *error = NULL; + guint current_port; + + /* We only edit this from feature thread, + * so no lock needed here */ + if (self->running) + return; + + if (!soup_server_listen_all (self->server, + clapper_server_get_port (self), + SOUP_SERVER_LISTEN_IPV4_ONLY, + &error)) { + ClapperServerErrorData *data; + + GST_ERROR_OBJECT (self, "Error starting server: %s", + GST_STR_NULL (error->message)); + + data = clapper_server_error_data_new (self, error); + + GST_OBJECT_LOCK (self); + g_clear_handle_id (&self->error_id, g_source_remove); + self->error_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc) clapper_server_send_error_on_main_idle, data, + (GDestroyNotify) clapper_server_error_data_free); + GST_OBJECT_UNLOCK (self); + + return; + } + + current_port = _find_current_port (self); + GST_INFO_OBJECT (self, "Server started on port: %u", current_port); + + GST_OBJECT_LOCK (self); + + self->current_port = current_port; + self->running = TRUE; + + g_clear_handle_id (&self->running_id, g_source_remove); + self->running_id = g_idle_add_once ( + (GSourceOnceFunc) clapper_server_notify_port_and_running_on_main_idle, self); + + GST_OBJECT_UNLOCK (self); + + clapper_server_mdns_serve ( + gst_object_get_name (GST_OBJECT_CAST (self)), current_port); +} + +static inline void +_stop_server (ClapperServer *self) +{ + guint current_port; + + if (!self->running) + return; + + _clear_delayed_queue_changed_timeout (self); + + GST_OBJECT_LOCK (self); + + current_port = self->current_port; + self->current_port = 0; + self->running = FALSE; + + g_clear_handle_id (&self->running_id, g_source_remove); + self->running_id = g_idle_add_once ( + (GSourceOnceFunc) clapper_server_notify_port_and_running_on_main_idle, self); + + GST_OBJECT_UNLOCK (self); + + clapper_server_mdns_remove (current_port); + + /* Remove everyone */ + if (self->ws_connections->len > 0) + g_ptr_array_remove_range (self->ws_connections, 0, self->ws_connections->len); + + soup_server_disconnect (self->server); + GST_INFO_OBJECT (self, "Server stopped listening"); +} + +static void +_clear_stored_queue (ClapperServer *self) +{ + if (self->items->len > 0) + g_ptr_array_remove_range (self->items, 0, self->items->len); + + gst_clear_object (&self->played_item); + self->played_index = CLAPPER_QUEUE_INVALID_POSITION; +} + +static void +_ws_connection_message_cb (SoupWebsocketConnection *connection, + gint type, GBytes *message, ClapperServer *self) +{ + ClapperPlayer *player; + ClapperServerAction action; + const gchar *text; + + if (G_UNLIKELY (type != SOUP_WEBSOCKET_DATA_TEXT)) { + GST_WARNING_OBJECT (self, "Received WS message with non-text data!"); + return; + } + + text = g_bytes_get_data (message, NULL); + + if (G_UNLIKELY (text == NULL)) { + GST_WARNING_OBJECT (self, "Received WS message without any text!"); + return; + } + + action = clapper_server_actions_get_action (text); + + if (action == CLAPPER_SERVER_ACTION_INVALID) { + GST_INFO_OBJECT (self, "Ignoring WS message with invalid action text"); + return; + } + + player = CLAPPER_PLAYER_CAST (gst_object_get_parent (GST_OBJECT_CAST (self))); + + if (G_UNLIKELY (player == NULL)) + return; + + switch (action) { + case CLAPPER_SERVER_ACTION_TOGGLE_PLAY: + switch (clapper_player_get_state (player)) { + case CLAPPER_PLAYER_STATE_STOPPED: + case CLAPPER_PLAYER_STATE_PAUSED: + clapper_player_play (player); + break; + case CLAPPER_PLAYER_STATE_PLAYING: + clapper_player_pause (player); + break; + default: + break; + } + break; + case CLAPPER_SERVER_ACTION_PLAY: + clapper_player_play (player); + break; + case CLAPPER_SERVER_ACTION_PAUSE: + clapper_player_pause (player); + break; + case CLAPPER_SERVER_ACTION_STOP: + clapper_player_stop (player); + break; + case CLAPPER_SERVER_ACTION_SEEK:{ + gdouble position; + if (clapper_server_actions_parse_seek (text, &position)) + clapper_player_seek (player, position); + break; + } + case CLAPPER_SERVER_ACTION_SET_SPEED:{ + gdouble speed; + if (clapper_server_actions_parse_set_speed (text, &speed)) + clapper_player_set_speed (player, speed); + break; + } + case CLAPPER_SERVER_ACTION_SET_VOLUME:{ + gdouble volume; + if (clapper_server_actions_parse_set_volume (text, &volume)) + clapper_player_set_volume (player, volume); + break; + } + case CLAPPER_SERVER_ACTION_SET_MUTE:{ + gboolean mute; + if (clapper_server_actions_parse_set_mute (text, &mute)) + clapper_player_set_mute (player, mute); + break; + } + case CLAPPER_SERVER_ACTION_SET_PROGRESSION:{ + ClapperQueueProgressionMode mode; + if (clapper_server_actions_parse_set_progression (text, &mode)) + clapper_queue_set_progression_mode (clapper_player_get_queue (player), mode); + break; + } + case CLAPPER_SERVER_ACTION_ADD:{ + const gchar *uri; + if (clapper_server_get_queue_controllable (self) + && clapper_server_actions_parse_add (text, &uri)) { + ClapperMediaItem *item = clapper_media_item_new (uri); + clapper_utils_queue_append_on_main_sync (clapper_player_get_queue (player), item); + gst_object_unref (item); + } + break; + } + case CLAPPER_SERVER_ACTION_INSERT:{ + gchar *uri; + guint after_id; + if (clapper_server_get_queue_controllable (self) + && clapper_server_actions_parse_insert (text, &uri, &after_id)) { + ClapperMediaItem *item, *after_item = NULL; + guint i; + for (i = 0; i < self->items->len; ++i) { + ClapperMediaItem *tmp_after_item = (ClapperMediaItem *) g_ptr_array_index (self->items, i); + if (after_id == clapper_media_item_get_id (tmp_after_item)) { + after_item = tmp_after_item; + break; + } + } + item = clapper_media_item_new (uri); + clapper_utils_queue_insert_on_main_sync (clapper_player_get_queue (player), item, after_item); + gst_object_unref (item); + g_free (uri); + } + break; + } + case CLAPPER_SERVER_ACTION_SELECT:{ + guint id; + if (clapper_server_get_queue_controllable (self) + && clapper_server_actions_parse_select (text, &id)) { + guint i; + for (i = 0; i < self->items->len; ++i) { + ClapperMediaItem *item = (ClapperMediaItem *) g_ptr_array_index (self->items, i); + if (id == clapper_media_item_get_id (item)) { + clapper_queue_select_item (clapper_player_get_queue (player), item); + break; + } + } + } + break; + } + case CLAPPER_SERVER_ACTION_REMOVE:{ + guint id; + if (clapper_server_get_queue_controllable (self) + && clapper_server_actions_parse_remove (text, &id)) { + guint i; + for (i = 0; i < self->items->len; ++i) { + ClapperMediaItem *item = (ClapperMediaItem *) g_ptr_array_index (self->items, i); + if (id == clapper_media_item_get_id (item)) { + clapper_utils_queue_remove_on_main_sync (clapper_player_get_queue (player), item); + break; + } + } + } + break; + } + case CLAPPER_SERVER_ACTION_CLEAR: + if (clapper_server_get_queue_controllable (self)) + clapper_utils_queue_clear_on_main_sync (clapper_player_get_queue (player)); + break; + default: + g_assert_not_reached (); + break; + } + + gst_object_unref (player); +} + +static void +_ws_connection_closed_cb (SoupWebsocketConnection *connection, ClapperServer *self) +{ + GST_INFO_OBJECT (self, "WebSocket connection closed: %p", connection); + g_ptr_array_remove (self->ws_connections, connection); +} + +static void +_request_cb (SoupServer *server, SoupServerMessage *msg, + const gchar *path, GHashTable *query, ClapperServer *self) +{ + gchar *data; + + if (!(data = clapper_server_json_build_complete (self, + self->played_item, self->played_index, self->items))) { + soup_server_message_set_status (msg, SOUP_STATUS_SERVICE_UNAVAILABLE, NULL); + return; + } + + soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL); + soup_server_message_set_response (msg, "application/json", + SOUP_MEMORY_TAKE, data, strlen (data)); +} + +static void +_websocket_connection_cb (SoupServer *server, SoupServerMessage *msg, + const gchar *path, SoupWebsocketConnection *connection, ClapperServer *self) +{ + GST_INFO_OBJECT (self, "New WebSocket connection: %p", connection); + + g_signal_connect (connection, "message", G_CALLBACK (_ws_connection_message_cb), self); + g_signal_connect (connection, "closed", G_CALLBACK (_ws_connection_closed_cb), self); + g_ptr_array_add (self->ws_connections, g_object_ref (connection)); +} + +static void +clapper_server_send_ws_message (ClapperServer *self, const gchar *text) +{ + guint i; + + GST_LOG_OBJECT (self, "Sending WS message to clients: \"%s\"", text); + + for (i = 0; i < self->ws_connections->len; ++i) { + SoupWebsocketConnection *connection = g_ptr_array_index (self->ws_connections, i); + + if (soup_websocket_connection_get_state (connection) != SOUP_WEBSOCKET_STATE_OPEN) + continue; + + soup_websocket_connection_send_text (connection, text); + } +} + +static inline void +clapper_server_send_ws_double_event (ClapperServer *self, const gchar *event, gdouble val) +{ + gchar text[DOUBLE_EVENT_MAX_SIZE]; + + g_snprintf (text, sizeof (text), "%s %.2lf", event, val); + clapper_server_send_ws_message (self, text); +} + +static inline void +clapper_server_send_ws_uint_event (ClapperServer *self, const gchar *event, guint val) +{ + gchar text[UINT_EVENT_MAX_SIZE]; + + g_snprintf (text, sizeof (text), "%s %u", event, val); + clapper_server_send_ws_message (self, text); +} + +static gboolean +_send_ws_queue_changed_delayed_cb (ClapperServer *self) +{ + GST_DEBUG_OBJECT (self, "Delayed queue changed handler reached"); + + _clear_delayed_queue_changed_timeout (self); + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + + return G_SOURCE_REMOVE; +} + +static void +clapper_server_state_changed (ClapperFeature *feature, ClapperPlayerState state) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "State changed to: %i", state); + + if (!self->running || self->ws_connections->len == 0) + return; + + switch (state) { + case CLAPPER_PLAYER_STATE_PLAYING: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_STATE_MAKE (CLAPPER_SERVER_PLAYER_STATE_PLAYING)); + break; + case CLAPPER_PLAYER_STATE_PAUSED: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_STATE_MAKE (CLAPPER_SERVER_PLAYER_STATE_PAUSED)); + break; + case CLAPPER_PLAYER_STATE_BUFFERING: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_STATE_MAKE (CLAPPER_SERVER_PLAYER_STATE_BUFFERING)); + break; + case CLAPPER_PLAYER_STATE_STOPPED: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_STATE_MAKE (CLAPPER_SERVER_PLAYER_STATE_STOPPED)); + break; + default: + g_assert_not_reached (); + break; + } +} + +static void +clapper_server_position_changed (ClapperFeature *feature, gdouble position) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + /* Limit to seconds */ + if (ABS (self->position_uint - position) < 1) + return; + + self->position_uint = (guint) position; + GST_LOG_OBJECT (self, "Position changed to: %u", self->position_uint); + + if (self->running && self->ws_connections->len > 0) + clapper_server_send_ws_uint_event (self, CLAPPER_SERVER_WS_EVENT_POSITION, position); +} + +static void +clapper_server_speed_changed (ClapperFeature *feature, gdouble speed) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_LOG_OBJECT (self, "Speed changed to: %lf", speed); + + if (self->running && self->ws_connections->len > 0) + clapper_server_send_ws_double_event (self, CLAPPER_SERVER_WS_EVENT_SPEED, speed); +} + +static void +clapper_server_volume_changed (ClapperFeature *feature, gdouble volume) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_LOG_OBJECT (self, "Volume changed to: %lf", volume); + + if (self->running && self->ws_connections->len > 0) + clapper_server_send_ws_double_event (self, CLAPPER_SERVER_WS_EVENT_VOLUME, volume); +} + +static void +clapper_server_mute_changed (ClapperFeature *feature, gboolean mute) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + if (self->running && self->ws_connections->len > 0) { + clapper_server_send_ws_message (self, + (mute) ? CLAPPER_SERVER_WS_EVENT_MUTED : CLAPPER_SERVER_WS_EVENT_UNMUTED); + } +} + +static void +clapper_server_played_item_changed (ClapperFeature *feature, ClapperMediaItem *item) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Played item changed to: %" GST_PTR_FORMAT, item); + + gst_object_replace ((GstObject **) &self->played_item, GST_OBJECT_CAST (item)); + if (!g_ptr_array_find (self->items, self->played_item, &self->played_index)) + self->played_index = CLAPPER_QUEUE_INVALID_POSITION; + + if (self->running && self->ws_connections->len > 0) + clapper_server_send_ws_uint_event (self, CLAPPER_SERVER_WS_EVENT_PLAYED_INDEX, self->played_index); +} + +static void +clapper_server_item_updated (ClapperFeature *feature, ClapperMediaItem *item) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_LOG_OBJECT (self, "Item updated: %" GST_PTR_FORMAT, item); + + if (!self->running || self->ws_connections->len == 0) + return; + + /* Clear timeout, since we will either send + * immediately or set same timeout again */ + _clear_delayed_queue_changed_timeout (self); + + if (item != self->played_item) { + /* Happens once per item when discovered, so send immediately */ + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + } else { + /* Current item can be updated very often (when bitrate changes) + * so reduce amount of work here by adding delay */ + self->timeout_source = clapper_shared_utils_context_timeout_add_full ( + g_main_context_get_thread_default (), + G_PRIORITY_DEFAULT_IDLE, 1000, + (GSourceFunc) _send_ws_queue_changed_delayed_cb, + self, NULL); + } +} + +static void +clapper_server_queue_item_added (ClapperFeature *feature, ClapperMediaItem *item, guint index) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Queue item added %" GST_PTR_FORMAT, item); + g_ptr_array_insert (self->items, index, gst_object_ref (item)); + + if (self->running && self->ws_connections->len > 0) { + _clear_delayed_queue_changed_timeout (self); + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + } +} + +static void +clapper_server_queue_item_removed (ClapperFeature *feature, ClapperMediaItem *item, guint index) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Queue item removed %" GST_PTR_FORMAT, item); + + if (item == self->played_item) { + gst_clear_object (&self->played_item); + self->played_index = CLAPPER_QUEUE_INVALID_POSITION; + } + g_ptr_array_remove_index (self->items, index); + + if (self->running && self->ws_connections->len > 0) { + _clear_delayed_queue_changed_timeout (self); + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + } +} + +static void +clapper_server_queue_item_repositioned (ClapperFeature *feature, guint before, guint after) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + ClapperMediaItem *item; + + GST_DEBUG_OBJECT (self, "Queue item repositioned: %u -> %u", before, after); + + item = (ClapperMediaItem *) g_ptr_array_steal_index (self->items, before); + g_ptr_array_insert (self->items, after, item); + + if (self->running && self->ws_connections->len > 0) { + _clear_delayed_queue_changed_timeout (self); + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + } +} + +static void +clapper_server_queue_cleared (ClapperFeature *feature) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Queue cleared"); + _clear_stored_queue (self); + + if (self->running && self->ws_connections->len > 0) { + _clear_delayed_queue_changed_timeout (self); + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + } +} + +static void +clapper_server_queue_progression_changed (ClapperFeature *feature, + ClapperQueueProgressionMode mode) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Queue progression changed to: %i", mode); + + if (!self->running || self->ws_connections->len == 0) + return; + + switch (mode) { + case CLAPPER_QUEUE_PROGRESSION_NONE: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION_MAKE (CLAPPER_SERVER_QUEUE_PROGRESSION_NONE)); + break; + case CLAPPER_QUEUE_PROGRESSION_CONSECUTIVE: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION_MAKE (CLAPPER_SERVER_QUEUE_PROGRESSION_CONSECUTIVE)); + break; + case CLAPPER_QUEUE_PROGRESSION_REPEAT_ITEM: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION_MAKE (CLAPPER_SERVER_QUEUE_PROGRESSION_REPEAT_ITEM)); + break; + case CLAPPER_QUEUE_PROGRESSION_CAROUSEL: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION_MAKE (CLAPPER_SERVER_QUEUE_PROGRESSION_CAROUSEL)); + break; + case CLAPPER_QUEUE_PROGRESSION_SHUFFLE: + clapper_server_send_ws_message (self, + CLAPPER_SERVER_WS_EVENT_QUEUE_PROGRESSION_MAKE (CLAPPER_SERVER_QUEUE_PROGRESSION_SHUFFLE)); + break; + default: + g_assert_not_reached (); + break; + } +} + +static gboolean +clapper_server_prepare (ClapperFeature *feature) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Prepare"); + + self->server = soup_server_new ( + "server-header", "clapper-server", + NULL); + + soup_server_add_handler (self->server, "/", (SoupServerCallback) _request_cb, self, NULL); + soup_server_add_websocket_handler (self->server, "/websocket", NULL, NULL, + (SoupServerWebsocketCallback) _websocket_connection_cb, self, NULL); + + if (clapper_server_get_enabled (self)) + _start_server (self); + + return TRUE; +} + +static gboolean +clapper_server_unprepare (ClapperFeature *feature) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Unprepare"); + + _stop_server (self); + _clear_stored_queue (self); + + g_clear_object (&self->server); + + return TRUE; +} + +static void +clapper_server_property_changed (ClapperFeature *feature, GParamSpec *pspec) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (feature); + + GST_DEBUG_OBJECT (self, "Property changed: \"%s\"", + g_param_spec_get_name (pspec)); + + if (pspec == param_specs[PROP_ENABLED]) { + if (clapper_server_get_enabled (self)) + _start_server (self); + else + _stop_server (self); + } else if (pspec == param_specs[PROP_QUEUE_CONTROLLABLE]) { + _clear_delayed_queue_changed_timeout (self); + clapper_server_send_ws_message (self, CLAPPER_SERVER_WS_EVENT_QUEUE_CHANGED); + } +} + +/** + * clapper_server_new: + * + * Creates a new #ClapperServer instance. + * + * Returns: (transfer full): a new #ClapperServer instance. + */ +ClapperServer * +clapper_server_new (void) +{ + ClapperServer *server = g_object_new (CLAPPER_TYPE_SERVER, NULL); + gst_object_ref_sink (server); + + return server; +} + +/** + * clapper_server_set_enabled: + * @server: a #ClapperServer + * @enabled: if #ClapperServer should run + * + * Set whether #ClapperServer should be running. + * + * Note that server feature will run only after being added to the player. + * It can be however set to enabled earlier. If server was already added, + * changing this property allows to start/stop server at any time. + * + * To be notified when server is actually running/stopped after being enabled/disabled, + * you can listen for changes to [property@Clapper.Server:running] property. + */ +void +clapper_server_set_enabled (ClapperServer *self, gboolean enabled) +{ + gboolean prev_enabled; + + g_return_if_fail (CLAPPER_IS_SERVER (self)); + + prev_enabled = (gboolean) g_atomic_int_exchange (&self->enabled, (gint) enabled); + + if (prev_enabled != enabled) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_ENABLED]); +} + +/** + * clapper_server_get_enabled: + * @server: a #ClapperServer + * + * Get whether #ClapperServer is set to be running. + * + * Returns: %TRUE if enabled, %FALSE otherwise. + */ +gboolean +clapper_server_get_enabled (ClapperServer *self) +{ + g_return_val_if_fail (CLAPPER_IS_SERVER (self), FALSE); + + return (gboolean) g_atomic_int_get (&self->enabled); +} + +/** + * clapper_server_get_running: + * @server: a #ClapperServer + * + * Get whether #ClapperServer is currently running. + * + * Returns: %TRUE if running, %FALSE otherwise. + */ +gboolean +clapper_server_get_running (ClapperServer *self) +{ + gboolean running; + + g_return_val_if_fail (CLAPPER_IS_SERVER (self), FALSE); + + GST_OBJECT_LOCK (self); + running = self->running; + GST_OBJECT_UNLOCK (self); + + return running; +} + +/** + * clapper_server_set_port: + * @server: a #ClapperServer + * @port: a port number or 0 for random free port + * + * Set server listening port. + */ +void +clapper_server_set_port (ClapperServer *self, guint port) +{ + gboolean changed; + + g_return_if_fail (CLAPPER_IS_SERVER (self)); + g_return_if_fail (port <= PORT_MAX); + + GST_OBJECT_LOCK (self); + if ((changed = (port != self->port))) + self->port = port; + GST_OBJECT_UNLOCK (self); + + if (changed) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_PORT]); +} + +/** + * clapper_server_get_port: + * @server: a #ClapperServer + * + * Get requested server listening port. + * + * Returns: Requested listening port or 0 using random port. + */ +guint +clapper_server_get_port (ClapperServer *self) +{ + guint port; + + g_return_val_if_fail (CLAPPER_IS_SERVER (self), 0); + + GST_OBJECT_LOCK (self); + port = self->port; + GST_OBJECT_UNLOCK (self); + + return port; +} + +/** + * clapper_server_get_current_port: + * @server: a #ClapperServer + * + * Get port on which server is currently listening on. + * + * Returns: Current listening port or 0 if server is not listening. + */ +guint +clapper_server_get_current_port (ClapperServer *self) +{ + guint current_port; + + g_return_val_if_fail (CLAPPER_IS_SERVER (self), 0); + + GST_OBJECT_LOCK (self); + current_port = self->current_port; + GST_OBJECT_UNLOCK (self); + + return current_port; +} + +/** + * clapper_server_set_queue_controllable: + * @server: a #ClapperServer + * @controllable: if #ClapperQueue should be controllable + * + * Set whether remote @server clients can control [class@Clapper.Queue]. + * + * This includes ability to add/remove items from + * the queue and selecting current item for playback + * remotely using WebSocket messages. + * + * You probably want to keep this disabled if your application + * is supposed to manage what is played now and not WebSocket client. + */ +void +clapper_server_set_queue_controllable (ClapperServer *self, gboolean controllable) +{ + gboolean prev_controllable; + + g_return_if_fail (CLAPPER_IS_SERVER (self)); + + prev_controllable = (gboolean) g_atomic_int_exchange (&self->queue_controllable, (gint) controllable); + + if (prev_controllable != controllable) + g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_QUEUE_CONTROLLABLE]); +} + +/** + * clapper_server_get_queue_controllable: + * @server: a #ClapperServer + * + * Get whether remote @server clients can control [class@Clapper.Queue]. + * + * Returns: %TRUE if control over #ClapperQueue is allowed, %FALSE otherwise. + */ +gboolean +clapper_server_get_queue_controllable (ClapperServer *self) +{ + g_return_val_if_fail (CLAPPER_IS_SERVER (self), FALSE); + + return (gboolean) g_atomic_int_get (&self->queue_controllable); +} + +static void +clapper_server_init (ClapperServer *self) +{ + self->items = g_ptr_array_new_with_free_func ((GDestroyNotify) gst_object_unref); + self->ws_connections = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + self->played_index = CLAPPER_QUEUE_INVALID_POSITION; + self->position_uint = G_MAXUINT; + + g_atomic_int_set (&self->enabled, (gint) DEFAULT_ENABLED); + g_atomic_int_set (&self->queue_controllable, (gint) DEFAULT_QUEUE_CONTROLLABLE); +} + +static void +clapper_server_dispose (GObject *object) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (object); + + GST_OBJECT_LOCK (self); + + g_clear_handle_id (&self->error_id, g_source_remove); + g_clear_handle_id (&self->running_id, g_source_remove); + + GST_OBJECT_UNLOCK (self); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +clapper_server_finalize (GObject *object) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (object); + + g_ptr_array_unref (self->ws_connections); + g_ptr_array_unref (self->items); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +clapper_server_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (object); + + switch (prop_id) { + case PROP_ENABLED: + clapper_server_set_enabled (self, g_value_get_boolean (value)); + break; + case PROP_PORT: + clapper_server_set_port (self, g_value_get_uint (value)); + break; + case PROP_QUEUE_CONTROLLABLE: + clapper_server_set_queue_controllable (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_server_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + ClapperServer *self = CLAPPER_SERVER_CAST (object); + + switch (prop_id) { + case PROP_ENABLED: + g_value_set_boolean (value, clapper_server_get_enabled (self)); + break; + case PROP_RUNNING: + g_value_set_boolean (value, clapper_server_get_running (self)); + break; + case PROP_PORT: + g_value_set_uint (value, clapper_server_get_port (self)); + break; + case PROP_CURRENT_PORT: + g_value_set_uint (value, clapper_server_get_current_port (self)); + break; + case PROP_QUEUE_CONTROLLABLE: + g_value_set_boolean (value, clapper_server_get_queue_controllable (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +clapper_server_class_init (ClapperServerClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + ClapperFeatureClass *feature_class = (ClapperFeatureClass *) klass; + + GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperserver", 0, + "Clapper Server"); + clapper_server_mdns_debug_init (); + + gobject_class->get_property = clapper_server_get_property; + gobject_class->set_property = clapper_server_set_property; + gobject_class->dispose = clapper_server_dispose; + gobject_class->finalize = clapper_server_finalize; + + feature_class->prepare = clapper_server_prepare; + feature_class->unprepare = clapper_server_unprepare; + feature_class->property_changed = clapper_server_property_changed; + feature_class->state_changed = clapper_server_state_changed; + feature_class->position_changed = clapper_server_position_changed; + feature_class->speed_changed = clapper_server_speed_changed; + feature_class->volume_changed = clapper_server_volume_changed; + feature_class->mute_changed = clapper_server_mute_changed; + feature_class->played_item_changed = clapper_server_played_item_changed; + feature_class->item_updated = clapper_server_item_updated; + feature_class->queue_item_added = clapper_server_queue_item_added; + feature_class->queue_item_removed = clapper_server_queue_item_removed; + feature_class->queue_item_repositioned = clapper_server_queue_item_repositioned; + feature_class->queue_cleared = clapper_server_queue_cleared; + feature_class->queue_progression_changed = clapper_server_queue_progression_changed; + + /** + * ClapperServer:enabled: + * + * Whether server is enabled. + */ + param_specs[PROP_ENABLED] = g_param_spec_boolean ("enabled", + NULL, NULL, DEFAULT_ENABLED, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperServer:running: + * + * Whether server is currently running. + */ + param_specs[PROP_RUNNING] = g_param_spec_boolean ("running", + NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperServer:port: + * + * Port to listen on or 0 for using random unused port. + */ + param_specs[PROP_PORT] = g_param_spec_uint ("port", + NULL, NULL, 0, PORT_MAX, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperServer:current-port: + * + * Port on which server is currently listening on or 0 if not listening. + */ + param_specs[PROP_CURRENT_PORT] = g_param_spec_uint ("current-port", + NULL, NULL, 0, PORT_MAX, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperServer:queue-controllable: + * + * Whether remote server clients can control #ClapperQueue. + */ + param_specs[PROP_QUEUE_CONTROLLABLE] = g_param_spec_boolean ("queue-controllable", + NULL, NULL, DEFAULT_QUEUE_CONTROLLABLE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * ClapperServer::error: + * @server: a #ClapperServer + * @error: a #GError + * + * Error signal when server could not start. + * This will be emitted from application main thread. + */ + signals[SIGNAL_ERROR] = g_signal_new ("error", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_ERROR); + + g_object_class_install_properties (gobject_class, PROP_LAST, param_specs); +} diff --git a/src/lib/clapper/features/server/clapper-server.h b/src/lib/clapper/features/server/clapper-server.h new file mode 100644 index 00000000..ffc10dcf --- /dev/null +++ b/src/lib/clapper/features/server/clapper-server.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#if !defined(__CLAPPER_INSIDE__) && !defined(CLAPPER_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLAPPER_TYPE_SERVER (clapper_server_get_type()) +#define CLAPPER_SERVER_CAST(obj) ((ClapperServer *)(obj)) + +G_DECLARE_FINAL_TYPE (ClapperServer, clapper_server, CLAPPER, SERVER, ClapperFeature) + +ClapperServer * clapper_server_new (void); + +void clapper_server_set_enabled (ClapperServer *server, gboolean enabled); + +gboolean clapper_server_get_enabled (ClapperServer *server); + +gboolean clapper_server_get_running (ClapperServer *server); + +void clapper_server_set_port (ClapperServer *server, guint port); + +guint clapper_server_get_port (ClapperServer *server); + +guint clapper_server_get_current_port (ClapperServer *server); + +void clapper_server_set_queue_controllable (ClapperServer *server, gboolean controllable); + +gboolean clapper_server_get_queue_controllable (ClapperServer *server); + +G_END_DECLS diff --git a/src/lib/clapper/features/server/meson.build b/src/lib/clapper/features/server/meson.build new file mode 100644 index 00000000..d80d2e66 --- /dev/null +++ b/src/lib/clapper/features/server/meson.build @@ -0,0 +1,34 @@ +feature_option = get_option(feature_name) + +if feature_option.disabled() + subdir_done() +endif + +feature_deps = [ + dependency('libsoup-3.0', required: false), + dependency('microdns', version: '>= 0.2.0', required: false), +] +foreach dep : feature_deps + if not dep.found() + if feature_option.enabled() + error('@0@ feature was enabled, but required dependencies were not found'.format(feature_name)) + endif + subdir_done() + endif +endforeach + +clapper_features_headers += [ + 'features/server/clapper-server.h', +] +clapper_features_sources += [ + 'features/server/clapper-server.c', + 'features/server/clapper-server-json.c', + 'features/server/clapper-server-actions.c', + 'features/server/clapper-server-mdns.c', +] +clapper_features_deps += feature_deps + +install_headers('clapper-server.h', + install_dir: join_paths(clapper_headers_dir, 'features', 'server'), +) +clapper_available_features += feature_name diff --git a/src/lib/clapper/meson.build b/src/lib/clapper/meson.build new file mode 100644 index 00000000..94e3aad7 --- /dev/null +++ b/src/lib/clapper/meson.build @@ -0,0 +1,219 @@ +clapper_dep = dependency('', required: false) +clapper_option = get_option('clapper') +build_clapper = false + +clapper_pkg_reqs = [ + 'gstreamer-1.0', + 'gstreamer-base-1.0', + 'gstreamer-audio-1.0', + 'gstreamer-tag-1.0', + 'gstreamer-pbutils-1.0', + 'glib-2.0', + 'gobject-2.0', + 'gio-2.0', +] + +if clapper_option.disabled() + subdir_done() +endif + +clapper_deps = [ + gst_dep, + gst_base_dep, + gst_audio_dep, + gst_tag_dep, + gst_pbutils_dep, + glib_dep, + gobject_dep, + gio_dep, +] + +foreach dep : clapper_deps + if not dep.found() + if clapper_option.enabled() + error('clapper option was enabled, but required dependencies were not found') + endif + subdir_done() + endif +endforeach + +version_conf = configuration_data() + +version_conf.set( + 'CLAPPER_VERSION', + meson.project_version(), +) +version_conf.set( + 'CLAPPER_MAJOR_VERSION', + version_array[0].to_int(), +) +version_conf.set( + 'CLAPPER_MINOR_VERSION', + version_array[1].to_int(), +) +version_conf.set( + 'CLAPPER_MICRO_VERSION', + version_array[2].to_int(), +) + +clapper_headers_dir = join_paths(includedir, clapper_api_name, 'clapper') + +clapper_version_header = configure_file( + input: 'clapper-version.h.in', + output: 'clapper-version.h', + configuration: version_conf, +) + +# Include the generated headers +clapper_conf_inc = [ + include_directories('.'), + include_directories('..'), +] + +clapper_headers = [ + 'clapper.h', + 'clapper-enums.h', + 'clapper-audio-stream.h', + 'clapper-feature.h', + 'clapper-marker.h', + 'clapper-media-item.h', + 'clapper-player.h', + 'clapper-queue.h', + 'clapper-stream.h', + 'clapper-stream-list.h', + 'clapper-subtitle-stream.h', + 'clapper-threaded-object.h', + 'clapper-timeline.h', + 'clapper-utils.h', + 'clapper-video-stream.h', + clapper_version_header, +] +clapper_sources = [ + 'clapper.c', + 'clapper-app-bus.c', + 'clapper-audio-stream.c', + 'clapper-feature.c', + 'clapper-features-bus.c', + 'clapper-features-manager.c', + 'clapper-marker.c', + 'clapper-media-item.c', + 'clapper-playbin-bus.c', + 'clapper-player.c', + 'clapper-queue.c', + 'clapper-stream.c', + 'clapper-stream-list.c', + 'clapper-subtitle-stream.c', + 'clapper-threaded-object.c', + 'clapper-timeline.c', + 'clapper-utils.c', + 'clapper-video-stream.c', + '../shared/clapper-shared-utils.c', +] +clapper_c_args = [ + '-DG_LOG_DOMAIN="Clapper"', + '-DCLAPPER_COMPILATION', + '-DGST_USE_UNSTABLE_API', +] + +subdir('features') + +clapper_enums = gnome.mkenums_simple( + 'clapper-enum-types', + sources: clapper_headers, + identifier_prefix: 'Clapper', + symbol_prefix: 'clapper', + install_header: true, + install_dir: clapper_headers_dir, +) + +clapper_lib = library( + clapper_api_name, + clapper_sources + clapper_features_sources + clapper_features_sources_internal + clapper_enums, + dependencies: clapper_deps + clapper_features_deps, + include_directories: clapper_conf_inc, + c_args: clapper_c_args, + version: clapper_version, + install: true, +) +install_headers(clapper_headers, + install_dir: clapper_headers_dir, +) +build_clapper = true + +if build_gir + clapper_gir = gnome.generate_gir(clapper_lib, + sources: [ + clapper_sources, + clapper_features_sources, + clapper_headers, + clapper_features_headers, + clapper_enums, + ], + extra_args: [ + gir_init_section, + '--quiet', + '--warn-all', + '-DCLAPPER_COMPILATION', + '-DGST_USE_UNSTABLE_API', + ], + nsversion: version_array[0] + '.0', + namespace: 'Clapper', + identifier_prefix: 'Clapper', + symbol_prefix: 'clapper', + export_packages: clapper_api_name, + install: true, + includes: [ + 'Gst-1.0', + 'GstBase-1.0', + 'GstAudio-1.0', + 'GstTag-1.0', + 'GstPbutils-1.0', + 'GLib-2.0', + 'GObject-2.0', + 'Gio-2.0', + ], + header: join_paths(meson.project_name(), 'clapper.h'), + ) +endif + +if build_vapi + if not build_gir + if get_option('vapi').enabled() + error('Cannot build "vapi" without "introspection"') + endif + else + clapper_vapi = gnome.generate_vapi(clapper_api_name, + sources: clapper_gir[0], + packages: clapper_pkg_reqs, + metadata_dirs: [ + join_paths (meson.current_source_dir(), 'metadata') + ], + install: true, + ) + endif +endif + +clapper_pkgconfig_variables = [ + 'features=' + ' '.join(clapper_available_features), +] + +pkgconfig.generate(clapper_lib, + unescaped_variables: clapper_pkgconfig_variables, + subdirs: [clapper_api_name], + filebase: clapper_api_name, + name: meson.project_name(), + version: meson.project_version(), + description: 'Clapper playback library', + requires: clapper_pkg_reqs, +) + +clapper_dep = declare_dependency( + link_with: clapper_lib, + include_directories: clapper_conf_inc, + dependencies: clapper_deps + clapper_features_deps, + sources: [ + clapper_version_header, + clapper_features_availability_header, + clapper_enums[1], + ], +) diff --git a/src/lib/clapper/metadata/Clapper-0.0.metadata b/src/lib/clapper/metadata/Clapper-0.0.metadata new file mode 100644 index 00000000..0f642711 --- /dev/null +++ b/src/lib/clapper/metadata/Clapper-0.0.metadata @@ -0,0 +1,7 @@ +// Skipped by GI, but Vala can handle it fine +//init_get_option_group skip=false +*_FORMAT skip=false + +// Init func compatibility +init.argv unowned +init_check.argv unowned diff --git a/src/lib/gst/meson.build b/src/lib/gst/meson.build new file mode 100644 index 00000000..e0d88069 --- /dev/null +++ b/src/lib/gst/meson.build @@ -0,0 +1 @@ +subdir('plugin') diff --git a/lib/gst/plugin/gstclappercontexthandler.c b/src/lib/gst/plugin/gstclappercontexthandler.c similarity index 100% rename from lib/gst/plugin/gstclappercontexthandler.c rename to src/lib/gst/plugin/gstclappercontexthandler.c diff --git a/lib/gst/plugin/gstclappercontexthandler.h b/src/lib/gst/plugin/gstclappercontexthandler.h similarity index 100% rename from lib/gst/plugin/gstclappercontexthandler.h rename to src/lib/gst/plugin/gstclappercontexthandler.h diff --git a/lib/gst/plugin/gstclapperimporter.c b/src/lib/gst/plugin/gstclapperimporter.c similarity index 100% rename from lib/gst/plugin/gstclapperimporter.c rename to src/lib/gst/plugin/gstclapperimporter.c diff --git a/lib/gst/plugin/gstclapperimporter.h b/src/lib/gst/plugin/gstclapperimporter.h similarity index 100% rename from lib/gst/plugin/gstclapperimporter.h rename to src/lib/gst/plugin/gstclapperimporter.h diff --git a/lib/gst/plugin/gstclapperimporterloader.c b/src/lib/gst/plugin/gstclapperimporterloader.c similarity index 100% rename from lib/gst/plugin/gstclapperimporterloader.c rename to src/lib/gst/plugin/gstclapperimporterloader.c diff --git a/lib/gst/plugin/gstclapperimporterloader.h b/src/lib/gst/plugin/gstclapperimporterloader.h similarity index 100% rename from lib/gst/plugin/gstclapperimporterloader.h rename to src/lib/gst/plugin/gstclapperimporterloader.h diff --git a/lib/gst/plugin/gstclapperpaintable.c b/src/lib/gst/plugin/gstclapperpaintable.c similarity index 100% rename from lib/gst/plugin/gstclapperpaintable.c rename to src/lib/gst/plugin/gstclapperpaintable.c diff --git a/lib/gst/plugin/gstclapperpaintable.h b/src/lib/gst/plugin/gstclapperpaintable.h similarity index 100% rename from lib/gst/plugin/gstclapperpaintable.h rename to src/lib/gst/plugin/gstclapperpaintable.h diff --git a/lib/gst/plugin/gstclappersink.c b/src/lib/gst/plugin/gstclappersink.c similarity index 100% rename from lib/gst/plugin/gstclappersink.c rename to src/lib/gst/plugin/gstclappersink.c diff --git a/lib/gst/plugin/gstclappersink.h b/src/lib/gst/plugin/gstclappersink.h similarity index 100% rename from lib/gst/plugin/gstclappersink.h rename to src/lib/gst/plugin/gstclappersink.h diff --git a/lib/gst/plugin/gstgdkformats.h b/src/lib/gst/plugin/gstgdkformats.h similarity index 100% rename from lib/gst/plugin/gstgdkformats.h rename to src/lib/gst/plugin/gstgdkformats.h diff --git a/lib/gst/plugin/gstgtkutils.c b/src/lib/gst/plugin/gstgtkutils.c similarity index 100% rename from lib/gst/plugin/gstgtkutils.c rename to src/lib/gst/plugin/gstgtkutils.c diff --git a/lib/gst/plugin/gstgtkutils.h b/src/lib/gst/plugin/gstgtkutils.h similarity index 100% rename from lib/gst/plugin/gstgtkutils.h rename to src/lib/gst/plugin/gstgtkutils.h diff --git a/lib/gst/plugin/gstplugin.c b/src/lib/gst/plugin/gstplugin.c similarity index 100% rename from lib/gst/plugin/gstplugin.c rename to src/lib/gst/plugin/gstplugin.c diff --git a/lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.c b/src/lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.c similarity index 100% rename from lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.c rename to src/lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.c diff --git a/lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.h b/src/lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.h similarity index 100% rename from lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.h rename to src/lib/gst/plugin/handlers/gl/gstclapperglcontexthandler.h diff --git a/src/lib/gst/plugin/handlers/gl/meson.build b/src/lib/gst/plugin/handlers/gl/meson.build new file mode 100644 index 00000000..b9ea7d12 --- /dev/null +++ b/src/lib/gst/plugin/handlers/gl/meson.build @@ -0,0 +1,140 @@ +gst_clapper_gl_ch_dep = dependency('', required: false) + +build_gl_ch = ( + not get_option('glimporter').disabled() + or not get_option('gluploader').disabled() +) +gl_support_required = ( + get_option('glimporter').enabled() + or get_option('gluploader').enabled() +) + +# GStreamer OpenGL +gst_gl_dep = dependency('gstreamer-gl-1.0', + version: gst_req, + fallback: ['gst-plugins-base'], + required: false, +) +gst_gl_x11_dep = dependency('', required: false) +gst_gl_wayland_dep = dependency('', required: false) +gst_gl_egl_dep = dependency('', required: false) + +gst_gl_apis = gst_gl_dep.get_variable('gl_apis').split() +gst_gl_winsys = gst_gl_dep.get_variable('gl_winsys').split() +gst_gl_platforms = gst_gl_dep.get_variable('gl_platforms').split() + +message('GStreamer OpenGL window systems: @0@'.format(' '.join(gst_gl_winsys))) +message('GStreamer OpenGL platforms: @0@'.format(' '.join(gst_gl_platforms))) +message('GStreamer OpenGL apis: @0@'.format(' '.join(gst_gl_apis))) + +foreach ws : ['x11', 'wayland', 'android', 'cocoa', 'eagl', 'win32', 'dispmanx', 'viv_fb'] + set_variable('gst_gl_have_window_@0@'.format(ws), gst_gl_winsys.contains(ws)) +endforeach + +foreach p : ['glx', 'egl', 'cgl', 'eagl', 'wgl'] + set_variable('gst_gl_have_platform_@0@'.format(p), gst_gl_platforms.contains(p)) +endforeach + +foreach api : ['gl', 'gles2'] + set_variable('gst_gl_have_api_@0@'.format(api), gst_gl_apis.contains(api)) +endforeach + +gst_gl_proto_dep = dependency('gstreamer-gl-prototypes-1.0', + version: gst_req, + fallback: ['gst-plugins-base'], + required: true +) +if gst_gl_have_window_x11 + gst_gl_x11_dep = dependency('gstreamer-gl-x11-1.0', + version: gst_req, + fallback: ['gst-plugins-base'], + required: true, +) +endif +if gst_gl_have_window_wayland + gst_gl_wayland_dep = dependency('gstreamer-gl-wayland-1.0', + version: gst_req, + fallback: ['gst-plugins-base'], + required: true, +) +endif +if gst_gl_have_platform_egl + gst_gl_egl_dep = dependency('gstreamer-gl-egl-1.0', + version: gst_req, + fallback: ['gst-plugins-base'], + required: true, +) +endif + +gst_plugin_gl_ch_deps = [gst_clapper_sink_dep, gst_gl_dep, gst_gl_proto_dep] +have_gtk_gl_windowing = false + +if gst_gl_have_window_x11 and (gst_gl_have_platform_egl or gst_gl_have_platform_glx) + gtk_x11_dep = dependency('gtk4-x11', required: false) + if gtk_x11_dep.found() + gst_plugin_gl_ch_deps += gtk_x11_dep + if gst_gl_have_platform_glx + gst_plugin_gl_ch_deps += gst_gl_x11_dep + endif + have_gtk_gl_windowing = true + endif +endif + +if gst_gl_have_window_wayland and gst_gl_have_platform_egl + gtk_wayland_dep = dependency('gtk4-wayland', required: false) + if gtk_wayland_dep.found() + gst_plugin_gl_ch_deps += [gtk_wayland_dep, gst_gl_wayland_dep] + have_gtk_gl_windowing = true + endif +endif + +if gst_gl_have_window_win32 and (gst_gl_have_platform_egl or gst_gl_have_platform_wgl) + gtk_win32_dep = dependency('gtk4-win32', required: false) + if gtk_win32_dep.found() + gst_plugin_gl_ch_deps += gtk_win32_dep + have_gtk_gl_windowing = true + endif +endif + +if gst_gl_have_window_cocoa and gst_gl_have_platform_cgl + gtk_macos_dep = dependency('gtk4-macos', required: false) + if gtk_macos_dep.found() + gst_plugin_gl_ch_deps += gtk_macos_dep + have_gtk_gl_windowing = true + endif +endif + +if not have_gtk_gl_windowing + if gl_support_required + error('GL-based importer was enabled, but support for current GL windowing is missing') + endif + build_gl_ch = false +endif + +if gst_gl_have_platform_egl + gst_plugin_gl_ch_deps += gst_gl_egl_dep +endif + +foreach dep : gst_plugin_gl_ch_deps + if not dep.found() + if gl_support_required + error('GL-based importer was enabled, but required dependencies were not found') + endif + build_gl_ch = false + endif +endforeach + +if build_gl_ch + gst_clapper_gl_ch_dep = declare_dependency( + link_with: library('gstclapperglcontexthandler', + 'gstclapperglcontexthandler.c', + c_args: gst_clapper_plugin_args, + include_directories: gst_plugin_conf_inc, + dependencies: gst_plugin_gl_ch_deps, + version: meson.project_version(), + install: true, + ), + include_directories: gst_plugin_conf_inc, + dependencies: gst_plugin_gl_ch_deps, + ) +endif diff --git a/lib/gst/plugin/handlers/meson.build b/src/lib/gst/plugin/handlers/meson.build similarity index 100% rename from lib/gst/plugin/handlers/meson.build rename to src/lib/gst/plugin/handlers/meson.build diff --git a/lib/gst/plugin/importers/gstclapperglimporter.c b/src/lib/gst/plugin/importers/gstclapperglimporter.c similarity index 100% rename from lib/gst/plugin/importers/gstclapperglimporter.c rename to src/lib/gst/plugin/importers/gstclapperglimporter.c diff --git a/lib/gst/plugin/importers/gstclapperglimporter.h b/src/lib/gst/plugin/importers/gstclapperglimporter.h similarity index 100% rename from lib/gst/plugin/importers/gstclapperglimporter.h rename to src/lib/gst/plugin/importers/gstclapperglimporter.h diff --git a/lib/gst/plugin/importers/gstclappergluploader.c b/src/lib/gst/plugin/importers/gstclappergluploader.c similarity index 100% rename from lib/gst/plugin/importers/gstclappergluploader.c rename to src/lib/gst/plugin/importers/gstclappergluploader.c diff --git a/lib/gst/plugin/importers/gstclappergluploader.h b/src/lib/gst/plugin/importers/gstclappergluploader.h similarity index 100% rename from lib/gst/plugin/importers/gstclappergluploader.h rename to src/lib/gst/plugin/importers/gstclappergluploader.h diff --git a/lib/gst/plugin/importers/gstclapperrawimporter.c b/src/lib/gst/plugin/importers/gstclapperrawimporter.c similarity index 100% rename from lib/gst/plugin/importers/gstclapperrawimporter.c rename to src/lib/gst/plugin/importers/gstclapperrawimporter.c diff --git a/lib/gst/plugin/importers/gstclapperrawimporter.h b/src/lib/gst/plugin/importers/gstclapperrawimporter.h similarity index 100% rename from lib/gst/plugin/importers/gstclapperrawimporter.h rename to src/lib/gst/plugin/importers/gstclapperrawimporter.h diff --git a/lib/gst/plugin/importers/meson.build b/src/lib/gst/plugin/importers/meson.build similarity index 91% rename from lib/gst/plugin/importers/meson.build rename to src/lib/gst/plugin/importers/meson.build index 7cce1ef0..53c9a71c 100644 --- a/lib/gst/plugin/importers/meson.build +++ b/src/lib/gst/plugin/importers/meson.build @@ -23,7 +23,7 @@ if build_glimporter 'gstclapperglimporter', 'gstclapperglimporter.c', dependencies: gst_clapper_gl_ch_dep, - include_directories: configinc, + include_directories: gst_plugin_conf_inc, c_args: gst_clapper_plugin_args, install: true, install_dir: gst_clapper_importers_libdir, @@ -40,7 +40,7 @@ if build_gluploader 'gstclappergluploader', 'gstclappergluploader.c', dependencies: gst_clapper_gl_ch_dep, - include_directories: configinc, + include_directories: gst_plugin_conf_inc, c_args: gst_clapper_plugin_args, install: true, install_dir: gst_clapper_importers_libdir, @@ -59,7 +59,7 @@ if build_rawimporter 'gstclapperrawimporter', 'gstclapperrawimporter.c', dependencies: gst_clapper_sink_dep, - include_directories: configinc, + include_directories: gst_plugin_conf_inc, c_args: gst_clapper_plugin_args, install: true, install_dir: gst_clapper_importers_libdir, diff --git a/lib/gst/plugin/meson.build b/src/lib/gst/plugin/meson.build similarity index 60% rename from lib/gst/plugin/meson.build rename to src/lib/gst/plugin/meson.build index a8b581a4..17e5f00c 100644 --- a/lib/gst/plugin/meson.build +++ b/src/lib/gst/plugin/meson.build @@ -1,24 +1,13 @@ -gst_plugins_libdir = join_paths(prefix, libdir, 'gstreamer-1.0') - -gst_clapper_plugin_args = [ - '-DHAVE_CONFIG_H', - '-DGST_USE_UNSTABLE_API', -] - gst_clapper_sink_dep = dependency('', required: false) -gtk4_dep = dependency('gtk4', version: '>=4.6.0', required: false) -gmodule_dep = dependency('gmodule-2.0', - version: glib_req, - required: false, - fallback: ['glib', 'libgmodule_dep'], -) +gst_plugins_libdir = join_paths(prefix, libdir, 'gstreamer-1.0') +gst_clapper_importers_libdir = join_paths(clapper_libdir, 'gst', 'plugin', 'importers') gst_clapper_plugin_deps = [ gtk4_dep, gst_dep, - gstbase_dep, - gstvideo_dep, + gst_base_dep, + gst_video_dep, gmodule_dep, ] @@ -32,10 +21,37 @@ foreach dep : gst_clapper_plugin_deps endif endforeach +gst_clapper_plugin_args = [ + '-DHAVE_CONFIG_H', + '-DGST_USE_UNSTABLE_API', +] + if get_option('default_library') == 'static' gst_clapper_plugin_args += ['-DGST_STATIC_COMPILATION'] endif +cdata = configuration_data() + +cdata.set_quoted('PACKAGE', meson.project_name()) +cdata.set_quoted('VERSION', meson.project_version()) +cdata.set_quoted('PACKAGE_VERSION', meson.project_version()) + +cdata.set_quoted('GST_PACKAGE_NAME', 'gst-plugin-clapper') +cdata.set_quoted('GST_PACKAGE_ORIGIN', 'https://github.com/Rafostar/clapper') +cdata.set_quoted('GST_LICENSE', 'LGPL') + +cdata.set_quoted('CLAPPER_SINK_IMPORTER_PATH', gst_clapper_importers_libdir) + +configure_file( + output: 'config.h', + configuration: cdata, +) +gst_plugin_conf_inc = [ + include_directories('.'), + include_directories('..'), + include_directories('../..'), +] + gst_clapper_plugin_sources = [ 'gstclappersink.c', 'gstclapperpaintable.c', @@ -51,12 +67,12 @@ if build_gst_plugin link_with: library('gstclapper', gst_clapper_plugin_sources, c_args: gst_clapper_plugin_args, - include_directories: configinc, + include_directories: gst_plugin_conf_inc, dependencies: gst_clapper_plugin_deps, install: true, install_dir: gst_plugins_libdir, ), - include_directories: configinc, + include_directories: gst_plugin_conf_inc, dependencies: gst_clapper_plugin_deps, ) endif diff --git a/src/lib/meson.build b/src/lib/meson.build new file mode 100644 index 00000000..95ce0967 --- /dev/null +++ b/src/lib/meson.build @@ -0,0 +1,14 @@ +gir = find_program('g-ir-scanner', required: get_option('introspection')) +build_gir = (gir.found() and not get_option('introspection').disabled()) + +vapigen = find_program('vapigen', required: get_option('vapi')) +build_vapi = (vapigen.found() and not get_option('vapi').disabled()) + +gir_init_section = '--add-init-section=extern void gst_init(gint*,gchar**);' + \ + 'g_setenv("GST_REGISTRY_1.0", "@0@", TRUE);'.format(meson.current_build_dir() + '/gir_empty_registry.reg') + \ + 'g_setenv("GST_PLUGIN_PATH_1_0", "", TRUE);' + \ + 'g_setenv("GST_PLUGIN_SYSTEM_PATH_1_0", "", TRUE);' + \ + 'gst_init(NULL,NULL);' + +subdir('gst') +subdir('clapper') diff --git a/src/lib/shared/clapper-shared-utils-private.h b/src/lib/shared/clapper-shared-utils-private.h new file mode 100644 index 00000000..d513ae08 --- /dev/null +++ b/src/lib/shared/clapper-shared-utils-private.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +gpointer clapper_shared_utils_context_invoke_sync (GMainContext *context, GThreadFunc func, gpointer data); + +gpointer clapper_shared_utils_context_invoke_sync_full (GMainContext *context, GThreadFunc func, gpointer data, GDestroyNotify destroy_func); + +GSource * clapper_shared_utils_context_timeout_add_full (GMainContext *context, gint priority, guint interval, GSourceFunc func, gpointer data, GDestroyNotify destroy_func); + +G_END_DECLS diff --git a/src/lib/shared/clapper-shared-utils.c b/src/lib/shared/clapper-shared-utils.c new file mode 100644 index 00000000..29802d7c --- /dev/null +++ b/src/lib/shared/clapper-shared-utils.c @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 Rafał Dzięgiel + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "clapper-shared-utils-private.h" + +struct ClapperSharedUtilsInvoke +{ + GMutex lock; + GCond cond; + gboolean fired; + GThreadFunc func; + gpointer data; + + gpointer res; +}; + +static gboolean +_invoke_func (struct ClapperSharedUtilsInvoke *invoke) +{ + g_mutex_lock (&invoke->lock); + invoke->res = invoke->func (invoke->data); + invoke->fired = TRUE; + g_cond_signal (&invoke->cond); + g_mutex_unlock (&invoke->lock); + + return G_SOURCE_REMOVE; +} + +inline gpointer +clapper_shared_utils_context_invoke_sync (GMainContext *context, GThreadFunc func, gpointer data) +{ + struct ClapperSharedUtilsInvoke invoke; + + g_mutex_init (&invoke.lock); + g_cond_init (&invoke.cond); + invoke.fired = FALSE; + invoke.func = func; + invoke.data = data; + + g_main_context_invoke (context, + (GSourceFunc) _invoke_func, &invoke); + + g_mutex_lock (&invoke.lock); + while (!invoke.fired) + g_cond_wait (&invoke.cond, &invoke.lock); + g_mutex_unlock (&invoke.lock); + + g_mutex_clear (&invoke.lock); + g_cond_clear (&invoke.cond); + + return invoke.res; +} + +inline gpointer +clapper_shared_utils_context_invoke_sync_full (GMainContext *context, GThreadFunc func, gpointer data, GDestroyNotify destroy_func) +{ + gpointer res = clapper_shared_utils_context_invoke_sync (context, func, data); + + if (destroy_func) + destroy_func (data); + + return res; +} + +inline GSource * +clapper_shared_utils_context_timeout_add_full (GMainContext *context, gint priority, guint interval, + GSourceFunc func, gpointer data, GDestroyNotify destroy_func) +{ + GSource *source = g_timeout_source_new (interval); + + g_source_set_priority (source, priority); + g_source_set_callback (source, func, data, destroy_func); + g_source_attach (source, context); + + return source; +} diff --git a/src/main.js b/src/main.js deleted file mode 100644 index d6f3ebfd..00000000 --- a/src/main.js +++ /dev/null @@ -1,30 +0,0 @@ -imports.gi.versions.Gdk = '4.0'; -imports.gi.versions.Gtk = '4.0'; -imports.gi.versions.Soup = '3.0'; - -pkg.initGettext(); -pkg.initFormat(); - -const Debug = imports.src.debug; -Debug.debug('imports'); - -const { GstClapper, Gtk, Adw } = imports.gi; -const { App } = imports.src.app; -const Misc = imports.src.misc; - -function main(argv) -{ - Debug.debug('main'); - - GstClapper.Clapper.gst_init(null); - Gtk.init(); - Adw.init(); - - /* U+2236 seems to break RTL languages, use U+003A instead */ - if(Gtk.Widget.get_default_direction() === Gtk.TextDirection.RTL) - Misc.timeColon = ':'; - - Debug.debug('initialized'); - - new App().run(argv); -} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 00000000..c2f563b5 --- /dev/null +++ b/src/meson.build @@ -0,0 +1 @@ +subdir('lib') diff --git a/src/misc.js b/src/misc.js deleted file mode 100644 index f9a612b2..00000000 --- a/src/misc.js +++ /dev/null @@ -1,227 +0,0 @@ -const { Gio, GLib, Gdk, Gtk } = imports.gi; -const Debug = imports.src.debug; - -const { debug } = Debug; - -var appName = 'Clapper'; -var appId = 'com.github.rafostar.Clapper'; -var subsMimes = [ - 'application/x-subrip', - 'text/x-ssa', -]; -var timeColon = '∶'; - -var settings = new Gio.Settings({ - schema_id: appId, -}); - -var maxVolume = 1.5; - -/* Keys must be lowercase */ -const subsTitles = { - sdh: 'SDH', - cc: 'CC', - traditional: 'Traditional', - simplified: 'Simplified', - honorifics: 'Honorifics', -}; -const subsKeys = Object.keys(subsTitles); - -let inhibitCookie; - -function getResourceUri(path) -{ - const res = `file://${pkg.pkgdatadir}/${path}`; - - debug(`importing ${res}`); - - return res; -} - -function getBuilderForName(name) -{ - return Gtk.Builder.new_from_file(`${pkg.pkgdatadir}/ui/${name}`); -} - -function loadCustomCss() -{ - const cssProvider = new Gtk.CssProvider(); - - cssProvider.load_from_path(`${pkg.pkgdatadir}/css/styles.css`); - - Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - cssProvider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); -} - -function getClapperThemeIconUri() -{ - const display = Gdk.Display.get_default(); - if(!display) return null; - - const iconTheme = Gtk.IconTheme.get_for_display(display); - if(!iconTheme || !iconTheme.has_icon(appId)) - return null; - - const iconPaintable = iconTheme.lookup_icon(appId, null, 256, 1, - Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_REGULAR - ); - const iconFile = iconPaintable.get_file(); - if(!iconFile) return null; - - const iconPath = iconFile.get_path(); - if(!iconPath) return null; - - let substractName = iconPath.substring( - iconPath.indexOf('/icons/') + 7, iconPath.indexOf('/scalable/') - ); - if(!substractName || substractName.includes('/')) - return null; - - substractName = substractName.toLowerCase(); - const postFix = (substractName === iconTheme.theme_name.toLowerCase()) - ? substractName - : 'hicolor'; - const cacheIconName = `clapper-${postFix}.svg`; - - /* We need to have this icon placed in a folder - * accessible from both app runtime and gnome-shell */ - const expectedFile = Gio.File.new_for_path( - GLib.get_user_cache_dir() + `/${appId}/icons/${cacheIconName}` - ); - if(!expectedFile.query_exists(null)) { - debug('no cached icon file'); - - const dirPath = expectedFile.get_parent().get_path(); - GLib.mkdir_with_parents(dirPath, 493); // octal 755 - iconFile.copy(expectedFile, - Gio.FileCopyFlags.TARGET_DEFAULT_PERMS, null, null - ); - debug(`icon copied to cache dir: ${cacheIconName}`); - } - const iconUri = expectedFile.get_uri(); - debug(`using cached clapper icon uri: ${iconUri}`); - - return iconUri; -} - -function getSubsTitle(infoTitle) -{ - if(!infoTitle) - return null; - - const searchName = infoTitle.toLowerCase(); - const found = subsKeys.find(key => key === searchName); - - return (found) ? subsTitles[found] : null; -} - -function setAppInhibit(isInhibit, window) -{ - let isInhibited = false; - - if(isInhibit) { - if(inhibitCookie) - return; - - const app = window.get_application(); - - inhibitCookie = app.inhibit( - window, - Gtk.ApplicationInhibitFlags.IDLE, - 'video is playing' - ); - if(!inhibitCookie) - debug(new Error('could not inhibit session!')); - - isInhibited = (inhibitCookie > 0); - } - else { - if(!inhibitCookie) - return; - - const app = window.get_application(); - app.uninhibit(inhibitCookie); - inhibitCookie = null; - } - - debug(`set prevent suspend to: ${isInhibited}`); -} - -function getFormattedTime(time, showHours) -{ - let hours; - - if(showHours || time >= 3600) { - hours = ('0' + Math.floor(time / 3600)).slice(-2); - time -= hours * 3600; - } - const minutes = ('0' + Math.floor(time / 60)).slice(-2); - time -= minutes * 60; - const seconds = ('0' + Math.floor(time)).slice(-2); - - const parsed = (hours) ? `${hours}${timeColon}` : ''; - return parsed + `${minutes}${timeColon}${seconds}`; -} - -function parsePlaylistFiles(filesArray) -{ - let index = filesArray.length; - let subs = null; - - while(index--) { - const file = filesArray[index]; - const filename = (file.get_basename) - ? file.get_basename() - : file.substring(file.lastIndexOf('/') + 1); - - const [type, isUncertain] = Gio.content_type_guess(filename, null); - - if(subsMimes.includes(type)) { - subs = file; - filesArray.splice(index, 1); - } - } - - /* We only support single video - * with external subtitles */ - if(subs && filesArray.length > 1) - subs = null; - - return [filesArray, subs]; -} - -function getFileFromLocalUri(uri) -{ - const file = Gio.file_new_for_uri(uri); - - if(!file.query_exists(null)) { - debug(new Error(`file does not exist: ${file.get_path()}`)); - - return null; - } - - return file; -} - -/* JS replacement of "Gst.Uri.get_protocol" */ -function getUriProtocol(uri) -{ - const arr = uri.split(':'); - return (arr.length > 1) ? arr[0] : null; -} - -function getIsTouch(gesture) -{ - const { source } = gesture.get_device(); - - switch(source) { - case Gdk.InputSource.PEN: - case Gdk.InputSource.TOUCHSCREEN: - return true; - default: - return false; - } -} diff --git a/src/player.js b/src/player.js deleted file mode 100644 index 5769aaa7..00000000 --- a/src/player.js +++ /dev/null @@ -1,711 +0,0 @@ -const { Adw, Gdk, Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi; -const ByteArray = imports.byteArray; -const Debug = imports.src.debug; -const Misc = imports.src.misc; -const { PlaylistWidget } = imports.src.playlist; - -const { debug, warn } = Debug; -const { settings } = Misc; - -let WebServer; - -var Player = GObject.registerClass({ - GTypeName: 'ClapperPlayer', -}, -class ClapperPlayer extends GstClapper.Clapper -{ - _init() - { - let vsink = null; - const use_legacy_sink = GLib.getenv('CLAPPER_USE_LEGACY_SINK'); - - if(!use_legacy_sink || use_legacy_sink != '1') { - vsink = Gst.ElementFactory.make('clappersink', null); - this.clappersink = vsink; - } - - if(!vsink) { - vsink = Gst.ElementFactory.make('glsinkbin', null); - const gtk4plugin = new GstClapper.ClapperGtk4Plugin(); - - warn('using legacy video sink'); - - this.clappersink = gtk4plugin.video_sink; - vsink.sink = this.clappersink; - } - - super._init({ - signal_dispatcher: new GstClapper.ClapperGMainContextSignalDispatcher(), - video_renderer: new GstClapper.ClapperVideoOverlayVideoRenderer({ - video_sink: vsink, - }), - mpris: new GstClapper.ClapperMpris({ - own_name: `org.mpris.MediaPlayer2.${Misc.appName}`, - id_path: '/' + Misc.appId.replace(/\./g, '/'), - identity: Misc.appName, - desktop_entry: Misc.appId, - default_art_url: Misc.getClapperThemeIconUri(), - }), - use_playbin3: settings.get_boolean('use-playbin3'), - use_pipewire: settings.get_boolean('use-pipewire'), - }); - - this.widget = this.clappersink.widget; - this.widget.add_css_class('videowidget'); - - this.visualization_enabled = false; - - this.webserver = null; - this.playlistWidget = new PlaylistWidget(); - - this.seekDone = true; - this.needsFastSeekRestore = false; - - this.windowMapped = false; - this.quitOnStop = false; - this.needsTocUpdate = true; - - this.set_all_plugins_ranks(); - this.set_and_bind_settings(); - - this.connect('state-changed', this._onStateChanged.bind(this)); - this.connect('uri-loaded', this._onUriLoaded.bind(this)); - this.connect('end-of-stream', this._onStreamEnded.bind(this)); - this.connect('warning', this._onPlayerWarning.bind(this)); - this.connect('error', this._onPlayerError.bind(this)); - - settings.connect('changed', this._onSettingsKeyChanged.bind(this)); - - this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this)); - } - - set_and_bind_settings() - { - const settingsToSet = [ - 'dark-theme', - 'after-playback', - 'seeking-mode', - 'audio-offset', - 'play-flags', - 'webserver-enabled' - ]; - - for(let key of settingsToSet) - this._onSettingsKeyChanged(settings, key); - - const flag = Gio.SettingsBindFlags.GET; - settings.bind('subtitle-font', this.pipeline, 'subtitle-font-desc', flag); - } - - set_all_plugins_ranks() - { - let data = {}; - let hadErr = false; - - /* Set empty plugin list if someone messed it externally */ - try { - data = JSON.parse(settings.get_string('plugin-ranking')); - if(Array.isArray(data)) { - data = {}; - hadErr = true; - } - } - catch(err) { - debug(err); - hadErr = true; - } - - if(hadErr) { - settings.set_string('plugin-ranking', "{}"); - debug('restored plugin ranking to defaults'); - } - - for(let plugin of Object.keys(data)) - this.set_plugin_rank(plugin, data[plugin]); - } - - set_plugin_rank(name, rank) - { - const gstRegistry = Gst.Registry.get(); - const feature = gstRegistry.lookup_feature(name); - if(!feature) { - warn(`cannot change rank of unavailable plugin: ${name}`); - return; - } - - const oldRank = feature.get_rank(); - if(rank === oldRank) - return; - - feature.set_rank(rank); - debug(`changed rank: ${oldRank} -> ${rank} for ${name}`); - } - - set_uri(uri) - { - if(Misc.getUriProtocol(uri) === 'file') { - const file = Misc.getFileFromLocalUri(uri); - if(!file) { - if(!this.playlistWidget.nextTrack()) - debug('set media reached end of playlist'); - - return; - } - if(uri.endsWith('.claps')) { - this.load_playlist_file(file); - - return; - } - } - - super.set_uri(uri); - } - - load_playlist_file(file) - { - const stream = new Gio.DataInputStream({ - base_stream: file.read(null) - }); - const listdir = file.get_parent(); - const playlist = []; - - let line; - while((line = stream.read_line(null)[0])) { - line = (line instanceof Uint8Array) - ? ByteArray.toString(line).trim() - : String(line).trim(); - - if(!Gst.uri_is_valid(line)) { - const lineFile = listdir.resolve_relative_path(line); - if(!lineFile) - continue; - - line = lineFile.get_uri(); - } - debug(`new playlist item: ${line}`); - playlist.push(line); - } - stream.close(null); - this.set_playlist(playlist); - } - - set_playlist(playlist) - { - if(this.state !== GstClapper.ClapperState.STOPPED) - this.stop(); - - debug('new playlist'); - this.playlistWidget.removeAll(); - this._addPlaylistItems(playlist); - - if(settings.get_boolean('fullscreen-auto')) { - const { root } = this.playlistWidget; - /* Do not enter fullscreen when already in it - * or when in floating mode */ - if( - root - && root.child - && !root.child.isFullscreenMode - && root.child.controlsRevealer.reveal_child - ) - root.fullscreen(); - } - - /* If not mapped yet, first track will play after map */ - if(this.windowMapped) - this._playFirstTrack(); - } - - append_playlist(playlist) - { - debug('appending playlist'); - this._addPlaylistItems(playlist); - - if( - !this.windowMapped - || this.state !== GstClapper.ClapperState.STOPPED - ) - return; - - if(!this.playlistWidget.nextTrack()) - debug('playlist append failed'); - } - - set_subtitles(source) - { - const uri = this._getSourceUri(source); - - /* Check local file existence */ - if( - Misc.getUriProtocol(uri) === 'file' - && !Misc.getFileFromLocalUri(uri) - ) - return; - - this.set_subtitle_uri(uri); - this.set_subtitle_track_enabled(true); - - debug(`applied subtitle track: ${uri}`); - } - - set_visualization_enabled(value) - { - if(value === this.visualization_enabled) - return; - - super.set_visualization_enabled(value); - this.visualization_enabled = value; - } - - get_visualization_enabled() - { - return this.visualization_enabled; - } - - seek(position) - { - /* avoid seek emits when position bar is altered */ - if(this.needsTocUpdate) - return; - - this.seekDone = false; - - if(this.state === GstClapper.ClapperState.STOPPED) - this.pause(); - - if(position < 0) - position = 0; - - super.seek(position); - } - - seek_seconds(seconds) - { - this.seek(seconds * Gst.SECOND); - } - - seek_chapter(seconds) - { - if(this.seek_mode !== GstClapper.ClapperSeekMode.FAST) { - this.seek_seconds(seconds); - return; - } - - this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT); - this.needsFastSeekRestore = true; - - this.seek_seconds(seconds); - } - - adjust_position(isIncrease) - { - this.seekDone = false; - - const { controls } = this.widget.get_ancestor(Gtk.Grid); - const max = controls.positionAdjustment.get_upper(); - - let seekingValue = settings.get_int('seeking-value'); - - switch(settings.get_int('seeking-unit')) { - case 2: /* Percentage */ - seekingValue *= max / 100; - break; - case 1: /* Minute */ - seekingValue *= 60; - break; - default: - break; - } - - if(!isIncrease) - seekingValue *= -1; - - let positionSeconds = controls.positionScale.get_value() + seekingValue; - - if(positionSeconds > max) - positionSeconds = max; - - controls.positionScale.set_value(positionSeconds); - } - - adjust_volume(isIncrease, offset) - { - offset = offset || 0.05; - - const { controls } = this.widget.get_ancestor(Gtk.Grid); - const value = (isIncrease) ? offset : -offset; - const scale = controls.volumeButton.volumeScale; - - scale.set_value(scale.get_value() + value); - } - - next_chapter() - { - return this._switchChapter(false); - } - - prev_chapter() - { - return this._switchChapter(true); - } - - emitWs(action, value) - { - if(!this.webserver) - return; - - this.webserver.sendMessage({ action, value }); - } - - receiveWs(action, value) - { - switch(action) { - case 'toggle_play': - case 'play': - case 'pause': - this[action](); - break; - case 'seek': - case 'set_playlist': - case 'append_playlist': - case 'set_subtitles': - this[action](value); - break; - case 'change_playlist_item': - this.playlistWidget.changeActiveRow(value); - break; - case 'toggle_fullscreen': - case 'volume_up': - case 'volume_down': - case 'next_track': - case 'prev_track': - case 'next_chapter': - case 'prev_chapter': - this.widget.activate_action(`app.${action}`, null); - break; - case 'toggle_maximized': - action = 'toggle-maximized'; - case 'minimize': - case 'close': - this.widget.activate_action(`window.${action}`, null); - break; - default: - warn(`unhandled WebSocket action: ${action}`); - break; - } - } - - _switchChapter(isPrevious) - { - if(this.state === GstClapper.ClapperState.STOPPED) - return false; - - const { chapters } = this.widget.root.child.controls; - if(!chapters) - return false; - - const now = this.position / Gst.SECOND; - const chapterTimes = Object.keys(chapters).sort((a, b) => a - b); - if(isPrevious) - chapterTimes.reverse(); - - const chapter = chapterTimes.find(time => (isPrevious) - ? now - 2.5 > time - : now < time - ); - if(!chapter) - return false; - - this.seek_chapter(chapter); - - return true; - } - - _addPlaylistItems(playlist) - { - for(let source of playlist) { - const uri = this._getSourceUri(source); - - debug(`added uri: ${uri}`); - this.playlistWidget.addItem(uri); - } - } - - _getSourceUri(source) - { - return (source.get_uri != null) - ? source.get_uri() - : Gst.uri_is_valid(source) - ? source - : Gst.filename_to_uri(source); - } - - _playFirstTrack() - { - const firstTrack = this.playlistWidget.get_row_at_index(0); - if(!firstTrack) return; - - firstTrack.activate(); - } - - _performCloseCleanup(window) - { - window.disconnect(this.closeRequestSignal); - this.closeRequestSignal = null; - - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - - if(!clapperWidget.isFullscreenMode && clapperWidget.controlsRevealer.child_revealed) { - const size = window.get_default_size(); - - if(size[0] > 0 && size[1] > 0) { - settings.set_string('window-size', JSON.stringify(size)); - debug(`saved window size: ${size[0]}x${size[1]}`); - } - } - /* If "quitOnStop" is set here it means that we are in middle of autoclosing */ - if(this.state !== GstClapper.ClapperState.STOPPED && !this.quitOnStop) { - const playlistItem = this.playlistWidget.getActiveRow(); - - let resumeInfo = {}; - if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) { - const resumeTime = Math.floor(this.position / Gst.SECOND); - const resumeDuration = this.duration / Gst.SECOND; - - /* Do not save resume info when video is very short, - * just started or almost finished */ - if( - resumeDuration > 60 - && resumeTime > 15 - && resumeDuration - resumeTime > 20 - ) { - resumeInfo.title = playlistItem.filename; - resumeInfo.time = resumeTime; - resumeInfo.duration = resumeDuration; - - debug(`saving resume info for: ${resumeInfo.title}`); - debug(`resume time: ${resumeInfo.time}, duration: ${resumeInfo.duration}`); - } - else - debug('resume info is not worth saving'); - } - settings.set_string('resume-database', JSON.stringify([resumeInfo])); - } - const volume = this.volume; - debug(`saving last volume: ${volume}`); - settings.set_double('volume-last', volume); - - clapperWidget.controls._onCloseRequest(); - } - - _onStateChanged(player, state) - { - this.emitWs('state_changed', state); - - if(state !== GstClapper.ClapperState.BUFFERING) { - const root = player.widget.get_root(); - - if(this.quitOnStop) { - if(root && state === GstClapper.ClapperState.STOPPED) - root.run_dispose(); - - return; - } - const isInhibit = (state === GstClapper.ClapperState.PLAYING); - Misc.setAppInhibit(isInhibit, root); - } - - const clapperWidget = player.widget.get_ancestor(Gtk.Grid); - if(!clapperWidget) return; - - if(!this.seekDone && state !== GstClapper.ClapperState.BUFFERING) { - clapperWidget.updateTime(); - - if(this.needsFastSeekRestore) { - this.set_seek_mode(GstClapper.ClapperSeekMode.FAST); - this.needsFastSeekRestore = false; - } - - this.seekDone = true; - debug('seeking finished'); - - clapperWidget._onPlayerPositionUpdated(this, this.position); - } - - clapperWidget._onPlayerStateChanged(player, state); - } - - _onStreamEnded(player) - { - const lastTrackId = this.playlistWidget.activeRowId; - - debug(`end of stream: ${lastTrackId}`); - this.emitWs('end_of_stream', lastTrackId); - - if(this.playlistWidget._handleStreamEnded(player)) - return; - - /* After playback equal 2 means close the app */ - if(settings.get_int('after-playback') === 2) { - /* Stop will be automatically called soon afterwards */ - this.quitOnStop = true; - this._performCloseCleanup(this.widget.get_root()); - } - - /* When this signal is connected player - * wants us to decide if it should stop */ - this.stop(); - } - - _onUriLoaded(player, uri) - { - debug(`URI loaded: ${uri}`); - this.needsTocUpdate = true; - - player.play(); - } - - _onPlayerWarning(player, error) - { - debug(error.message); - } - - _onPlayerError(player, error) - { - debug(error); - } - - _onWidgetRealize() - { - this.widget.disconnect(this._realizeSignal); - this._realizeSignal = null; - - if(this.widget.get_error) { - const error = this.widget.get_error(); - if(error) { - debug('player widget error detected'); - debug(error); - - this.widget.add_css_class('blackbackground'); - } - } - - const root = this.widget.get_root(); - if(!root) return; - - this.closeRequestSignal = root.connect( - 'close-request', this._onCloseRequest.bind(this) - ); - } - - _onWindowMap(window) - { - this.windowMapped = true; - this._playFirstTrack(); - } - - _onCloseRequest(window) - { - this._performCloseCleanup(window); - - if(this.state === GstClapper.ClapperState.STOPPED) - return window.run_dispose(); - - this.quitOnStop = true; - this.stop(); - } - - _onSettingsKeyChanged(settings, key) - { - let root, value, action; - - switch(key) { - case 'after-playback': - this.clappersink.keep_last_frame = (settings.get_int(key) === 1); - break; - case 'seeking-mode': - switch(settings.get_int(key)) { - case 2: /* Fast */ - this.set_seek_mode(GstClapper.ClapperSeekMode.FAST); - break; - case 1: /* Accurate */ - this.set_seek_mode(GstClapper.ClapperSeekMode.ACCURATE); - break; - default: /* Normal */ - this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT); - break; - } - break; - case 'dark-theme': - /* TODO: Remove libadwaita alpha2 compat someday */ - if (Adw.StyleManager != null) { - const styleManager = Adw.StyleManager.get_default(); - styleManager.color_scheme = (settings.get_boolean(key)) - ? Adw.ColorScheme.FORCE_DARK - : Adw.ColorScheme.FORCE_LIGHT; - } - else { - const gtkSettings = Gtk.Settings.get_default(); - gtkSettings.gtk_application_prefer_dark_theme = settings.get_boolean(key); - } - break; - case 'render-shadows': - root = this.widget.get_root(); - if(!root) break; - - const gpuClass = 'gpufriendly'; - const renderShadows = settings.get_boolean(key); - const hasShadows = !root.has_css_class(gpuClass); - - if(renderShadows === hasShadows) - break; - - action = (renderShadows) ? 'remove' : 'add'; - root[action + '_css_class'](gpuClass); - break; - case 'audio-offset': - value = Math.round(settings.get_int(key) * -Gst.MSECOND); - this.set_audio_video_offset(value); - debug(`set audio-video offset: ${value}`); - break; - case 'play-flags': - const initialFlags = this.pipeline.flags; - const settingsFlags = settings.get_int(key); - - if(initialFlags === settingsFlags) - break; - - this.pipeline.flags = settingsFlags; - debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`); - break; - case 'webserver-enabled': - const webserverEnabled = settings.get_boolean('webserver-enabled'); - - if(webserverEnabled) { - if(!WebServer) { - /* Probably most users will not use this, - * so conditional import for faster startup */ - WebServer = imports.src.webServer.WebServer; - } - - if(!this.webserver) { - this.webserver = new WebServer(settings.get_int('webserver-port')); - this.webserver.passMsgData = this.receiveWs.bind(this); - } - this.webserver.startListening(); - } - else if(this.webserver) { - this.webserver.stopListening(); - } - break; - case 'webserver-port': - if(!this.webserver) - break; - - this.webserver.setListeningPort(settings.get_int(key)); - break; - default: - break; - } - } -}); diff --git a/src/playlist.js b/src/playlist.js deleted file mode 100644 index 71cb8e8b..00000000 --- a/src/playlist.js +++ /dev/null @@ -1,378 +0,0 @@ -const { Gdk, GLib, GObject, Gtk, Pango } = imports.gi; -const Debug = imports.src.debug; -const Misc = imports.src.misc; - -const { debug, warn } = Debug; - -var RepeatMode = { - NONE: 0, - TRACK: 1, - PLAYLIST: 2, - SHUFFLE: 3, -}; - -const repeatIcons = [ - 'media-playlist-consecutive-symbolic', - 'media-playlist-repeat-song-symbolic', - 'media-playlist-repeat-symbolic', - 'media-playlist-shuffle-symbolic', -]; - -var PlaylistWidget = GObject.registerClass({ - GTypeName: 'ClapperPlaylistWidget', -}, -class ClapperPlaylistWidget extends Gtk.ListBox -{ - _init() - { - super._init({ - selection_mode: Gtk.SelectionMode.NONE, - }); - this.activeRowId = -1; - this.repeatMode = RepeatMode.NONE; - this.add_css_class('clapperplaylist'); - - this.connect('row-activated', this._onRowActivated.bind(this)); - } - - addItem(uri) - { - const item = new PlaylistItem(uri); - this.append(item); - } - - removeItem(item) - { - const itemIndex = item.get_index(); - - if(itemIndex === this.activeRowId) { - this.activate_action('window.close', null); - return; - } - - if(itemIndex < this.activeRowId) - this.activeRowId--; - - this.remove(item); - } - - removeAll() - { - let oldItem; - while((oldItem = this.get_row_at_index(0))) - this.remove(oldItem); - - this.activeRowId = -1; - } - - nextTrack() - { - return this._switchTrack(false); - } - - prevTrack() - { - return this._switchTrack(true); - } - - getActiveRow() - { - return this.get_row_at_index(this.activeRowId); - } - - getPlaylist(useFilePaths) - { - const playlist = []; - let index = 0; - let item; - - while((item = this.get_row_at_index(index))) { - const path = (useFilePaths && item.isLocalFile) - ? GLib.filename_from_uri(item.uri)[0] - : item.uri; - - playlist.push(path); - index++; - } - - return playlist; - } - - getActiveFilename() - { - const row = this.getActiveRow(); - if(!row) return null; - - return row.filename; - } - - changeActiveRow(rowId) - { - const row = this.get_row_at_index(rowId); - if(!row) - return false; - - row.activate(); - - return true; - } - - changeRepeatMode(mode) - { - const lastMode = Object.keys(RepeatMode).length - 1; - const row = this.getActiveRow(); - if(!row) return null; - - if(mode < 0 || mode > lastMode) { - warn(`ignored invalid repeat mode value: ${mode}`); - return; - } - - if(mode >= 0) - this.repeatMode = mode; - else { - this.repeatMode++; - if(this.repeatMode > lastMode) - this.repeatMode = 0; - } - - const repeatButton = row.child.get_first_child(); - repeatButton.icon_name = repeatIcons[this.repeatMode]; - - debug(`set repeat mode: ${this.repeatMode}`); - } - - _deactivateActiveItem(isRemoveChange) - { - if(this.activeRowId < 0) - return; - - const row = this.getActiveRow(); - if(!row) return null; - - const repeatButton = row.child.get_first_child(); - repeatButton.sensitive = false; - repeatButton.icon_name = 'open-menu-symbolic'; - - if(isRemoveChange) { - const removeButton = row.child.get_last_child(); - removeButton.icon_name = 'list-remove-symbolic'; - } - } - - _switchTrack(isPrevious) - { - const rowId = (isPrevious) - ? this.activeRowId - 1 - : this.activeRowId + 1; - - return this.changeActiveRow(rowId); - } - - _onRowActivated(listBox, row) - { - const { player } = this.get_ancestor(Gtk.Grid); - const repeatButton = row.child.get_first_child(); - const removeButton = row.child.get_last_child(); - - this._deactivateActiveItem(true); - repeatButton.sensitive = true; - repeatButton.icon_name = repeatIcons[this.repeatMode]; - removeButton.icon_name = 'window-close-symbolic'; - - this.activeRowId = row.get_index(); - player.set_uri(row.uri); - } - - _handleStreamEnded(player) - { - /* Seek to beginning when repeating track - * or playlist with only one item */ - if( - this.repeatMode === RepeatMode.TRACK - || (this.repeatMode !== RepeatMode.NONE - && this.activeRowId === 0 - && !this.get_row_at_index(1)) - ) { - debug('seeking to beginning'); - - player.seek(0); - return true; - } - - if(this.repeatMode === RepeatMode.SHUFFLE) { - const playlistIds = []; - let index = 0; - - debug('selecting random playlist item'); - - while(this.get_row_at_index(index)) { - /* We prefer to not repeat the same track */ - if(index !== this.activeRowId) - playlistIds.push(index); - - index++; - } - - /* We always have non-empty array here, - * otherwise seek to beginning is performed */ - const randomId = playlistIds[ - Math.floor(Math.random() * playlistIds.length) - ]; - debug(`selected random playlist item: ${randomId}`); - - return this.changeActiveRow(randomId); - } - - if(this.nextTrack()) - return true; - - if(this.repeatMode === RepeatMode.PLAYLIST) - return this.changeActiveRow(0); - - this._deactivateActiveItem(false); - - return false; - } -}); - -let PlaylistItem = GObject.registerClass({ - GTypeName: 'ClapperPlaylistItem', -}, -class ClapperPlaylistItem extends Gtk.ListBoxRow -{ - _init(uri) - { - super._init({ - can_focus: false, - }); - - this.uri = uri; - this.isLocalFile = false; - - let filename; - if(Misc.getUriProtocol(uri) === 'file') { - filename = GLib.path_get_basename( - GLib.filename_from_uri(uri)[0] - ); - this.isLocalFile = true; - } - this.filename = filename || uri; - this.set_tooltip_text(this.filename); - - const box = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - spacing: 6, - margin_start: 6, - margin_end: 6, - height_request: 22, - }); - const repeatButton = new Gtk.Button({ - icon_name: 'open-menu-symbolic', - sensitive: false, - }); - repeatButton.add_css_class('flat'); - repeatButton.add_css_class('circular'); - repeatButton.connect('clicked', this._onRepeatClicked.bind(this)); - const label = new Gtk.Label({ - label: this.filename, - single_line_mode: true, - ellipsize: Pango.EllipsizeMode.END, - width_chars: 5, - hexpand: true, - halign: Gtk.Align.START, - }); - const removeButton = new Gtk.Button({ - icon_name: 'list-remove-symbolic', - }); - removeButton.add_css_class('flat'); - removeButton.add_css_class('circular'); - removeButton.connect('clicked', this._onRemoveClicked.bind(this)); - - box.append(repeatButton); - box.append(label); - box.append(removeButton); - this.set_child(box); - -/* FIXME: D&D inside popover is broken in GTK4 - const dragSource = new Gtk.DragSource({ - actions: Gdk.DragAction.MOVE - }); - dragSource.connect('prepare', this._onDragPrepare.bind(this)); - dragSource.connect('drag-begin', this._onDragBegin.bind(this)); - dragSource.connect('drag-end', this._onDragEnd.bind(this)); - this.add_controller(dragSource); - - const dropTarget = new Gtk.DropTarget({ - actions: Gdk.DragAction.MOVE, - preload: true, - }); - dropTarget.set_gtypes([PlaylistItem]); - dropTarget.connect('enter', this._onEnter.bind(this)); - dropTarget.connect('drop', this._onDrop.bind(this)); - this.add_controller(dropTarget); -*/ - } - - _onRepeatClicked(button) - { - const listBox = this.get_ancestor(Gtk.ListBox); - - listBox.changeRepeatMode(); - } - - _onRemoveClicked(button) - { - const listBox = this.get_ancestor(Gtk.ListBox); - - listBox.removeItem(this); - } - - _onDragPrepare(source, x, y) - { - const widget = source.get_widget(); - const paintable = new Gtk.WidgetPaintable({ widget }); - const staticImg = paintable.get_current_image(); - - source.set_icon(staticImg, x, y); - - return Gdk.ContentProvider.new_for_value(widget); - } - - _onDragBegin(source, drag) - { - this.child.set_opacity(0.3); - } - - _onDragEnd(source, drag, deleteData) - { - this.child.set_opacity(1.0); - } - - _onEnter(target, x, y) - { - return (target.value) - ? Gdk.DragAction.MOVE - : 0; - } - - _onDrop(target, value, x, y) - { - const destIndex = this.get_index(); - const targetIndex = value.get_index(); - - if(destIndex === targetIndex) - return true; - - const listBox = this.get_ancestor(Gtk.ListBox); - - if(listBox && destIndex >= 0) { - listBox.remove(value); - listBox.insert(value, destIndex); - - return true; - } - - return false; - } -}); diff --git a/src/prefs.js b/src/prefs.js deleted file mode 100644 index 570f7420..00000000 --- a/src/prefs.js +++ /dev/null @@ -1,563 +0,0 @@ -const { Adw, GObject, Gio, Gst, Gtk } = imports.gi; -const Debug = imports.src.debug; -const Misc = imports.src.misc; - -const { debug } = Debug; -const { settings } = Misc; - -/* PlayFlags are not exported through GI */ -Gst.PlayFlags = { - VIDEO: 1, - AUDIO: 2, - TEXT: 4, - VIS: 8, - SOFT_VOLUME: 16, - NATIVE_AUDIO: 32, - NATIVE_VIDEO: 64, - DOWNLOAD: 128, - BUFFERING: 256, - DEINTERLACE: 512, - SOFT_COLORBALANCE: 1024, - FORCE_FILTERS: 2048, - FORCE_SW_DECODERS: 4096, -}; - -const widgetOpts = { - halign: Gtk.Align.CENTER, - valign: Gtk.Align.CENTER, -}; - -function getCommonProps() -{ - return { - 'schema-name': GObject.ParamSpec.string( - 'schema-name', - 'GSchema setting name', - 'Name of the setting to bind', - GObject.ParamFlags.WRITABLE, - null - ), - }; -} - -const flags = Gio.SettingsBindFlags.DEFAULT; - -let PrefsActionRow = GObject.registerClass({ - GTypeName: 'ClapperPrefsActionRow', - Properties: getCommonProps(), -}, -class ClapperPrefsActionRow extends Adw.ActionRow -{ - _init(widget) - { - super._init(); - - this._schemaName = null; - this._bindProp = null; - - if(widget) { - this.add_suffix(widget); - this.set_activatable_widget(widget); - } - } - - set schema_name(value) - { - this._schemaName = value; - } - - vfunc_realize() - { - super.vfunc_realize(); - - if(this._schemaName && this._bindProp) { - settings.bind(this._schemaName, - this.activatable_widget, this._bindProp, flags - ); - } - this._schemaName = null; - } -}); - -let PrefsSubpageRow = GObject.registerClass({ - GTypeName: 'ClapperPrefsSubpageRow', - Properties: getCommonProps(), -}, -class ClapperPrefsSubpageRow extends Adw.ActionRow -{ - _init(widget) - { - super._init({ - activatable: true, - }); - - this._prefsSubpage = null; - - const icon = new Gtk.Image({ - icon_name: 'go-next-symbolic', - }); - this.add_suffix(icon); - } - - vfunc_activate() - { - super.vfunc_activate(); - - if(!this._prefsSubpage) - this._prefsSubpage = this._createSubpage(); - - const prefs = this.get_ancestor(PrefsWindow); - prefs.present_subpage(this._prefsSubpage); - } - - _createSubpage() - { - /* For override */ - return null; - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsSwitch', - Properties: { - 'custom-icon-name': GObject.ParamSpec.string( - 'custom-icon-name', - 'Icon name', - 'Name of the icon', - GObject.ParamFlags.WRITABLE | GObject.ParamFlags.CONSTRUCT_ONLY, - null - ), - 'custom-icon-subtitle': GObject.ParamSpec.string( - 'custom-icon-subtitle', - 'Icon subtitle', - 'Text below the icon', - GObject.ParamFlags.WRITABLE | GObject.ParamFlags.CONSTRUCT_ONLY, - null - ), - } -}, -class ClapperPrefsSwitch extends PrefsActionRow -{ - _init(opts) - { - super._init(null); - this._bindProp = 'active'; - - if(opts.custom_icon_name || opts.custom_icon_subtitle) { - const box = new Gtk.Box({ - margin_top: 2, - orientation: Gtk.Orientation.VERTICAL, - valign: Gtk.Align.CENTER, - }); - const customIcon = new Gtk.Image({ - icon_name: opts.custom_icon_name || null, - }); - box.append(customIcon); - - const customLabel = new Gtk.Label({ - label: opts.custom_icon_subtitle || '', - }); - customLabel.add_css_class('subtitle'); - box.append(customLabel); - - this.add_suffix(box); - } - - const sw = new Gtk.Switch(widgetOpts); - this.add_suffix(sw); - this.set_activatable_widget(sw); - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsPlayFlagSwitch', - Properties: { - 'play-flag': GObject.ParamSpec.int( - 'play-flag', - 'PlayFlag', - 'Value of the gstreamer play flag to toggle', - GObject.ParamFlags.WRITABLE, - 1, 4096, 1, - ), - }, -}, -class ClapperPrefsPlayFlagSwitch extends PrefsActionRow -{ - _init() - { - super._init(new Gtk.Switch(widgetOpts)); - - this._flag = 1; - this._doneRealize = false; - } - - set play_flag(value) - { - this._flag = value; - } - - vfunc_realize() - { - super.vfunc_realize(); - - if(!this._doneRealize) { - const playFlags = settings.get_int('play-flags'); - - this.activatable_widget.active = ( - (playFlags & this._flag) === this._flag - ); - this.activatable_widget.connect( - 'notify::active', this._onPlayFlagToggled.bind(this) - ); - } - this._doneRealize = true; - } - - _onPlayFlagToggled() - { - let playFlags = settings.get_int('play-flags'); - - if(this.activatable_widget.active) - playFlags |= this._flag; - else - playFlags &= ~this._flag; - - settings.set_int('play-flags', playFlags); - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsSpin', - Properties: { - 'spin-adjustment': GObject.ParamSpec.object( - 'spin-adjustment', - 'GtkAdjustment', - 'Custom GtkAdjustment for spin button', - GObject.ParamFlags.WRITABLE, - Gtk.Adjustment - ), - }, -}, -class ClapperPrefsSpin extends PrefsActionRow -{ - _init() - { - super._init(new Gtk.SpinButton(widgetOpts)); - this._bindProp = 'value'; - } - - set spin_adjustment(value) - { - this.activatable_widget.set_adjustment(value); - } -}); - -let PrefsPluginFeature = GObject.registerClass({ - GTypeName: 'ClapperPrefsPluginFeature', -}, -class ClapperPrefsPluginFeature extends Adw.ActionRow -{ - _init(featureObj) - { - super._init({ - title: featureObj.name, - }); - - const enableSwitch = new Gtk.Switch(widgetOpts); - const spinButton = new Gtk.SpinButton(widgetOpts); - - spinButton.set_range(0, 512); - spinButton.set_increments(1, 1); - - enableSwitch.active = featureObj.enabled; - spinButton.value = featureObj.rank; - this.currentRank = featureObj.rank; - - this.add_suffix(enableSwitch); - this.add_suffix(spinButton); - - enableSwitch.bind_property('active', spinButton, 'sensitive', - GObject.BindingFlags.SYNC_CREATE - ); - - enableSwitch.connect('notify::active', this._onSwitchActivate.bind(this)); - spinButton.connect('value-changed', this._onValueChanged.bind(this)); - } - - _updateRanking(data) - { - settings.set_string('plugin-ranking', JSON.stringify(data)); - } - - _onSwitchActivate(enableSwitch) - { - const { settingsData } = this.get_ancestor(PrefsPluginRankingSubpage); - const pluginExp = this.get_ancestor(PrefsPluginExpander); - - if(enableSwitch.active) { - settingsData[this.title] = this.currentRank; - pluginExp.modCount++; - } - else if(settingsData[this.title] != null) { - delete settingsData[this.title]; - pluginExp.modCount--; - } - - this._updateRanking(settingsData); - } - - _onValueChanged(spinButton) - { - const { settingsData } = this.get_ancestor(PrefsPluginRankingSubpage); - - this.currentRank = spinButton.value; - settingsData[this.title] = this.currentRank; - - this._updateRanking(settingsData); - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsFont', -}, -class ClapperPrefsFont extends PrefsActionRow -{ - _init() - { - const opts = { - use_font: true, - use_size: true, - }; - Object.assign(opts, widgetOpts); - - super._init(new Gtk.FontButton(opts)); - this._bindProp = 'font'; - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsCombo', - Properties: getCommonProps(), -}, -class ClapperPrefsCombo extends Adw.ComboRow -{ - _init() - { - super._init(); - this._schemaName = null; - } - - set schema_name(value) - { - this._schemaName = value; - } - - vfunc_realize() - { - super.vfunc_realize(); - - if(this._schemaName) - settings.bind(this._schemaName, this, 'selected', flags); - - this._schemaName = null; - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsExpander', - Properties: getCommonProps(), -}, -class ClapperPrefsExpander extends Adw.ExpanderRow -{ - _init() - { - super._init({ - show_enable_switch: true, - }); - } - - set schema_name(value) - { - settings.bind(value, this, 'enable-expansion', flags); - } -}); - -GObject.registerClass({ - GTypeName: 'ClapperPrefsPluginRankingSubpageRow', -}, -class ClapperPrefsPluginRankingSubpageRow extends PrefsSubpageRow -{ - _createSubpage() - { - return new PrefsPluginRankingSubpage(); - } -}); - -let PrefsPluginExpander = GObject.registerClass({ - GTypeName: 'ClapperPrefsPluginExpander', -}, -class ClapperPrefsPluginExpander extends Adw.ExpanderRow -{ - _init(plugin, modCount) - { - super._init({ - title: plugin, - show_enable_switch: false, - }); - this.modCount = modCount; - - this.expandSignal = this.connect( - 'notify::expanded', this._onExpandedNotify.bind(this) - ); - } - - set modCount(value) - { - this._modCount = value; - this.icon_name = (value > 0) ? 'dialog-information-symbolic' : null; - - debug(`Plugin ${this.title} has ${value} modified features`); - } - - get modCount() - { - return this._modCount; - } - - _onExpandedNotify() - { - if(!this.expanded) - return; - - this.disconnect(this.expandSignal); - this.expandSignal = null; - - const { pluginsData } = this.get_ancestor(PrefsPluginRankingSubpage); - - pluginsData[this.title].sort((a, b) => - (a.name > b.name) - (a.name < b.name) - ); - const featuresNames = Object.keys(pluginsData[this.title]); - debug(`Adding ${featuresNames.length} features to the list of plugin: ${this.title}`); - - for(let featureObj of pluginsData[this.title]) { - const prefsPluginFeature = new PrefsPluginFeature(featureObj); - - /* TODO: Remove old libadwaita compat */ - if(this.add_row) - this.add_row(prefsPluginFeature); - else - this.add(prefsPluginFeature); - } - } -}); - -let PrefsPluginRankingSubpage = GObject.registerClass({ - GTypeName: 'ClapperPrefsPluginRankingSubpage', - Template: Misc.getResourceUri('ui/preferences-plugin-ranking-subpage.ui'), - InternalChildren: ['decoders_group'], -}, -class ClapperPrefsPluginRankingSubpage extends Gtk.Box -{ - _init() - { - super._init(); - - if(!Gst.is_initialized()) - Gst.init(null); - - const gstRegistry = Gst.Registry.get(); - const decoders = gstRegistry.feature_filter(this._decodersFilterCb, false); - - const plugins = {}; - const mods = {}; - this.settingsData = {}; - - /* In case someone messed up gsettings values */ - try { - this.settingsData = JSON.parse(settings.get_string('plugin-ranking')); - /* Might be an array in older Clapper versions */ - if(Array.isArray(this.settingsData)) - this.settingsData = {}; - } - catch(err) { /* Ignore */ } - - for(let decoder of decoders) { - const pluginName = decoder.get_plugin_name(); - - /* Do not add unsupported plugins */ - switch(pluginName) { - case 'playback': - continue; - default: - break; - } - - if(!plugins[pluginName]) - plugins[pluginName] = []; - - const decName = decoder.get_name(); - const isModified = (this.settingsData[decName] != null); - - plugins[pluginName].push({ - name: decName, - rank: decoder.get_rank(), - enabled: isModified, - }); - - if(isModified) { - if(!mods[pluginName]) - mods[pluginName] = 0; - - mods[pluginName]++; - } - } - - const pluginsNames = Object.keys(plugins); - debug(`Adding ${pluginsNames.length} found plugins to the list`); - - this.pluginsData = pluginsNames.sort().reduce((res, key) => - (res[key] = plugins[key], res), {} - ); - - for(let plugin in this.pluginsData) { - const modCount = mods[plugin] || 0; - this._decoders_group.add(new PrefsPluginExpander(plugin, modCount)); - } - } - - _decodersFilterCb(feature) - { - return ( - feature.list_is_type - && feature.list_is_type(Gst.ELEMENT_FACTORY_TYPE_DECODER) - ); - } - - _onReturnClicked(button) - { - const prefs = this.get_ancestor(PrefsWindow); - prefs.close_subpage(); - } -}); - -var PrefsWindow = GObject.registerClass({ - GTypeName: 'ClapperPrefsWindow', - Template: Misc.getResourceUri('ui/preferences-window.ui'), -}, -class ClapperPrefsWindow extends Adw.PreferencesWindow -{ - _init(window) - { - super._init({ - transient_for: window, - }); - - /* FIXME: old libadwaita compat, should be - * normally in prefs UI file */ - this.can_swipe_back = true; - this.can_navigate_back = true; - - this.show(); - } -}); diff --git a/src/revealers.js b/src/revealers.js deleted file mode 100644 index 7aac9f64..00000000 --- a/src/revealers.js +++ /dev/null @@ -1,427 +0,0 @@ -const { GLib, GObject, Gtk, Pango } = imports.gi; -const { HeaderBar } = imports.src.headerbar; -const Debug = imports.src.debug; -const DBus = imports.src.dbus; -const Misc = imports.src.misc; - -const { debug } = Debug; -const { settings } = Misc; - -var CustomRevealer = GObject.registerClass({ - GTypeName: 'ClapperCustomRevealer', -}, -class ClapperCustomRevealer extends Gtk.Revealer -{ - _init(opts) - { - opts = opts || {}; - - const defaults = { - visible: false, - can_focus: false, - transition_duration: 800, - }; - Object.assign(opts, defaults); - - super._init(opts); - - this.revealerName = ''; - this.bind_property('child_revealed', this, 'visible', - GObject.BindingFlags.DEFAULT - ); - } - - revealChild(isReveal) - { - if(this.reveal_child === isReveal) - return; - - if(isReveal) - this.visible = true; - - this.reveal_child = isReveal; - } -}); - -var RevealerTop = GObject.registerClass({ - GTypeName: 'ClapperRevealerTop', -}, -class ClapperRevealerTop extends CustomRevealer -{ - _init() - { - super._init({ - transition_type: Gtk.RevealerTransitionType.CROSSFADE, - valign: Gtk.Align.START, - }); - this.revealerName = 'top'; - this._requestedTransition = this.transition_type; - - const initTime = GLib.DateTime.new_now_local().format('%X'); - this.timeFormat = (initTime.length > 8) - ? `%I${Misc.timeColon}%M %p` - : `%H${Misc.timeColon}%M`; - - this.mediaTitle = new Gtk.Label({ - ellipsize: Pango.EllipsizeMode.END, - halign: Gtk.Align.START, - valign: Gtk.Align.CENTER, - margin_start: 10, - margin_end: 10, - visible: false, - }); - this.mediaTitle.add_css_class('tvtitle'); - - this.currentTime = new Gtk.Label({ - halign: Gtk.Align.END, - valign: Gtk.Align.CENTER, - margin_start: 10, - margin_end: 10, - }); - this.currentTime.add_css_class('tvtime'); - - this.endTime = new Gtk.Label({ - halign: Gtk.Align.END, - valign: Gtk.Align.START, - margin_start: 10, - margin_end: 10, - visible: false, - }); - this.endTime.add_css_class('tvendtime'); - - const revealerBox = new Gtk.Box({ - orientation: Gtk.Orientation.VERTICAL, - }); - this.headerBar = new HeaderBar(); - revealerBox.append(this.headerBar); - - this.revealerGrid = new Gtk.Grid({ - column_spacing: 4, - margin_top: 8, - margin_start: 8, - margin_end: 8, - visible: false, - }); - this.revealerGrid.add_css_class('revealertopgrid'); - - const topLeftBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - }); - topLeftBox.add_css_class('osd'); - topLeftBox.add_css_class('roundedcorners'); - topLeftBox.append(this.mediaTitle); - - const topSpacerBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - hexpand: true, - }); - - const topRightBox = new Gtk.Box({ - orientation: Gtk.Orientation.VERTICAL, - halign: Gtk.Align.END, - }); - topRightBox.add_css_class('osd'); - topRightBox.add_css_class('roundedcorners'); - topRightBox.append(this.currentTime); - topRightBox.append(this.endTime); - - this.revealerGrid.attach(topLeftBox, 0, 0, 1, 1); - this.revealerGrid.attach(topSpacerBox, 1, 0, 1, 1); - this.revealerGrid.attach(topRightBox, 2, 0, 1, 2); - revealerBox.append(this.revealerGrid); - - this.set_child(revealerBox); - - this.mediaTitle.bind_property('visible', this.endTime, 'visible', - GObject.BindingFlags.DEFAULT - ); - this.connect('notify::child-revealed', this._onTopRevealed.bind(this)); - } - - set title(text) - { - this.mediaTitle.label = text; - } - - get title() - { - return this.mediaTitle.label; - } - - set showTitle(isShow) - { - this.mediaTitle.visible = isShow; - } - - get showTitle() - { - return this.mediaTitle.visible; - } - - setTimes(currTime, endTime, isEndKnown) - { - const now = currTime.format(this.timeFormat); - this.currentTime.label = now; - - const end = (isEndKnown) - ? endTime.format(this.timeFormat) - : 'unknown'; - - this.endTime.label = _('Ends at: %s').format(end); - - /* Make sure that next timeout is always run after clock changes, - * by delaying it for additional few milliseconds */ - const nextUpdate = 60004 - parseInt(currTime.get_seconds() * 1000); - debug(`updated current time: ${now}, ends at: ${end}`); - - return nextUpdate; - } - - setFullscreenMode(isFullscreen, isMobileMonitor) - { - const isTvMode = (isFullscreen && !isMobileMonitor); - - this.headerBar.visible = !isTvMode; - this.revealerGrid.visible = isTvMode; - - this.headerBar.extraButtonsBox.visible = !isFullscreen; - - this._requestedTransition = (isTvMode) - ? Gtk.RevealerTransitionType.SLIDE_DOWN - : Gtk.RevealerTransitionType.CROSSFADE; - - const isRevealed = this.child_revealed; - - /* FIXME: Changing transition in middle or when not fully - * revealed has dire consequences, seems to be a GTK4 bug */ - if(isRevealed && isRevealed === this.reveal_child) - this._checkSwitchTransitionType(); - } - - _checkSwitchTransitionType() - { - if(this.transition_type !== this._requestedTransition) - this.transition_type = this._requestedTransition; - } - - _onTopRevealed() - { - if(this.child_revealed) { - /* TODO: Move before above if statement when GTK4 can handle - * changing transition type while not fully revealed */ - this._checkSwitchTransitionType(); - - const clapperWidget = this.root.child; - if(!clapperWidget) return; - - clapperWidget._setHideControlsTimeout(); - } - } -}); - -var RevealerBottom = GObject.registerClass({ - GTypeName: 'ClapperRevealerBottom', -}, -class ClapperRevealerBottom extends CustomRevealer -{ - _init() - { - super._init({ - transition_type: Gtk.RevealerTransitionType.SLIDE_UP, - valign: Gtk.Align.END, - }); - - this.revealerName = 'bottom'; - this.revealerBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - margin_start: 8, - margin_end: 8, - margin_bottom: 8, - visible: false, - }); - this.revealerBox.add_css_class('osd'); - this.revealerBox.add_css_class('roundedcorners'); - - this.set_child(this.revealerBox); - - const motionController = new Gtk.EventControllerMotion(); - motionController.connect('motion', this._onMotion.bind(this)); - this.add_controller(motionController); - } - - append(widget) - { - this.revealerBox.append(widget); - } - - remove(widget) - { - this.revealerBox.remove(widget); - } - - setLayoutMargins(layoutWidth) - { - const maxWidth = 1720; - - const margin = (layoutWidth > maxWidth) - ? (layoutWidth - maxWidth) / 2 - : 0; - - this.margin_start = margin; - this.margin_end = margin; - } - - _onMotion(controller, x, y) - { - const clapperWidget = this.root.child; - clapperWidget._clearTimeout('hideControls'); - } -}); - -var ControlsRevealer = GObject.registerClass({ - GTypeName: 'ClapperControlsRevealer', -}, -class ClapperControlsRevealer extends Gtk.Revealer -{ - _init() - { - super._init({ - transition_duration: 600, - transition_type: Gtk.RevealerTransitionType.SLIDE_DOWN, - reveal_child: true, - }); - - this.connect('notify::child-revealed', this._onControlsRevealed.bind(this)); - } - - toggleReveal() - { - /* Prevent interrupting transition */ - if(this.reveal_child !== this.child_revealed) - return; - - const { widget } = this.root.child.player; - - if(this.child_revealed) { - const [width] = this.root.get_default_size(); - const height = widget.get_height(); - - this.add_tick_callback( - this._onUnrevealTick.bind(this, widget, width, height) - ); - } - else - this.visible = true; - - widget.height_request = widget.get_height(); - this.reveal_child ^= true; - - const isFloating = !this.reveal_child; - DBus.shellWindowEval('make_above', isFloating); - - const isStick = (isFloating && settings.get_boolean('floating-stick')); - DBus.shellWindowEval('stick', isStick); - - this.root.child.refreshWindowTitle(this.root.title); - } - - _onControlsRevealed() - { - if(this.child_revealed) { - const clapperWidget = this.root.child; - if(!clapperWidget) return; - - const [width, height] = this.root.get_default_size(); - - clapperWidget.player.widget.height_request = -1; - this.root.set_default_size(width, height); - } - } - - _onUnrevealTick(playerWidget, width, height) - { - const isRevealed = this.child_revealed; - - if(!isRevealed) { - playerWidget.height_request = -1; - this.visible = false; - } - this.root.set_default_size(width, height); - - return isRevealed; - } -}); - -var ButtonsRevealer = GObject.registerClass({ - GTypeName: 'ClapperButtonsRevealer', -}, -class ClapperButtonsRevealer extends Gtk.Revealer -{ - _init(trType, toggleButton) - { - super._init({ - transition_duration: 500, - transition_type: Gtk.RevealerTransitionType[trType], - }); - - const revealerBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - }); - this.set_child(revealerBox); - - if(toggleButton) { - toggleButton.connect('clicked', this._onToggleButtonClicked.bind(this)); - this.connect('notify::reveal-child', this._onRevealChild.bind(this, toggleButton)); - this.connect('notify::child-revealed', this._onChildRevealed.bind(this, toggleButton)); - } - } - - append(widget) - { - this.get_child().append(widget); - } - - revealInstantly(isReveal) - { - if(this.child_revealed === isReveal) - return; - - const initialDuration = this.transition_duration; - - this.transition_duration = 0; - this.reveal_child = isReveal; - this.transition_duration = initialDuration; - } - - _setRotateClass(icon, isAdd) - { - const cssClass = 'halfrotate'; - const hasClass = icon.has_css_class(cssClass); - - if(!hasClass && isAdd) - icon.add_css_class(cssClass); - else if(hasClass && !isAdd) - icon.remove_css_class(cssClass); - } - - _onToggleButtonClicked(button) - { - this.set_reveal_child(!this.reveal_child); - } - - _onRevealChild(button) - { - if(this.reveal_child !== this.child_revealed) - this._setRotateClass(button.child, true); - } - - _onChildRevealed(button) - { - if(!this.child_revealed) - button.setPrimaryIcon(); - else - button.setSecondaryIcon(); - - this._setRotateClass(button.child, false); - } -}); diff --git a/src/webHelpers.js b/src/webHelpers.js deleted file mode 100644 index 79e25fbd..00000000 --- a/src/webHelpers.js +++ /dev/null @@ -1,30 +0,0 @@ -const { Soup } = imports.gi; -const ByteArray = imports.byteArray; -const Debug = imports.src.debug; - -const { debug } = Debug; - -function parseData(dataType, bytes) -{ - if(dataType !== Soup.WebsocketDataType.TEXT) { - debug('ignoring non-text WebSocket message'); - return [false]; - } - - let parsedMsg = null; - const msg = bytes.get_data(); - - try { - parsedMsg = JSON.parse(ByteArray.toString(msg)); - } - catch(err) { - debug(err); - } - - if(!parsedMsg || !parsedMsg.action) { - debug('no "action" in parsed WebSocket message'); - return [false]; - } - - return [true, parsedMsg]; -} diff --git a/src/webServer.js b/src/webServer.js deleted file mode 100644 index 9ba0e58b..00000000 --- a/src/webServer.js +++ /dev/null @@ -1,141 +0,0 @@ -const { Soup, GObject } = imports.gi; -const Debug = imports.src.debug; -const WebHelpers = imports.src.webHelpers; - -const { debug } = Debug; - -var WebServer = GObject.registerClass({ - GTypeName: 'ClapperWebServer', -}, -class ClapperWebServer extends Soup.Server -{ - _init(port) - { - super._init(); - - this.isListening = false; - this.listeningPort = null; - this.wsConns = []; - - if(port) - this.setListeningPort(port); - } - - setListeningPort(port) - { - if(!port) - return; - - const wasListening = this.isListening; - - if(wasListening) - this.stopListening(); - - this.listeningPort = port; - - if(wasListening) - this.startListening(); - } - - startListening() - { - if(this.isListening || !this.listeningPort) - return; - - let isListening = false; - - this.add_handler('/', this._onDefaultAccess.bind(this)); - this.add_websocket_handler('/websocket', null, null, this._onWsConnection.bind(this)); - - try { - isListening = this.listen_all(this.listeningPort, Soup.ServerListenOptions.IPV4_ONLY); - } - catch(err) { - debug(err); - } - - if(isListening) { - const uris = this.get_uris(); - const usedPort = uris[0].get_port(); - debug(`WebSocket server started listening on port: ${usedPort}`); - } - else { - debug(new Error('WebSocket server could not start listening')); - this._closeCleanup(); - } - - this.isListening = isListening; - } - - stopListening() - { - if(!this.isListening) - return; - - this._closeCleanup(); - this.disconnect(); - - this.isListening = false; - } - - sendMessage(data) - { - for(const connection of this.wsConns) { - if(connection.state !== Soup.WebsocketState.OPEN) - continue; - - connection.send_text(JSON.stringify(data)); - } - } - - passMsgData(action, value) - { - } - - _closeCleanup() - { - while(this.wsConns.length) { - const connection = this.wsConns.pop(); - - if(connection.state !== Soup.WebsocketState.OPEN) - continue; - - connection.close(Soup.WebsocketCloseCode.NORMAL, null); - } - - this.remove_handler('/websocket'); - this.remove_handler('/'); - } - - _onWsConnection(server, msg, path, connection) - { - debug('new WebSocket connection'); - - connection.connect('message', this._onWsMessage.bind(this)); - connection.connect('closed', this._onWsClosed.bind(this)); - - this.wsConns.push(connection); - debug(`total WebSocket connections: ${this.wsConns.length}`); - } - - _onWsMessage(connection, dataType, bytes) - { - const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes); - - if(success) - this.passMsgData(parsedMsg.action, parsedMsg.value); - } - - _onWsClosed(connection) - { - debug('closed WebSocket connection'); - - this.wsConns = this.wsConns.filter(conn => conn !== connection); - debug(`remaining WebSocket connections: ${this.wsConns.length}`); - } - - _onDefaultAccess(server, msg) - { - msg.status_code = 404; - } -}); diff --git a/src/widget.js b/src/widget.js deleted file mode 100644 index c9ef2518..00000000 --- a/src/widget.js +++ /dev/null @@ -1,1046 +0,0 @@ -const { Gdk, Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi; -const { Controls } = imports.src.controls; -const Debug = imports.src.debug; -const Dialogs = imports.src.dialogs; -const Misc = imports.src.misc; -const { Player } = imports.src.player; -const Revealers = imports.src.revealers; - -const { debug } = Debug; -const { settings } = Misc; - -let lastTvScaling = null; - -var Widget = GObject.registerClass({ - GTypeName: 'ClapperWidget', -}, -class ClapperWidget extends Gtk.Grid -{ - _init() - { - super._init(); - - /* load CSS here to allow using this class - * separately as a pre-made GTK widget */ - Misc.loadCustomCss(); - - this.posX = 0; - this.posY = 0; - this.layoutWidth = 0; - - this.isFullscreenMode = false; - this.isMobileMonitor = false; - - this.isSeekable = false; - this.isDragAllowed = false; - this.isSwipePerformed = false; - this.isReleaseKeyEnabled = false; - this.isLongPressed = false; - - this.isCursorInPlayer = false; - this.isPopoverOpen = false; - - this._hideControlsTimeout = null; - this._updateTimeTimeout = null; - this.surfaceMapSignal = null; - - this.needsCursorRestore = false; - - this.overlay = new Gtk.Overlay(); - this.revealerTop = new Revealers.RevealerTop(); - this.revealerBottom = new Revealers.RevealerBottom(); - this.controls = new Controls(); - - this.controlsBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - }); - this.controlsBox.add_css_class('controlsbox'); - this.controlsBox.append(this.controls); - - this.controlsRevealer = new Revealers.ControlsRevealer(); - this.controlsRevealer.set_child(this.controlsBox); - - this.attach(this.overlay, 0, 0, 1, 1); - this.attach(this.controlsRevealer, 0, 1, 1, 1); - - this.player = new Player(); - const playerWidget = this.player.widget; - - this.controls.elapsedButton.scrolledWindow.set_child(this.player.playlistWidget); - - const speedAdjustment = this.controls.elapsedButton.speedScale.get_adjustment(); - speedAdjustment.bind_property( - 'value', this.player, 'rate', GObject.BindingFlags.BIDIRECTIONAL - ); - - const volumeAdjustment = this.controls.volumeButton.volumeScale.get_adjustment(); - volumeAdjustment.bind_property( - 'value', this.player, 'volume', GObject.BindingFlags.BIDIRECTIONAL - ); - - this.player.connect('position-updated', this._onPlayerPositionUpdated.bind(this)); - this.player.connect('duration-changed', this._onPlayerDurationChanged.bind(this)); - this.player.connect('media-info-updated', this._onMediaInfoUpdated.bind(this)); - - this.player.connect('video-decoder-changed', this._onPlayerVideoDecoderChanged.bind(this)); - this.player.connect('audio-decoder-changed', this._onPlayerAudioDecoderChanged.bind(this)); - - this.overlay.set_child(playerWidget); - this.overlay.add_overlay(this.revealerTop); - this.overlay.add_overlay(this.revealerBottom); - - const clickGesture = this._getClickGesture(); - playerWidget.add_controller(clickGesture); - const clickGestureTop = this._getClickGesture(); - this.revealerTop.add_controller(clickGestureTop); - - const longPressGesture = this._getLongPressGesture(); - playerWidget.add_controller(longPressGesture); - const longPressGestureTop = this._getLongPressGesture(); - this.revealerTop.add_controller(longPressGestureTop); - - const dragGesture = this._getDragGesture(); - playerWidget.add_controller(dragGesture); - const dragGestureTop = this._getDragGesture(); - this.revealerTop.add_controller(dragGestureTop); - - const swipeGesture = this._getSwipeGesture(); - playerWidget.add_controller(swipeGesture); - const swipeGestureTop = this._getSwipeGesture(); - this.revealerTop.add_controller(swipeGestureTop); - - const scrollController = this._getScrollController(); - playerWidget.add_controller(scrollController); - const scrollControllerTop = this._getScrollController(); - this.revealerTop.add_controller(scrollControllerTop); - - const motionController = this._getMotionController(); - playerWidget.add_controller(motionController); - const motionControllerTop = this._getMotionController(); - this.revealerTop.add_controller(motionControllerTop); - - const dropTarget = this._getDropTarget(); - playerWidget.add_controller(dropTarget); - - /* Applied only for widget to detect simple action key releases */ - const keyController = new Gtk.EventControllerKey(); - keyController.connect('key-released', this._onKeyReleased.bind(this)); - this.add_controller(keyController); - } - - revealControls() - { - this.revealerTop.revealChild(true); - this.revealerBottom.revealChild(true); - - this._checkSetUpdateTimeInterval(); - - /* Reset timeout if already revealed, otherwise - * timeout will be set after reveal finishes */ - if(this.revealerTop.child_revealed) - this._setHideControlsTimeout(); - } - - toggleFullscreen() - { - const root = this.get_root(); - if(!root) return; - - const un = (this.isFullscreenMode) ? 'un' : ''; - root[`${un}fullscreen`](); - } - - setFullscreenMode(isFullscreen) - { - if(this.isFullscreenMode === isFullscreen) - return; - - debug('changing fullscreen mode'); - this.isFullscreenMode = isFullscreen; - - if(!isFullscreen) - this._clearTimeout('updateTime'); - - this.revealerTop.setFullscreenMode(isFullscreen, this.isMobileMonitor); - this.revealerBottom.revealerBox.visible = isFullscreen; - - this._changeControlsPlacement(isFullscreen); - this.controls.setFullscreenMode(isFullscreen, this.isMobileMonitor); - - if(this.revealerTop.child_revealed) - this._checkSetUpdateTimeInterval(); - - debug(`interface in fullscreen mode: ${isFullscreen}`); - } - - _changeControlsPlacement(isOnTop) - { - if(isOnTop) { - this.controlsBox.remove(this.controls); - this.revealerBottom.append(this.controls); - } - else { - this.revealerBottom.remove(this.controls); - this.controlsBox.append(this.controls); - } - - this.controlsBox.set_visible(!isOnTop); - } - - _onMediaInfoUpdated(player, mediaInfo) - { - /* Set titlebar media title */ - this.updateTitle(mediaInfo); - - /* FIXME: replace number with Gst.CLOCK_TIME_NONE when GJS - * can do UINT64: https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/524 */ - const isLive = (mediaInfo.is_live() || player.duration === 18446744073709552000); - this.isSeekable = (!isLive && mediaInfo.is_seekable()); - - /* Show/hide position scale on LIVE */ - this.controls.setLiveMode(isLive, this.isSeekable); - - /* Update remaining end time if visible */ - this.updateTime(); - - if(this.player.needsTocUpdate) { - if(!isLive) - this.updateChapters(mediaInfo.get_toc()); - - this.player.needsTocUpdate = false; - } - - const streamList = mediaInfo.get_stream_list(); - const parsedInfo = { - videoTracks: [], - audioTracks: [], - subtitleTracks: [] - }; - - for(let info of streamList) { - let type, text, codec; - - switch(info.constructor) { - case GstClapper.ClapperVideoInfo: - type = 'video'; - codec = info.get_codec() || _('Undetermined'); - text = `${codec}, ${info.get_width()}×${info.get_height()}`; - let fps = info.get_framerate(); - fps = Number((fps[0] / fps[1]).toFixed(2)); - if(fps) - text += `@${fps}`; - break; - case GstClapper.ClapperAudioInfo: - type = 'audio'; - codec = info.get_codec() || _('Undetermined'); - if(codec.includes('(')) { - codec = codec.substring( - codec.indexOf('(') + 1, codec.indexOf(')') - ); - } - text = info.get_language() || _('Undetermined'); - text += `, ${codec}, ${info.get_channels()} ` + _('Channels'); - break; - case GstClapper.ClapperSubtitleInfo: - type = 'subtitle'; - const subsLang = info.get_language(); - text = (subsLang) ? subsLang.split(',')[0] : _('Undetermined'); - const subsTitle = Misc.getSubsTitle(info.get_title()); - if(subsTitle) - text += `, ${subsTitle}`; - break; - default: - debug(`unrecognized media info type: ${info.constructor}`); - break; - } - const tracksArr = parsedInfo[`${type}Tracks`]; - if(!tracksArr.length) - { - tracksArr[0] = { - label: _('Disabled'), - type: type, - activeId: -1 - }; - } - tracksArr.push({ - label: text, - type: type, - activeId: info.get_index(), - }); - } - - let anyButtonShown = false; - - for(let type of ['video', 'audio', 'subtitle']) { - const currStream = this.player[`get_current_${type}_track`](); - const activeId = (currStream) ? currStream.get_index() : -1; - - if(currStream && type !== 'subtitle') { - const caps = currStream.get_caps(); - if (caps) - debug(`${type} caps: ${caps.to_string()}`); - } - if(type === 'video') { - const isShowVis = ( - !parsedInfo.videoTracks.length - && parsedInfo.audioTracks.length - ); - this.showVisualizationsButton(isShowVis); - } - if(!parsedInfo[`${type}Tracks`].length) { - debug(`hiding popover button without contents: ${type}`); - this.controls[`${type}TracksButton`].set_visible(false); - - continue; - } - this.controls.addCheckButtons( - this.controls[`${type}TracksButton`].popoverBox, - parsedInfo[`${type}Tracks`], - activeId - ); - debug(`showing popover button with contents: ${type}`); - this.controls[`${type}TracksButton`].set_visible(true); - - anyButtonShown = true; - } - this.controls.revealTracksRevealer.set_visible(anyButtonShown); - } - - updateTitle(mediaInfo) - { - let title = mediaInfo.get_title(); - - if(!title) { - const item = this.player.playlistWidget.getActiveRow(); - title = item.filename; - } - - this.refreshWindowTitle(title); - this.revealerTop.title = title; - this.revealerTop.showTitle = true; - } - - refreshWindowTitle(title) - { - const isFloating = !this.controlsRevealer.reveal_child; - const pipSuffix = ' - PiP'; - const hasPipSuffix = title.endsWith(pipSuffix); - - this.root.title = (isFloating && !hasPipSuffix) - ? title + pipSuffix - : (!isFloating && hasPipSuffix) - ? title.substring(0, title.length - pipSuffix.length) - : title; - } - - updateTime() - { - if( - !this.revealerTop.visible - || !this.revealerTop.revealerGrid.visible - || !this.isFullscreenMode - || this.isMobileMonitor - ) - return null; - - const currTime = GLib.DateTime.new_now_local(); - const endTime = currTime.add_seconds( - (this.controls.positionAdjustment.get_upper() - this.controls.currentPosition) - / this.controls.elapsedButton.speedScale.get_value() - ); - const nextUpdate = this.revealerTop.setTimes(currTime, endTime, this.isSeekable); - - return nextUpdate; - } - - updateChapters(toc) - { - if(!toc) return; - - const entries = toc.get_entries(); - if(!entries) return; - - for(let entry of entries) { - const subentries = entry.get_sub_entries(); - if(!subentries) continue; - - for(let subentry of subentries) - this._parseTocSubentry(subentry); - } - } - - _parseTocSubentry(subentry) - { - const [success, start, stop] = subentry.get_start_stop_times(); - if(!success) { - debug('could not obtain toc subentry start/stop times'); - return; - } - - const pos = Math.floor(start / Gst.MSECOND) / 1000; - const tags = subentry.get_tags(); - - this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null); - this.controls.positionScale.add_mark(pos, Gtk.PositionType.BOTTOM, null); - - if(!tags) { - debug('could not obtain toc subentry tags'); - return; - } - - const [isString, title] = tags.get_string('title'); - if(!isString) { - debug('toc subentry tag does not have a title'); - return; - } - - if(!this.controls.chapters) - this.controls.chapters = {}; - - this.controls.chapters[pos] = title; - debug(`chapter at ${pos}: ${title}`); - } - - showVisualizationsButton(isShow) - { - if(isShow && !this.controls.visualizationsButton.isVisList) { - debug('creating visualizations list'); - const visArr = GstClapper.Clapper.visualizations_get(); - if(!visArr.length) - return; - - const parsedVisArr = [{ - label: 'Disabled', - type: 'visualization', - activeId: null - }]; - - visArr.forEach(vis => { - parsedVisArr.push({ - label: vis.name[0].toUpperCase() + vis.name.substring(1), - type: 'visualization', - activeId: vis.name, - }); - }); - - this.controls.addCheckButtons( - this.controls.visualizationsButton.popoverBox, - parsedVisArr, - null - ); - this.controls.visualizationsButton.isVisList = true; - debug(`total visualizations: ${visArr.length}`); - } - - if(this.controls.visualizationsButton.visible === isShow) - return; - - const action = (isShow) ? 'show' : 'hide'; - this.controls.visualizationsButton[action](); - debug(`show visualizations button: ${isShow}`); - } - - _onPlayerStateChanged(player, state) - { - switch(state) { - case GstClapper.ClapperState.BUFFERING: - debug('player state changed to: BUFFERING'); - if(player.needsTocUpdate) { - this.controls._setChapterVisible(false); - this.controls.positionScale.clear_marks(); - this.controls.chapters = null; - } - break; - case GstClapper.ClapperState.STOPPED: - debug('player state changed to: STOPPED'); - this.controls.setInitialState(); - this.revealerTop.showTitle = false; - break; - case GstClapper.ClapperState.PAUSED: - debug('player state changed to: PAUSED'); - this.controls.togglePlayButton.setPrimaryIcon(); - break; - case GstClapper.ClapperState.PLAYING: - debug('player state changed to: PLAYING'); - this.controls.togglePlayButton.setSecondaryIcon(); - break; - default: - break; - } - } - - _onPlayerDurationChanged(player, duration) - { - const durationSeconds = duration / Gst.SECOND; - const durationFloor = Math.floor(durationSeconds); - - debug(`duration changed: ${durationSeconds}`); - - this.controls.showHours = (durationFloor >= 3600); - this.controls.positionAdjustment.set_upper(durationFloor); - this.controls.durationFormatted = Misc.getFormattedTime(durationFloor); - this.controls.updateElapsedLabel(); - - if(settings.get_boolean('resume-enabled')) { - const resumeDatabase = JSON.parse(settings.get_string('resume-database')); - const title = player.playlistWidget.getActiveFilename(); - - debug(`searching database for resume info: ${title}`); - - const resumeInfo = resumeDatabase.find(info => { - return (info.title === title && info.duration === durationSeconds); - }); - - if(resumeInfo) { - debug('found resume info: ' + JSON.stringify(resumeInfo)); - new Dialogs.ResumeDialog(this.root, resumeInfo); - - const shrunkDatabase = resumeDatabase.filter(info => { - return !(info.title === title && info.duration === durationSeconds); - }); - settings.set_string('resume-database', JSON.stringify(shrunkDatabase)); - } - else - debug('resume info not found'); - } - } - - _onPlayerPositionUpdated(player, position) - { - if( - !this.isSeekable - || this.controls.isPositionDragging - || !player.seekDone - ) - return; - - const positionSeconds = Math.round(position / Gst.SECOND); - if(positionSeconds === this.controls.currentPosition) - return; - - this.controls.positionScale.set_value(positionSeconds); - } - - _onPlayerVideoDecoderChanged(player, decoder) - { - this.controls.videoTracksButton.setDecoder(decoder); - } - - _onPlayerAudioDecoderChanged(player, decoder) - { - this.controls.audioTracksButton.setDecoder(decoder); - } - - _onStateNotify(toplevel) - { - const isMaximized = Boolean( - toplevel.state & Gdk.ToplevelState.MAXIMIZED - ); - const isFullscreen = Boolean( - toplevel.state & Gdk.ToplevelState.FULLSCREEN - ); - const headerBar = this.revealerTop.headerBar; - - headerBar.setMaximized(isMaximized); - this.setFullscreenMode(isFullscreen); - } - - _onLayoutUpdate(surface, width, height) - { - if(width === this.layoutWidth) - return; - - /* Launch without showing revealers transitions on mobile width */ - if(!this.layoutWidth && width < this.controls.minFullViewWidth) { - for(let revealer of this.controls.revealersArr) - revealer.revealInstantly(false); - } - - this.layoutWidth = width; - - if(this.isFullscreenMode) - this.revealerBottom.setLayoutMargins(width); - - this.controls._onPlayerResize(width, height); - } - - _onWindowMap(window) - { - const surface = window.get_surface(); - - if(!surface.mapped) - this.surfaceMapSignal = surface.connect( - 'notify::mapped', this._onSurfaceMapNotify.bind(this) - ); - else - this._onSurfaceMapNotify(surface); - - surface.connect('notify::state', this._onStateNotify.bind(this)); - surface.connect('enter-monitor', this._onEnterMonitor.bind(this)); - surface.connect('layout', this._onLayoutUpdate.bind(this)); - - this.player._onWindowMap(window); - } - - _onSurfaceMapNotify(surface) - { - if(!surface.mapped) - return; - - if(this.surfaceMapSignal) { - surface.disconnect(this.surfaceMapSignal); - this.surfaceMapSignal = null; - } - - const monitor = surface.display.get_monitor_at_surface(surface); - const size = JSON.parse(settings.get_string('window-size')); - const hasMonitor = Boolean(monitor && monitor.geometry); - - /* Let GTK handle window restore if no monitor, otherwise - check if its size is greater then saved window size */ - if( - !hasMonitor - || (monitor.geometry.width >= size[0] - && monitor.geometry.height >= size[1]) - ) { - if(!hasMonitor) - debug('restoring window size without monitor geometry'); - - this.root.set_default_size(size[0], size[1]); - debug(`restored window size: ${size[0]}x${size[1]}`); - } - } - - _onEnterMonitor(surface, monitor) - { - debug('entered new monitor'); - - const { geometry } = monitor; - debug(`monitor application-pixels: ${geometry.width}x${geometry.height}`); - - const monitorWidth = Math.max(geometry.width, geometry.height); - this.isMobileMonitor = (monitorWidth < 1280); - debug(`mobile monitor detected: ${this.isMobileMonitor}`); - - const hasTVCss = this.root.has_css_class('tvmode'); - if(hasTVCss === this.isMobileMonitor) { - const action = (this.isMobileMonitor) ? 'remove' : 'add'; - this.root[action + '_css_class']('tvmode'); - } - - /* Mobile does not have TV mode, so we do not care about removing scaling */ - if(!this.isMobileMonitor) { - const pixWidth = monitorWidth * monitor.scale_factor; - const tvScaling = (pixWidth <= 1280) - ? 'lowres' - : (pixWidth > 1920) - ? 'hires' - : null; - - if(lastTvScaling !== tvScaling) { - if(lastTvScaling) - this.root.remove_css_class(lastTvScaling); - if(tvScaling) - this.root.add_css_class(tvScaling); - - lastTvScaling = tvScaling; - } - debug(`using scaling mode: ${tvScaling || 'normal'}`); - } - - /* Update top revealer display mode */ - this.revealerTop.setFullscreenMode(this.isFullscreenMode, this.isMobileMonitor); - } - - _clearTimeout(name) - { - if(!this[`_${name}Timeout`]) - return; - - GLib.source_remove(this[`_${name}Timeout`]); - this[`_${name}Timeout`] = null; - - if(name === 'updateTime') - debug('cleared update time interval'); - } - - _setHideControlsTimeout() - { - this._clearTimeout('hideControls'); - - let time = 2500; - - if(this.isFullscreenMode && !this.isMobileMonitor) - time += 1500; - - this._hideControlsTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, time, () => { - this._hideControlsTimeout = null; - - if(this.isCursorInPlayer) { - const blankCursor = Gdk.Cursor.new_from_name('none', null); - - this.player.widget.set_cursor(blankCursor); - this.revealerTop.set_cursor(blankCursor); - this.needsCursorRestore = true; - } - if(!this.isPopoverOpen) { - this._clearTimeout('updateTime'); - - this.revealerTop.revealChild(false); - this.revealerBottom.revealChild(false); - } - - return GLib.SOURCE_REMOVE; - }); - } - - _checkSetUpdateTimeInterval() - { - if( - this.isFullscreenMode - && !this.isMobileMonitor - && !this._updateTimeTimeout - ) { - debug('setting update time interval'); - this._setUpdateTimeInterval(); - } - } - - _setUpdateTimeInterval() - { - this._clearTimeout('updateTime'); - - const nextUpdate = this.updateTime(); - - if(nextUpdate === null) - return; - - this._updateTimeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, nextUpdate, () => { - this._updateTimeTimeout = null; - - if(this.isFullscreenMode) - this._setUpdateTimeInterval(); - - return GLib.SOURCE_REMOVE; - }); - } - - _handleDoublePress(gesture, x, y) - { - if(!this.isFullscreenMode || !Misc.getIsTouch(gesture)) - return this.toggleFullscreen(); - - const fieldSize = this.layoutWidth / 6; - - if(x < fieldSize) { - debug('left side double press'); - this.player.playlistWidget.prevTrack(); - } - else if(x > this.layoutWidth - fieldSize) { - debug('right side double press'); - this.player.playlistWidget.nextTrack(); - } - else { - this.toggleFullscreen(); - } - } - - _getClickGesture() - { - const clickGesture = new Gtk.GestureClick({ - button: 0, - propagation_phase: Gtk.PropagationPhase.CAPTURE, - }); - clickGesture.connect('pressed', this._onPressed.bind(this)); - clickGesture.connect('released', this._onReleased.bind(this)); - - return clickGesture; - } - - _getLongPressGesture() - { - const longPressGesture = new Gtk.GestureLongPress({ - touch_only: true, - delay_factor: 0.9, - propagation_phase: Gtk.PropagationPhase.CAPTURE, - }); - longPressGesture.connect('pressed', this._onLongPressed.bind(this)); - - return longPressGesture; - } - - _getDragGesture() - { - const dragGesture = new Gtk.GestureDrag({ - propagation_phase: Gtk.PropagationPhase.CAPTURE, - }); - dragGesture.connect('drag-update', this._onDragUpdate.bind(this)); - - return dragGesture; - } - - _getSwipeGesture() - { - const swipeGesture = new Gtk.GestureSwipe({ - touch_only: true, - propagation_phase: Gtk.PropagationPhase.CAPTURE, - }); - swipeGesture.connect('swipe', this._onSwipe.bind(this)); - swipeGesture.connect('update', this._onSwipeUpdate.bind(this)); - - return swipeGesture; - } - - _getScrollController() - { - const scrollController = new Gtk.EventControllerScroll(); - scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); - scrollController.connect('scroll', this._onScroll.bind(this)); - - return scrollController; - } - - _getMotionController() - { - const motionController = new Gtk.EventControllerMotion(); - motionController.connect('enter', this._onEnter.bind(this)); - motionController.connect('leave', this._onLeave.bind(this)); - motionController.connect('motion', this._onMotion.bind(this)); - - return motionController; - } - - _getDropTarget() - { - const dropTarget = new Gtk.DropTarget({ - actions: Gdk.DragAction.COPY | Gdk.DragAction.MOVE, - }); - dropTarget.set_gtypes([GObject.TYPE_STRING]); - dropTarget.connect('motion', this._onDataMotion.bind(this)); - dropTarget.connect('drop', this._onDataDrop.bind(this)); - - return dropTarget; - } - - _getIsSwipeOk(velocity, otherVelocity) - { - if(!velocity) - return false; - - const absVel = Math.abs(velocity); - - if(absVel < 20 || Math.abs(otherVelocity) * 1.5 >= absVel) - return false; - - return this.isFullscreenMode; - } - - _onPressed(gesture, nPress, x, y) - { - const button = gesture.get_current_button(); - const isDouble = (nPress % 2 == 0); - - this.isDragAllowed = !isDouble; - this.isSwipePerformed = false; - this.isLongPressed = false; - - switch(button) { - case Gdk.BUTTON_PRIMARY: - if(isDouble) - this._handleDoublePress(gesture, x, y); - break; - case Gdk.BUTTON_SECONDARY: - this.player.toggle_play(); - break; - default: - break; - } - } - - _onReleased(gesture, nPress, x, y) - { - /* Reveal if touch was not a swipe/long press or was already revealed */ - if( - ((!this.isSwipePerformed && !this.isLongPressed) - || this.revealerBottom.child_revealed) - && Misc.getIsTouch(gesture) - ) - this.revealControls(); - } - - _onLongPressed(gesture, x, y) - { - if(!this.isDragAllowed || !this.isFullscreenMode) - return; - - this.isLongPressed = true; - this.player.toggle_play(); - } - - _onKeyReleased(controller, keyval, keycode, state) - { - /* Ignore releases that did not trigger keypress - * e.g. while holding left "Super" key */ - if(!this.isReleaseKeyEnabled) - return; - - switch(keyval) { - case Gdk.KEY_Right: - case Gdk.KEY_Left: - const value = Math.round( - this.controls.positionScale.get_value() - ); - this.player.seek_seconds(value); - this._setHideControlsTimeout(); - this.isReleaseKeyEnabled = false; - break; - default: - break; - } - } - - _onDragUpdate(gesture, offsetX, offsetY) - { - if(!this.isDragAllowed || this.isFullscreenMode) - return; - - const { gtk_double_click_distance } = this.get_settings(); - - if ( - Math.abs(offsetX) > gtk_double_click_distance - || Math.abs(offsetY) > gtk_double_click_distance - ) { - const [isActive, startX, startY] = gesture.get_start_point(); - if(!isActive) return; - - const playerWidget = this.player.widget; - - const native = playerWidget.get_native(); - if(!native) return; - - let [isShared, winX, winY] = playerWidget.translate_coordinates( - native, startX, startY - ); - if(!isShared) return; - - const [nativeX, nativeY] = native.get_surface_transform(); - winX += nativeX; - winY += nativeY; - - native.get_surface().begin_move( - gesture.get_device(), - gesture.get_current_button(), - winX, - winY, - gesture.get_current_event_time() - ); - - gesture.reset(); - } - } - - _onSwipe(gesture, velocityX, velocityY) - { - if(!this._getIsSwipeOk(velocityX, velocityY)) - return; - - this._onScroll(gesture, -velocityX, 0); - this.isSwipePerformed = true; - } - - _onSwipeUpdate(gesture, sequence) - { - const [isCalc, velocityX, velocityY] = gesture.get_velocity(); - if(!isCalc) return; - - if(!this._getIsSwipeOk(velocityY, velocityX)) - return; - - const isIncrease = velocityY < 0; - - this.player.adjust_volume(isIncrease, 0.01); - this.isSwipePerformed = true; - } - - _onScroll(controller, dx, dy) - { - const isHorizontal = (Math.abs(dx) >= Math.abs(dy)); - const isIncrease = (isHorizontal) ? dx < 0 : dy < 0; - - if(isHorizontal) { - this.player.adjust_position(isIncrease); - const value = Math.round(this.controls.positionScale.get_value()); - this.player.seek_seconds(value); - } - else - this.player.adjust_volume(isIncrease); - - return true; - } - - _onEnter(controller, x, y) - { - this.isCursorInPlayer = true; - } - - _onLeave(controller) - { - if(this.isFullscreenMode) - return; - - this.isCursorInPlayer = false; - } - - _onMotion(controller, posX, posY) - { - this.isCursorInPlayer = true; - - /* GTK4 sometimes generates motions with same coords */ - if(this.posX === posX && this.posY === posY) - return; - - /* Do not show cursor on small movements */ - if( - Math.abs(this.posX - posX) >= 0.5 - || Math.abs(this.posY - posY) >= 0.5 - ) { - if(this.needsCursorRestore) { - const defaultCursor = Gdk.Cursor.new_from_name('default', null); - - this.player.widget.set_cursor(defaultCursor); - this.revealerTop.set_cursor(defaultCursor); - this.needsCursorRestore = false; - } - this.revealControls(); - } - - this.posX = posX; - this.posY = posY; - } - - _onDataMotion(dropTarget, x, y) - { - return Gdk.DragAction.MOVE; - } - - _onDataDrop(dropTarget, value, x, y) - { - const files = value.split(/\r?\n/).filter(uri => { - return Gst.uri_is_valid(uri); - }); - - if(!files.length) - return false; - - for(let index in files) - files[index] = Gio.File.new_for_uri(files[index]); - - /* TODO: remove GTK < 4.3.2 compat someday */ - const currentDrop = dropTarget.current_drop || dropTarget.drop; - const app = this.root.application; - app.isFileAppend = Boolean(currentDrop.actions & Gdk.DragAction.COPY); - app.open(files, ""); - - return true; - } -}); diff --git a/test/keyboard.js b/test/keyboard.js deleted file mode 100755 index b8356061..00000000 --- a/test/keyboard.js +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/gjs - -imports.gi.versions.Gdk = '4.0'; -imports.gi.versions.Gtk = '4.0'; - -const { Gdk, GObject, Gtk } = imports.gi; - -var KeyboardTest = GObject.registerClass( -class ClapperKeyboardTest extends Gtk.Application -{ - _init(opts) - { - super._init({ - application_id: 'com.github.rafostar.ClapperKeyboardTest' - }); - } - - vfunc_startup() - { - super.vfunc_startup(); - - let window = new Gtk.ApplicationWindow({ - application: this, - title: 'Clapper Keyboard Test', - }); - - let grid = new Gtk.Grid({ - margin_top: 10, - margin_bottom: 10, - margin_start: 20, - margin_end: 20, - row_spacing: 4, - column_spacing: 8, - focusable: true, - can_focus: true, - halign: Gtk.Align.CENTER, - valign: Gtk.Align.CENTER, - }); - - let label; - - label = new Gtk.Label({ label: 'KEY:' }); - grid.attach(label, 0, 0, 1, 1); - label = new Gtk.Label({ label: 'none' }); - grid.attach(label, 1, 0, 1, 1); - - label = new Gtk.Label({ label: 'VALUE:' }); - grid.attach(label, 0, 1, 1, 1); - label = new Gtk.Label({ label: '0' }); - grid.attach(label, 1, 1, 1, 1); - - label = new Gtk.Label({ label: 'CODE:' }); - grid.attach(label, 0, 2, 1, 1); - label = new Gtk.Label({ label: '0' }); - grid.attach(label, 1, 2, 1, 1); - - let keyController = new Gtk.EventControllerKey(); - keyController.connect('key-pressed', this._onKeyPressed.bind(this)); - grid.add_controller(keyController); - - window.set_child(grid); - } - - vfunc_activate() - { - this.active_window.present(); - this.active_window.get_child().grab_focus(); - } - - _onKeyPressed(controller, keyval, keycode, state) - { - let grid = controller.get_widget(); - - let keyName = grid.get_child_at(1, 0); - keyName.set_label(Gdk.keyval_name(keyval)); - - let keyVal = grid.get_child_at(1, 1); - keyVal.set_label(String(keyval)); - - let keyCode = grid.get_child_at(1, 2); - keyCode.set_label(String(keycode)); - } -}); - -new KeyboardTest().run([]); diff --git a/ui/clapper.ui b/ui/clapper.ui deleted file mode 100644 index 4d2f0f4e..00000000 --- a/ui/clapper.ui +++ /dev/null @@ -1,31 +0,0 @@ - - - -
- - Open Files… - app.open_local - - - Open URI… - app.open_uri - -
-
- - Preferences - app.prefs - - - Shortcuts - app.shortcuts - -
-
- - About Clapper - app.about - -
-
-
diff --git a/ui/elapsed-time-button.ui b/ui/elapsed-time-button.ui deleted file mode 100644 index 9932b010..00000000 --- a/ui/elapsed-time-button.ui +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - vertical - True - True - - - False - True - - - - - Speed - - - - - horizontal - bottom - False - 2 - True - center - speed_adjustment - - 0.25× - Normal - - - - - - - - - - 0.01 - 2 - 1 - 0.1 - - diff --git a/ui/help-overlay.ui b/ui/help-overlay.ui deleted file mode 100644 index 4203ead7..00000000 --- a/ui/help-overlay.ui +++ /dev/null @@ -1,160 +0,0 @@ - - - - True - - - app - - - General - - - Show shortcuts - F1 <Ctrl>question - - - - - Open preferences - <Ctrl>comma - - - - - Toggle fullscreen - Double tap | Double click - F11 f - - - - - Leave fullscreen - Escape - - - - - Reveal OSD (fullscreen only) - Tap - Return - - - - - Quit - <Ctrl>Q Q - - - - - - - Media - - - Open files - <Ctrl>O - - - - - Open URI - <Ctrl>U - - - - - - - Playlist - - - Next item - Double tap (right side) - <Ctrl>Right - - - - - Previous item - Double tap (left side) - <Ctrl>Left - - - - - Change repeat mode - <Ctrl>R - - - - - Export to file - <Ctrl>E - - - - - - - Playback - - - Toggle play - Long press | Right click - space - - - - - Seek forward - Swipe right | Scroll right - Right - - - - - Seek backward - Swipe left | Scroll left - Left - - - - - Volume up - Swipe up | Scroll up - Up - - - - - Volume down - Swipe down | Scroll down - Down - - - - - Toggle mute - <Ctrl>M M - - - - - Next chapter - <Shift>Right - - - - - Previous chapter - <Shift>Left - - - - - - - - diff --git a/ui/popover-separator.ui b/ui/popover-separator.ui deleted file mode 100644 index 64eea34f..00000000 --- a/ui/popover-separator.ui +++ /dev/null @@ -1,29 +0,0 @@ - - - - diff --git a/ui/preferences-plugin-ranking-subpage.ui b/ui/preferences-plugin-ranking-subpage.ui deleted file mode 100644 index 6d98919e..00000000 --- a/ui/preferences-plugin-ranking-subpage.ui +++ /dev/null @@ -1,30 +0,0 @@ - - - - diff --git a/ui/preferences-window.ui b/ui/preferences-window.ui deleted file mode 100644 index 4ea019c6..00000000 --- a/ui/preferences-window.ui +++ /dev/null @@ -1,270 +0,0 @@ - - - - - 0 - 99 - 1 - 1 - - - 0 - 150 - 1 - 1 - - - -1000 - 1000 - 25 - 1 - - - 1024 - 65535 - 1 - 1 - - - 1024 - 65535 - 1 - 1 - - diff --git a/ui/track-select-button.ui b/ui/track-select-button.ui deleted file mode 100644 index 2079e152..00000000 --- a/ui/track-select-button.ui +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - vertical - True - True - - - - - - - True - True - - - vertical - - - - - - - - diff --git a/ui/volume-button.ui b/ui/volume-button.ui deleted file mode 100644 index 50ad277c..00000000 --- a/ui/volume-button.ui +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - vertical - True - True - - - vertical - True - top - False - True - volume_adjustment - - - 0% - 100% - 150% - - - - - - - - - 0 - 1.5 - 0.05 - 0.05 - -