pax_global_header00006660000000000000000000000064142552700560014520gustar00rootroot0000000000000052 comment=23bb253ca2400fb392222dba826f199a1d036bd9 clapper-0.5.2/000077500000000000000000000000001425527005600131525ustar00rootroot00000000000000clapper-0.5.2/.gitattributes000066400000000000000000000003001425527005600160360ustar00rootroot00000000000000extras/**/* linguist-vendored lib/**/* linguist-vendored lib/**/**/* linguist-vendored lib/gst/clapper/gstclapper-mpris* linguist-vendored=false lib/gst/clapper/gtk4/* linguist-vendored=false clapper-0.5.2/.github/000077500000000000000000000000001425527005600145125ustar00rootroot00000000000000clapper-0.5.2/.github/workflows/000077500000000000000000000000001425527005600165475ustar00rootroot00000000000000clapper-0.5.2/.github/workflows/flatpak-nightly.yml000066400000000000000000000025561425527005600224000ustar00rootroot00000000000000on: workflow_dispatch: schedule: - cron: "0 0 * * *" name: "Flatpak Nightly" jobs: flatpak: name: "Flatpak" runs-on: ubuntu-latest timeout-minutes: 600 container: image: bilelmoussaoui/flatpak-github-actions:gnome-nightly options: --privileged strategy: matrix: arch: [x86_64, aarch64] fail-fast: false steps: - uses: actions/checkout@v2 with: submodules: recursive - name: Install Docker run: | dnf -y install docker - name: Setup QEMU id: qemu uses: docker/setup-qemu-action@v1 with: platforms: arm64 - name: Prepare Runtime run: | flatpak --system install -y --noninteractive flathub org.freedesktop.Sdk.Extension.rust-nightly/${{ matrix.arch }}/21.08 flatpak --system install -y --noninteractive flathub org.freedesktop.Sdk.Extension.llvm13/${{ matrix.arch }}/21.08 - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4 name: Build with: bundle: com.github.rafostar.Clapper.flatpak manifest-path: pkgs/flatpak/com.github.rafostar.Clapper-nightly.json repository-name: gnome-nightly repository-url: https://nightly.gnome.org/gnome-nightly.flatpakrepo cache-key: flatpak-builder-${{ github.sha }}-testing-${{ github.run_number }} arch: ${{ matrix.arch }} clapper-0.5.2/.github/workflows/flatpak.yml000066400000000000000000000017201425527005600207140ustar00rootroot00000000000000on: workflow_dispatch: push: branches: - master pull_request: branches: - master name: "Flatpak" jobs: flatpak: name: "Flatpak" runs-on: ubuntu-latest timeout-minutes: 600 container: image: bilelmoussaoui/flatpak-github-actions:gnome-42 options: --privileged strategy: matrix: arch: [x86_64, aarch64] fail-fast: false steps: - uses: actions/checkout@v2 with: submodules: recursive - name: Install Docker run: | dnf -y install docker - name: Setup QEMU id: qemu uses: docker/setup-qemu-action@v1 with: platforms: arm64 - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4 name: "Build" with: bundle: com.github.rafostar.Clapper.flatpak manifest-path: pkgs/flatpak/com.github.rafostar.Clapper.json cache-key: flatpak-builder-${{ github.sha }} arch: ${{ matrix.arch }} clapper-0.5.2/.gitignore000066400000000000000000000000501425527005600151350ustar00rootroot00000000000000# meson/ninja build/ install/ builddir/ clapper-0.5.2/.gitmodules000066400000000000000000000002021425527005600153210ustar00rootroot00000000000000[submodule "pkgs/flatpak/flathub"] path = pkgs/flatpak/flathub url = https://github.com/flathub/com.github.rafostar.Clapper.git clapper-0.5.2/.obs/000077500000000000000000000000001425527005600140135ustar00rootroot00000000000000clapper-0.5.2/.obs/workflows.yml000066400000000000000000000002571425527005600165770ustar00rootroot00000000000000rebuild_master: steps: - trigger_services: project: home:Rafostar package: clapper filters: event: push branches: only: - master clapper-0.5.2/COPYING000066400000000000000000001045151425527005600142130ustar00rootroot00000000000000 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 . clapper-0.5.2/FUNDING.yml000066400000000000000000000000621425527005600147650ustar00rootroot00000000000000liberapay: Clapper custom: ['paypal.me/Rafostar'] clapper-0.5.2/README.md000066400000000000000000000076051425527005600144410ustar00rootroot00000000000000# Clapper [![Flatpak](https://github.com/Rafostar/clapper/actions/workflows/flatpak.yml/badge.svg?event=push)](https://github.com/Rafostar/clapper/actions/workflows/flatpak.yml) [![Flatpak Nightly](https://github.com/Rafostar/clapper/actions/workflows/flatpak-nightly.yml/badge.svg?event=schedule)](https://github.com/Rafostar/clapper/actions/workflows/flatpak-nightly.yml) [![Crowdin](https://badges.crowdin.net/clapper/localized.svg)](https://crowdin.com/project/clapper) [![Matrix](https://img.shields.io/matrix/clapper-player:matrix.org?label=matrix)](https://matrix.to/#/#clapper-player:matrix.org) [![Donate](https://img.shields.io/liberapay/receives/Clapper.svg?logo=liberapay)](https://liberapay.com/Clapper) A GNOME media player built using [GJS](https://gitlab.gnome.org/GNOME/gjs) with [GTK4](https://www.gtk.org) toolkit. The media player uses [GStreamer](https://gstreamer.freedesktop.org/) as a media backend and renders everything via [OpenGL](https://www.opengl.org).


Windowed Mode


Fullscreen Mode


Floating Mode

### Features: * [Hardware acceleration](https://github.com/Rafostar/clapper/wiki/Hardware-acceleration) * [Floating mode](https://github.com/Rafostar/clapper/wiki/Floating-mode) * [Adaptive UI](https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-mobile.png) * [Playlist from file](https://github.com/Rafostar/clapper/wiki/Playlists) * Chapters on progress bar * MPRIS support ## Installation from Flatpak The `Flatpak` package includes all required dependencies and codecs. Additionally it also has a few patches, thus some functionalities work better (or are only available) in `Flatpak` version (until my changes are accepted upstream). List of patches used in this version can be found [here](https://github.com/Rafostar/clapper/issues/35). Download on Flathub ## Packages in Linux Distributions [![Packaging status](https://repology.org/badge/vertical-allrepos/clapper.svg)](https://repology.org/project/clapper/versions) Pre-built RPM packages are also available in [my repo](https://software.opensuse.org//download.html?project=home%3ARafostar&package=clapper) ([see status](https://build.opensuse.org/package/show/home:Rafostar/clapper)).
Those are automatically built on each git commit, thus are considered unstable. ## Installation from Source Code ```sh meson builddir --prefix=/usr/local sudo meson install -C builddir ``` ## Questions? Feel free to ask me any questions. Come and talk on Matrix: [#clapper-player:matrix.org](https://matrix.to/#/#clapper-player:matrix.org) ## Translations Preferred translation method is to use [Clapper Crowdin](https://crowdin.com/project/clapper) web page. Crowdin does not require any additional tools and translating can be done through web browser. You can login using GitHub account or create a new one. Only I can add new languages to this project, so if your language is not available, please contact me first. ## Special Thanks Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files. Big thanks to [bridadan](https://github.com/bridadan) and [Uniformbuffer3](https://github.com/Uniformbuffer3) for helping with testing V4L2 and NVDEC hardware acceleration methods. Thanks a lot to all the people who are supporting the development with their anonymous donations through [Liberapay](https://liberapay.com/Clapper/). I :heart: U. clapper-0.5.2/TODO.md000066400000000000000000000034421425527005600142440ustar00rootroot00000000000000- [X] Implement GstPlayer API - [X] Update to custom GstClapper API - [X] Inhibit screen locking - [X] Hide cursor on video window - Adaptive GUI: - [X] Darker and bigger in fullscreen - [X] Mobile/narrow widths transitions - [ ] Mobile friendly other windows e.g. prefs window (libadwaita) - [X] Dragging player by video (MPV) - [X] Switching video/audio/subtitles tracks from bottom bar (MPV) - [X] Over-amplification supported by default (VLC) - [X] Audio visualizations (VLC) - [X] Clock with current hour and "Ends at" time on top overlay (Kodi) - [ ] Auto select subtitles matching OS language (Totem) - [X] Picture-in-Picture mode window (floating window) - [ ] Touch gestures/swipes support - Media playlists: - [X] Add more items to playlist via D&D - [X] Select video from playlist - [ ] Reorder playlist items via D&D - [X] Load special playlist file (.claps) - [X] Save to playlist file from GUI - Seeking: - [X] Customizable seek time - [X] Set seek mode (default, accurate, fast) - [ ] Statistics and codec info page (VLC) - [X] Resume playback from last position - [X] Chapters support - [ ] Set tracks time offset - [ ] Subtitles offset - [X] Audio offset - [ ] MDNS and UPNP (discovering media in local network) - [X] DND files from Nautilus to play (ignore incompatible ones) - [X] Support dropping whole folders - [ ] Search for subtitles, download and activate (SMplayer) - [ ] Auto add subtitles from same folder - [ ] Set global subtitles folders - [X] RSTP streaming - [X] Playback speed - [X] Remote playback controls via HTTP (VLC) + WebSockets - [ ] Expand available API - [ ] API documentation - [X] Integration with the top bar - [X] MPRIS support - [X] Controls in the notifications panel - [ ] Progress bar in the notifications panel (maybe via extension) clapper-0.5.2/_config.yml000066400000000000000000000000321425527005600152740ustar00rootroot00000000000000theme: jekyll-theme-caymanclapper-0.5.2/_service000066400000000000000000000010001425527005600146630ustar00rootroot00000000000000 git https://github.com/Rafostar/clapper.git pkgs/rpm/clapper.spec pkgs/rpm/clapper.rpmlintrc xz *.tar clapper-0.5.2/bin/000077500000000000000000000000001425527005600137225ustar00rootroot00000000000000clapper-0.5.2/bin/com.github.rafostar.Clapper.in000066400000000000000000000002611425527005600215150ustar00rootroot00000000000000#!@GJS@ imports.package.init({ name: '@PACKAGE_NAME@', version: '@PACKAGE_VERSION@', prefix: '@prefix@', libdir: '@libdir@', }); imports.package.run(imports.src.main); clapper-0.5.2/bin/meson.build000066400000000000000000000012041425527005600160610ustar00rootroot00000000000000bin_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) clapper-0.5.2/build-aux/000077500000000000000000000000001425527005600150445ustar00rootroot00000000000000clapper-0.5.2/build-aux/meson/000077500000000000000000000000001425527005600161655ustar00rootroot00000000000000clapper-0.5.2/build-aux/meson/postinstall.py000077500000000000000000000013671425527005600211250ustar00rootroot00000000000000#!/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')]) clapper-0.5.2/crowdin.yml000066400000000000000000000001341425527005600153400ustar00rootroot00000000000000files: - source: /po/com.github.rafostar.Clapper.pot translation: /po/%osx_locale%.po clapper-0.5.2/css/000077500000000000000000000000001425527005600137425ustar00rootroot00000000000000clapper-0.5.2/css/styles.css000066400000000000000000000201661425527005600160040ustar00rootroot00000000000000/* 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 */ } clapper-0.5.2/data/000077500000000000000000000000001425527005600140635ustar00rootroot00000000000000clapper-0.5.2/data/com.github.rafostar.Clapper-symbolic.svg000066400000000000000000000114461425527005600236750ustar00rootroot00000000000000 clapper-0.5.2/data/com.github.rafostar.Clapper.data.gresource.xml000066400000000000000000000011551425527005600247600ustar00rootroot00000000000000 icons/play-symbolic.svg icons/pause-symbolic.svg icons/pip-in-symbolic.svg icons/pip-out-symbolic.svg clapper-0.5.2/data/com.github.rafostar.Clapper.desktop000066400000000000000000000050051425527005600227220ustar00rootroot00000000000000[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; clapper-0.5.2/data/com.github.rafostar.Clapper.gschema.xml000066400000000000000000000071231425527005600234620ustar00rootroot00000000000000 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 clapper-0.5.2/data/com.github.rafostar.Clapper.metainfo.xml000066400000000000000000000227631425527005600236640ustar00rootroot00000000000000 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
clapper-0.5.2/data/com.github.rafostar.Clapper.service.in000066400000000000000000000001141425527005600233120ustar00rootroot00000000000000[D-BUS Service] Name=@app_id@ Exec=@bindir@/@app_id@ --gapplication-service clapper-0.5.2/data/com.github.rafostar.Clapper.svg000066400000000000000000000075311425527005600220560ustar00rootroot00000000000000 clapper-0.5.2/data/com.github.rafostar.Clapper.xml000066400000000000000000000004511425527005600220510ustar00rootroot00000000000000 Clapper Playlist clapper-0.5.2/data/gstclapper-mpris-gdbus.xml000066400000000000000000000042421425527005600212050ustar00rootroot00000000000000 clapper-0.5.2/data/icons/000077500000000000000000000000001425527005600151765ustar00rootroot00000000000000clapper-0.5.2/data/icons/pause-symbolic.svg000066400000000000000000000004061425527005600206530ustar00rootroot00000000000000 clapper-0.5.2/data/icons/pip-in-symbolic.svg000066400000000000000000000112171425527005600207340ustar00rootroot00000000000000 clapper-0.5.2/data/icons/pip-out-symbolic.svg000066400000000000000000000112511425527005600211330ustar00rootroot00000000000000 clapper-0.5.2/data/icons/play-symbolic.svg000066400000000000000000000007401425527005600205040ustar00rootroot00000000000000 clapper-0.5.2/data/meson.build000066400000000000000000000027251425527005600162330ustar00rootroot00000000000000iconsdir = 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', 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'), ) clapper-0.5.2/extras/000077500000000000000000000000001425527005600144605ustar00rootroot00000000000000clapper-0.5.2/extras/debug/000077500000000000000000000000001425527005600155465ustar00rootroot00000000000000clapper-0.5.2/extras/debug/Debug.js000066400000000000000000000104671425527005600171420ustar00rootroot00000000000000const { 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(); } } clapper-0.5.2/extras/ink/000077500000000000000000000000001425527005600152415ustar00rootroot00000000000000clapper-0.5.2/extras/ink/Ink.js000066400000000000000000000170331425527005600163240ustar00rootroot00000000000000const 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; } } clapper-0.5.2/lib/000077500000000000000000000000001425527005600137205ustar00rootroot00000000000000clapper-0.5.2/lib/gst/000077500000000000000000000000001425527005600145155ustar00rootroot00000000000000clapper-0.5.2/lib/gst/COPYING000066400000000000000000000636421425527005600155630ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! clapper-0.5.2/lib/gst/clapper/000077500000000000000000000000001425527005600161435ustar00rootroot00000000000000clapper-0.5.2/lib/gst/clapper/clapper-prelude.h000066400000000000000000000026741425527005600214110ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/clapper.h000066400000000000000000000025321425527005600177440ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.c000066400000000000000000000153021425527005600267030ustar00rootroot00000000000000/* 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); } clapper-0.5.2/lib/gst/clapper/gstclapper-g-main-context-signal-dispatcher.h000066400000000000000000000053651425527005600267200ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-gtk4-plugin.c000066400000000000000000000067041425527005600226250ustar00rootroot00000000000000/* 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); } clapper-0.5.2/lib/gst/clapper/gstclapper-gtk4-plugin.h000066400000000000000000000047231425527005600226310ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-media-info-private.h000066400000000000000000000053651425527005600241470ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-media-info.c000066400000000000000000000522171425527005600224700ustar00rootroot00000000000000/* 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); } clapper-0.5.2/lib/gst/clapper/gstclapper-media-info.h000066400000000000000000000230741425527005600224740ustar00rootroot00000000000000/* 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 */ clapper-0.5.2/lib/gst/clapper/gstclapper-mpris-private.h000066400000000000000000000031211425527005600232550ustar00rootroot00000000000000/* * 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-mpris.c000066400000000000000000000570061425527005600216130ustar00rootroot00000000000000/* * 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); } clapper-0.5.2/lib/gst/clapper/gstclapper-mpris.h000066400000000000000000000044031425527005600216110ustar00rootroot00000000000000/* * 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-signal-dispatcher-private.h000066400000000000000000000025141425527005600255310ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-signal-dispatcher.c000066400000000000000000000034501425527005600240540ustar00rootroot00000000000000/* 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); } clapper-0.5.2/lib/gst/clapper/gstclapper-signal-dispatcher.h000066400000000000000000000043571425527005600240700ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-types.h000066400000000000000000000022371425527005600216260ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-video-overlay-video-renderer.c000066400000000000000000000262751425527005600261620ustar00rootroot00000000000000/* 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; } clapper-0.5.2/lib/gst/clapper/gstclapper-video-overlay-video-renderer.h000066400000000000000000000071271425527005600261620ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-video-renderer-private.h000066400000000000000000000023621425527005600250430ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-video-renderer.c000066400000000000000000000031771425527005600233730ustar00rootroot00000000000000/* 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); } clapper-0.5.2/lib/gst/clapper/gstclapper-video-renderer.h000066400000000000000000000040141425527005600233670ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper-visualization.c000066400000000000000000000114071425527005600233550ustar00rootroot00000000000000/* 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; } clapper-0.5.2/lib/gst/clapper/gstclapper-visualization.h000066400000000000000000000037301425527005600233620ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gstclapper.c000066400000000000000000004460101425527005600204600ustar00rootroot00000000000000/* 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; GParamSpec *prop; factory = gst_element_get_factory (element); if (factory) { 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); } prop = g_object_class_find_property (G_OBJECT_GET_CLASS (element), "user-agent"); if (prop && prop->value_type == G_TYPE_STRING) { const gchar *user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"; GST_INFO_OBJECT (self, "Setting element user-agent: %s", user_agent); g_object_set (element, "user-agent", user_agent, NULL); } } 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; } clapper-0.5.2/lib/gst/clapper/gstclapper.h000066400000000000000000000256741425527005600204760ustar00rootroot00000000000000/* 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__ */ clapper-0.5.2/lib/gst/clapper/gtk4/000077500000000000000000000000001425527005600170145ustar00rootroot00000000000000clapper-0.5.2/lib/gst/clapper/gtk4/gstclapperglsink.c000066400000000000000000000602161425527005600225410ustar00rootroot00000000000000/* * 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; } clapper-0.5.2/lib/gst/clapper/gtk4/gstclapperglsink.h000066400000000000000000000057571425527005600225570ustar00rootroot00000000000000/* * 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__ */ clapper-0.5.2/lib/gst/clapper/gtk4/gstclapperglutils.c000066400000000000000000000056711425527005600227410ustar00rootroot00000000000000/* * 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); } } clapper-0.5.2/lib/gst/clapper/gtk4/gstclapperglutils.h000066400000000000000000000022341425527005600227360ustar00rootroot00000000000000/* * 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__ */ clapper-0.5.2/lib/gst/clapper/gtk4/gstgtkutils.c000066400000000000000000000055371425527005600215560ustar00rootroot00000000000000/* * 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)); } clapper-0.5.2/lib/gst/clapper/gtk4/gstgtkutils.h000066400000000000000000000026321425527005600215540ustar00rootroot00000000000000/* * 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__ */ clapper-0.5.2/lib/gst/clapper/gtk4/gtkclapperglwidget.c000066400000000000000000001102571425527005600230510ustar00rootroot00000000000000/* * 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; } clapper-0.5.2/lib/gst/clapper/gtk4/gtkclapperglwidget.h000066400000000000000000000076731425527005600230650ustar00rootroot00000000000000/* * 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__ */ clapper-0.5.2/lib/gst/clapper/meson.build000066400000000000000000000063161425527005600203130ustar00rootroot00000000000000gstclapper_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] ) clapper-0.5.2/lib/gst/meson.build000066400000000000000000000001011425527005600166470ustar00rootroot00000000000000if get_option('lib') subdir('clapper') endif subdir('plugin') clapper-0.5.2/lib/gst/plugin/000077500000000000000000000000001425527005600160135ustar00rootroot00000000000000clapper-0.5.2/lib/gst/plugin/gstclapperimporter.c000066400000000000000000000340311425527005600221060ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclapperimporter.h" #include "gstgtkutils.h" #define GST_CAT_DEFAULT gst_clapper_importer_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); #define parent_class gst_clapper_importer_parent_class G_DEFINE_TYPE (GstClapperImporter, gst_clapper_importer, GST_TYPE_OBJECT); typedef struct { GdkTexture *texture; GstVideoOverlayRectangle *rectangle; gint x, y; guint width, height; gint index; gatomicrefcount ref_count; } GstClapperGdkOverlay; static GstClapperGdkOverlay * gst_clapper_gdk_overlay_new (GdkTexture *texture, GstVideoOverlayRectangle *rectangle, gint x, gint y, guint width, guint height, guint index) { GstClapperGdkOverlay *overlay = g_slice_new (GstClapperGdkOverlay); overlay->texture = g_object_ref (texture); overlay->rectangle = gst_video_overlay_rectangle_ref (rectangle); overlay->x = x; overlay->y = y; overlay->width = width; overlay->height = height; overlay->index = index; g_atomic_ref_count_init (&overlay->ref_count); return overlay; } static GstClapperGdkOverlay * gst_clapper_gdk_overlay_ref (GstClapperGdkOverlay *overlay) { g_atomic_ref_count_inc (&overlay->ref_count); return overlay; } static void gst_clapper_gdk_overlay_unref (GstClapperGdkOverlay *overlay) { if (g_atomic_ref_count_dec (&overlay->ref_count)) { GST_TRACE ("Freeing overlay: %" GST_PTR_FORMAT, overlay); g_object_unref (overlay->texture); gst_video_overlay_rectangle_unref (overlay->rectangle); g_slice_free (GstClapperGdkOverlay, overlay); } } static GstBufferPool * _default_create_pool (GstClapperImporter *self, GstStructure **config) { GST_FIXME_OBJECT (self, "Need to create buffer pool"); return NULL; } static GdkTexture * _default_generate_texture (GstClapperImporter *self, GstBuffer *buffer, GstVideoInfo *v_info) { GST_FIXME_OBJECT (self, "GdkTexture generation not implemented"); return NULL; } static void gst_clapper_importer_init (GstClapperImporter *self) { gst_video_info_init (&self->pending_v_info); gst_video_info_init (&self->v_info); self->pending_overlays = g_ptr_array_new_with_free_func ( (GDestroyNotify) gst_clapper_gdk_overlay_unref); self->overlays = g_ptr_array_new_with_free_func ( (GDestroyNotify) gst_clapper_gdk_overlay_unref); gdk_rgba_parse (&self->bg, "black"); } static void gst_clapper_importer_finalize (GObject *object) { GstClapperImporter *self = GST_CLAPPER_IMPORTER_CAST (object); GST_TRACE ("Finalize"); gst_clear_caps (&self->pending_caps); gst_clear_buffer (&self->pending_buffer); gst_clear_buffer (&self->buffer); g_ptr_array_unref (self->pending_overlays); g_ptr_array_unref (self->overlays); g_clear_object (&self->texture); GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); } static void gst_clapper_importer_class_init (GstClapperImporterClass *klass) { GObjectClass *gobject_class = (GObjectClass *) klass; GstClapperImporterClass *importer_class = (GstClapperImporterClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperimporter", 0, "Clapper Importer"); gobject_class->finalize = gst_clapper_importer_finalize; importer_class->create_pool = _default_create_pool; importer_class->generate_texture = _default_generate_texture; } static GstClapperGdkOverlay * _get_cached_overlay (GPtrArray *overlays, GstVideoOverlayRectangle *rectangle) { guint i; for (i = 0; i < overlays->len; i++) { GstClapperGdkOverlay *overlay = g_ptr_array_index (overlays, i); if (overlay->rectangle == rectangle) return overlay; } return NULL; } static gint _sort_overlays_cb (gconstpointer a, gconstpointer b) { GstClapperGdkOverlay *overlay_a, *overlay_b; overlay_a = *((GstClapperGdkOverlay **) a); overlay_b = *((GstClapperGdkOverlay **) b); return (overlay_a->index - overlay_b->index); } /* * Prepares overlays to show with the next rendered buffer. * * In order for overlays caching to work correctly, this should be called for * every received buffer (even if its going to be disgarded), also must be * called together with pending buffer replacement within a single importer * locking, to make sure prepared overlays always match the pending buffer. */ static void gst_clapper_importer_prepare_overlays_locked (GstClapperImporter *self) { GstVideoOverlayCompositionMeta *comp_meta; guint num_overlays, i; if (G_UNLIKELY (!self->pending_buffer) || !(comp_meta = gst_buffer_get_video_overlay_composition_meta (self->pending_buffer))) { guint n_pending = self->pending_overlays->len; /* Remove all cached overlays if new buffer does not have any */ if (n_pending > 0) { GST_TRACE ("No overlays in buffer, removing all cached ones"); g_ptr_array_remove_range (self->pending_overlays, 0, n_pending); } return; } GST_LOG_OBJECT (self, "Preparing overlays..."); /* Mark all old overlays as unused by giving them negative index */ for (i = 0; i < self->pending_overlays->len; i++) { GstClapperGdkOverlay *overlay = g_ptr_array_index (self->pending_overlays, i); overlay->index = -1; } num_overlays = gst_video_overlay_composition_n_rectangles (comp_meta->overlay); for (i = 0; i < num_overlays; i++) { GdkTexture *texture; GstBuffer *comp_buffer; GstVideoFrame comp_frame; GstVideoMeta *v_meta; GstVideoInfo v_info; GstVideoOverlayRectangle *rectangle; GstClapperGdkOverlay *overlay; GstVideoOverlayFormatFlags flags, alpha_flags = 0; gint comp_x, comp_y; guint comp_width, comp_height; rectangle = gst_video_overlay_composition_get_rectangle (comp_meta->overlay, i); if ((overlay = _get_cached_overlay (self->pending_overlays, rectangle))) { overlay->index = i; GST_TRACE ("Reusing cached overlay: %" GST_PTR_FORMAT, overlay); continue; } if (G_UNLIKELY (!gst_video_overlay_rectangle_get_render_rectangle (rectangle, &comp_x, &comp_y, &comp_width, &comp_height))) { GST_WARNING ("Invalid overlay rectangle dimensions: %" GST_PTR_FORMAT, rectangle); continue; } flags = gst_video_overlay_rectangle_get_flags (rectangle); if (flags & GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA) alpha_flags |= GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA; comp_buffer = gst_video_overlay_rectangle_get_pixels_unscaled_argb (rectangle, alpha_flags); /* Update overlay video info from video meta */ if ((v_meta = gst_buffer_get_video_meta (comp_buffer))) { gst_video_info_set_format (&v_info, v_meta->format, v_meta->width, v_meta->height); v_info.stride[0] = v_meta->stride[0]; if (alpha_flags & GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA) v_info.flags |= GST_VIDEO_FLAG_PREMULTIPLIED_ALPHA; } if (G_UNLIKELY (!gst_video_frame_map (&comp_frame, &v_info, comp_buffer, GST_MAP_READ))) return; if ((texture = gst_video_frame_into_gdk_texture (&comp_frame))) { overlay = gst_clapper_gdk_overlay_new (texture, rectangle, comp_x, comp_y, comp_width, comp_height, i); g_object_unref (texture); GST_TRACE_OBJECT (self, "Created overlay: %" GST_PTR_FORMAT ", x: %i, y: %i, width: %u, height: %u", overlay, overlay->x, overlay->y, overlay->width, overlay->height); g_ptr_array_insert (self->pending_overlays, i, overlay); } gst_video_frame_unmap (&comp_frame); } /* Remove all overlays that are not going to be used */ for (i = self->pending_overlays->len; i > 0; i--) { GstClapperGdkOverlay *overlay = g_ptr_array_index (self->pending_overlays, i - 1); if (overlay->index < 0) { GST_TRACE ("Removing unused cached overlay: %" GST_PTR_FORMAT, overlay); g_ptr_array_remove (self->pending_overlays, overlay); } } /* Sort remaining overlays */ if (self->pending_overlays->len > 1) { GST_LOG_OBJECT (self, "Sorting overlays"); g_ptr_array_sort (self->pending_overlays, (GCompareFunc) _sort_overlays_cb); } if (G_UNLIKELY (num_overlays != self->pending_overlays->len)) { GST_WARNING_OBJECT (self, "Some overlays could not be prepared, %u != %u", num_overlays, self->pending_overlays->len); } GST_LOG_OBJECT (self, "Prepared overlays: %u", self->pending_overlays->len); } gboolean gst_clapper_importer_prepare (GstClapperImporter *self) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); if (importer_class->prepare) { if (!importer_class->prepare (self)) return FALSE; } GST_DEBUG_OBJECT (self, "Importer prepared"); return TRUE; } void gst_clapper_importer_share_data (GstClapperImporter *self, GstClapperImporter *dest) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); if (importer_class->share_data) importer_class->share_data (self, dest); } void gst_clapper_importer_set_caps (GstClapperImporter *self, GstCaps *caps) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); GST_OBJECT_LOCK (self); gst_caps_replace (&self->pending_caps, caps); GST_OBJECT_UNLOCK (self); if (importer_class->set_caps) importer_class->set_caps (self, caps); } void gst_clapper_importer_set_buffer (GstClapperImporter *self, GstBuffer *buffer) { GST_OBJECT_LOCK (self); /* Pending v_info, buffer and overlays must be * set within a single importer locking */ if (self->pending_caps) { self->has_pending_v_info = gst_video_info_from_caps (&self->pending_v_info, self->pending_caps); gst_clear_caps (&self->pending_caps); } gst_buffer_replace (&self->pending_buffer, buffer); gst_clapper_importer_prepare_overlays_locked (self); GST_OBJECT_UNLOCK (self); } GstBufferPool * gst_clapper_importer_create_pool (GstClapperImporter *self, GstStructure **config) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); return importer_class->create_pool (self, config); } void gst_clapper_importer_add_allocation_metas (GstClapperImporter *self, GstQuery *query) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); if (importer_class->add_allocation_metas) importer_class->add_allocation_metas (self, query); } gboolean gst_clapper_importer_handle_context_query (GstClapperImporter *self, GstBaseSink *bsink, GstQuery *query) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); if (!importer_class->handle_context_query) return FALSE; return importer_class->handle_context_query (self, bsink, query); } void gst_clapper_importer_snapshot (GstClapperImporter *self, GdkSnapshot *snapshot, gdouble width, gdouble height) { guint i; gboolean buffer_changed; /* Collect all data that we need to snapshot pending buffer, * lock ourselves to make sure everything matches */ GST_OBJECT_LOCK (self); if (self->has_pending_v_info) { self->v_info = self->pending_v_info; self->has_pending_v_info = FALSE; } buffer_changed = gst_buffer_replace (&self->buffer, self->pending_buffer); /* Ref overlays associated with current buffer */ for (i = 0; i < self->pending_overlays->len; i++) { GstClapperGdkOverlay *overlay = g_ptr_array_index (self->pending_overlays, i); g_ptr_array_insert (self->overlays, i, gst_clapper_gdk_overlay_ref (overlay)); } GST_OBJECT_UNLOCK (self); /* Draw black BG when no buffer or imported format has alpha */ if (!self->buffer || GST_VIDEO_INFO_HAS_ALPHA (&self->v_info)) gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, height)); if (self->buffer) { if (buffer_changed || !self->texture) { GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self); GST_TRACE_OBJECT (self, "Importing %" GST_PTR_FORMAT, self->buffer); g_clear_object (&self->texture); self->texture = importer_class->generate_texture (self, self->buffer, &self->v_info); } else { GST_TRACE_OBJECT (self, "Reusing texture from %" GST_PTR_FORMAT, self->buffer); } if (G_LIKELY (self->texture)) { gtk_snapshot_append_texture (snapshot, self->texture, &GRAPHENE_RECT_INIT (0, 0, width, height)); if (self->overlays->len > 0) { gfloat scale_x, scale_y; /* FIXME: GStreamer scales subtitles without considering pixel aspect ratio. * See: https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/20 */ scale_x = (gfloat) width / GST_VIDEO_INFO_WIDTH (&self->v_info); scale_y = (gfloat) height / GST_VIDEO_INFO_HEIGHT (&self->v_info); for (i = 0; i < self->overlays->len; i++) { GstClapperGdkOverlay *overlay = g_ptr_array_index (self->overlays, i); gtk_snapshot_append_texture (snapshot, overlay->texture, &GRAPHENE_RECT_INIT (overlay->x * scale_x, overlay->y * scale_y, overlay->width * scale_x, overlay->height * scale_y)); } } } else { GST_ERROR_OBJECT (self, "Failed import of %" GST_PTR_FORMAT, self->buffer); /* Draw black instead of texture on failure if not drawn already */ if (!GST_VIDEO_INFO_HAS_ALPHA (&self->v_info)) gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, height)); } } /* Unref all used overlays */ if (self->overlays->len > 0) g_ptr_array_remove_range (self->overlays, 0, self->overlays->len); } clapper-0.5.2/lib/gst/plugin/gstclapperimporter.h000066400000000000000000000107101425527005600221110ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include #include #include G_BEGIN_DECLS #define GST_TYPE_CLAPPER_IMPORTER (gst_clapper_importer_get_type()) #define GST_IS_CLAPPER_IMPORTER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_IMPORTER)) #define GST_IS_CLAPPER_IMPORTER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_IMPORTER)) #define GST_CLAPPER_IMPORTER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_IMPORTER, GstClapperImporterClass)) #define GST_CLAPPER_IMPORTER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_IMPORTER, GstClapperImporter)) #define GST_CLAPPER_IMPORTER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_IMPORTER, GstClapperImporterClass)) #define GST_CLAPPER_IMPORTER_CAST(obj) ((GstClapperImporter *)(obj)) #define GST_CLAPPER_IMPORTER_DEFINE(camel,lower,type) \ G_DEFINE_TYPE (camel, lower, type) \ G_MODULE_EXPORT GstClapperImporter *make_importer (void); \ G_MODULE_EXPORT GstCaps *make_caps (gboolean is_template, \ GstRank *rank, GStrv *context_types); typedef struct _GstClapperImporter GstClapperImporter; typedef struct _GstClapperImporterClass GstClapperImporterClass; #ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstClapperImporter, gst_object_unref) #endif struct _GstClapperImporter { GstObject parent; GstCaps *pending_caps; GstBuffer *pending_buffer, *buffer; GPtrArray *pending_overlays, *overlays; GstVideoInfo pending_v_info, v_info; gboolean has_pending_v_info; GdkTexture *texture; GdkRGBA bg; }; struct _GstClapperImporterClass { GstObjectClass parent_class; gboolean (* prepare) (GstClapperImporter *importer); void (* share_data) (GstClapperImporter *src, GstClapperImporter *dest); void (* set_caps) (GstClapperImporter *importer, GstCaps *caps); gboolean (* handle_context_query) (GstClapperImporter *importer, GstBaseSink *bsink, GstQuery *query); GstBufferPool * (* create_pool) (GstClapperImporter *importer, GstStructure **config); void (* add_allocation_metas) (GstClapperImporter *importer, GstQuery *query); GdkTexture * (* generate_texture) (GstClapperImporter *importer, GstBuffer *buffer, GstVideoInfo *v_info); }; GType gst_clapper_importer_get_type (void); gboolean gst_clapper_importer_prepare (GstClapperImporter *importer); void gst_clapper_importer_share_data (GstClapperImporter *importer, GstClapperImporter *dest); gboolean gst_clapper_importer_handle_context_query (GstClapperImporter *importer, GstBaseSink *bsink, GstQuery *query); GstBufferPool * gst_clapper_importer_create_pool (GstClapperImporter *importer, GstStructure **config); void gst_clapper_importer_add_allocation_metas (GstClapperImporter *importer, GstQuery *query); void gst_clapper_importer_set_caps (GstClapperImporter *importer, GstCaps *caps); void gst_clapper_importer_set_buffer (GstClapperImporter *importer, GstBuffer *buffer); void gst_clapper_importer_snapshot (GstClapperImporter *importer, GdkSnapshot *snapshot, gdouble width, gdouble height); G_END_DECLS clapper-0.5.2/lib/gst/plugin/gstclapperimporterloader.c000066400000000000000000000257231425527005600233050ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclapperimporterloader.h" #include "gstclapperimporter.h" #define GST_CAT_DEFAULT gst_clapper_importer_loader_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); #define parent_class gst_clapper_importer_loader_parent_class G_DEFINE_TYPE (GstClapperImporterLoader, gst_clapper_importer_loader, GST_TYPE_OBJECT); typedef GstClapperImporter* (* MakeImporter) (void); typedef GstCaps* (* MakeCaps) (gboolean is_template, GstRank *rank, GStrv *context_types); typedef struct { GModule *module; GstCaps *caps; GstRank rank; GStrv context_types; } GstClapperImporterData; static void gst_clapper_importer_data_free (GstClapperImporterData *data) { GST_TRACE ("Freeing importer data: %" GST_PTR_FORMAT, data); gst_clear_caps (&data->caps); g_strfreev (data->context_types); g_free (data); } static GstClapperImporterData * _obtain_importer_data (GModule *module, gboolean is_template) { MakeCaps make_caps; GstClapperImporterData *data; if (!g_module_symbol (module, "make_caps", (gpointer *) &make_caps) || make_caps == NULL) { GST_WARNING ("Make caps function missing in importer"); return NULL; } data = g_new0 (GstClapperImporterData, 1); data->module = module; data->caps = make_caps (is_template, &data->rank, &data->context_types); GST_TRACE ("Created importer data: %" GST_PTR_FORMAT, data); if (G_UNLIKELY (!data->caps)) { GST_ERROR ("Invalid importer without caps: %s", g_module_name (data->module)); gst_clapper_importer_data_free (data); return NULL; } GST_DEBUG ("Found importer: %s, caps: %" GST_PTR_FORMAT, g_module_name (data->module), data->caps); return data; } static GstClapperImporter * _obtain_importer_internal (GModule *module) { MakeImporter make_importer; GstClapperImporter *importer; if (!g_module_symbol (module, "make_importer", (gpointer *) &make_importer) || make_importer == NULL) { GST_WARNING ("Make function missing in importer"); return NULL; } importer = make_importer (); GST_TRACE ("Created importer: %" GST_PTR_FORMAT, importer); return importer; } static gpointer _obtain_available_modules_once (G_GNUC_UNUSED gpointer data) { GPtrArray *modules; GFile *dir; GFileEnumerator *dir_enum; GError *error = NULL; GST_INFO ("Preparing modules"); modules = g_ptr_array_new (); dir = g_file_new_for_path (CLAPPER_SINK_IMPORTER_PATH); if ((dir_enum = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error))) { while (TRUE) { GFileInfo *info = NULL; GModule *module; gchar *module_path; const gchar *module_name; if (!g_file_enumerator_iterate (dir_enum, &info, NULL, NULL, &error) || !info) break; module_name = g_file_info_get_name (info); if (!g_str_has_suffix (module_name, G_MODULE_SUFFIX)) continue; module_path = g_module_build_path (CLAPPER_SINK_IMPORTER_PATH, module_name); module = g_module_open (module_path, G_MODULE_BIND_LAZY); g_free (module_path); if (!module) { GST_WARNING ("Could not read module: %s, reason: %s", module_name, g_module_error ()); continue; } GST_INFO ("Found module: %s", module_name); g_ptr_array_add (modules, module); } g_object_unref (dir_enum); } g_object_unref (dir); if (error) { GST_ERROR ("Could not load module, reason: %s", (error->message) ? error->message : "unknown"); g_error_free (error); } return modules; } static const GPtrArray * gst_clapper_importer_loader_get_available_modules (void) { static GOnce once = G_ONCE_INIT; g_once (&once, _obtain_available_modules_once, NULL); return (const GPtrArray *) once.retval; } static gint _sort_importers_cb (gconstpointer a, gconstpointer b) { GstClapperImporterData *data_a, *data_b; data_a = *((GstClapperImporterData **) a); data_b = *((GstClapperImporterData **) b); return (data_b->rank - data_a->rank); } static GPtrArray * _obtain_available_importers (gboolean is_template) { const GPtrArray *modules; GPtrArray *importers; guint i; GST_DEBUG ("Checking available importers"); modules = gst_clapper_importer_loader_get_available_modules (); importers = g_ptr_array_new_with_free_func ( (GDestroyNotify) gst_clapper_importer_data_free); for (i = 0; i < modules->len; i++) { GModule *module = g_ptr_array_index (modules, i); GstClapperImporterData *data; if ((data = _obtain_importer_data (module, is_template))) g_ptr_array_add (importers, data); } g_ptr_array_sort (importers, (GCompareFunc) _sort_importers_cb); GST_DEBUG ("Found %i available importers", importers->len); return importers; } GstClapperImporterLoader * gst_clapper_importer_loader_new (void) { return g_object_new (GST_TYPE_CLAPPER_IMPORTER_LOADER, NULL); } static GstCaps * _make_caps_for_importers (const GPtrArray *importers) { GstCaps *caps = gst_caps_new_empty (); guint i; for (i = 0; i < importers->len; i++) { GstClapperImporterData *data = g_ptr_array_index (importers, i); gst_caps_append (caps, gst_caps_ref (data->caps)); } return caps; } GstPadTemplate * gst_clapper_importer_loader_make_sink_pad_template (void) { GPtrArray *importers; GstCaps *caps; GstPadTemplate *templ; /* This is only called once from sink class init function */ GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperimporterloader", 0, "Clapper Importer Loader"); GST_DEBUG ("Making sink pad template"); importers = _obtain_available_importers (TRUE); caps = _make_caps_for_importers (importers); g_ptr_array_unref (importers); if (G_UNLIKELY (gst_caps_is_empty (caps))) gst_caps_append (caps, gst_caps_new_any ()); templ = gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, caps); gst_caps_unref (caps); GST_TRACE ("Created sink pad template"); return templ; } GstCaps * gst_clapper_importer_loader_make_actual_caps (GstClapperImporterLoader *self) { return _make_caps_for_importers (self->importers); } static const GstClapperImporterData * _get_importer_data_for_caps (const GPtrArray *importers, const GstCaps *caps) { guint i; for (i = 0; i < importers->len; i++) { GstClapperImporterData *data = g_ptr_array_index (importers, i); if (!gst_caps_is_always_compatible (caps, data->caps)) continue; return data; } return NULL; } static const GstClapperImporterData * _get_importer_data_for_context_type (const GPtrArray *importers, const gchar *context_type) { guint i; for (i = 0; i < importers->len; i++) { GstClapperImporterData *data = g_ptr_array_index (importers, i); guint j; if (!data->context_types) continue; for (j = 0; data->context_types[j]; j++) { if (strcmp (context_type, data->context_types[j])) continue; return data; } } return NULL; } static gboolean _find_importer_internal (GstClapperImporterLoader *self, GstCaps *caps, GstQuery *query, GstClapperImporter **importer) { const GstClapperImporterData *data = NULL; GstClapperImporter *found_importer = NULL; GST_OBJECT_LOCK (self); if (caps) { GST_DEBUG_OBJECT (self, "Requested importer for caps: %" GST_PTR_FORMAT, caps); data = _get_importer_data_for_caps (self->importers, caps); } else if (query) { const gchar *context_type; gst_query_parse_context_type (query, &context_type); GST_DEBUG_OBJECT (self, "Requested importer for context: %s", context_type); data = _get_importer_data_for_context_type (self->importers, context_type); } GST_LOG_OBJECT (self, "Old importer path: %s, new path: %s", (self->last_module) ? g_module_name (self->last_module) : NULL, (data) ? g_module_name (data->module) : NULL); if (!data) { /* In case of missing importer for context query, leave the old one. * We should allow some queries to go through unresponded */ if (query) GST_DEBUG_OBJECT (self, "No importer for query, leaving old one"); else gst_clear_object (importer); goto finish; } if (*importer && (self->last_module == data->module)) { GST_DEBUG_OBJECT (self, "No importer change"); if (caps) gst_clapper_importer_set_caps (*importer, caps); goto finish; } found_importer = _obtain_importer_internal (data->module); if (*importer && found_importer) gst_clapper_importer_share_data (*importer, found_importer); gst_clear_object (importer); if (!found_importer || !gst_clapper_importer_prepare (found_importer)) { gst_clear_object (&found_importer); goto finish; } if (caps) gst_clapper_importer_set_caps (found_importer, caps); *importer = found_importer; finish: self->last_module = (*importer && data) ? data->module : NULL; GST_OBJECT_UNLOCK (self); return (*importer != NULL); } gboolean gst_clapper_importer_loader_find_importer_for_caps (GstClapperImporterLoader *self, GstCaps *caps, GstClapperImporter **importer) { return _find_importer_internal (self, caps, NULL, importer); } gboolean gst_clapper_importer_loader_find_importer_for_context_query (GstClapperImporterLoader *self, GstQuery *query, GstClapperImporter **importer) { return _find_importer_internal (self, NULL, query, importer); } static void gst_clapper_importer_loader_init (GstClapperImporterLoader *self) { } static void gst_clapper_importer_loader_constructed (GObject *object) { GstClapperImporterLoader *self = GST_CLAPPER_IMPORTER_LOADER_CAST (object); self->importers = _obtain_available_importers (FALSE); GST_CALL_PARENT (G_OBJECT_CLASS, constructed, (object)); } static void gst_clapper_importer_loader_finalize (GObject *object) { GstClapperImporterLoader *self = GST_CLAPPER_IMPORTER_LOADER_CAST (object); GST_TRACE ("Finalize"); g_ptr_array_unref (self->importers); GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); } static void gst_clapper_importer_loader_class_init (GstClapperImporterLoaderClass *klass) { GObjectClass *gobject_class = (GObjectClass *) klass; gobject_class->constructed = gst_clapper_importer_loader_constructed; gobject_class->finalize = gst_clapper_importer_loader_finalize; } clapper-0.5.2/lib/gst/plugin/gstclapperimporterloader.h000066400000000000000000000036631425527005600233110ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include #include "gstclapperimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_IMPORTER_LOADER (gst_clapper_importer_loader_get_type()) G_DECLARE_FINAL_TYPE (GstClapperImporterLoader, gst_clapper_importer_loader, GST, CLAPPER_IMPORTER_LOADER, GstObject) #define GST_CLAPPER_IMPORTER_LOADER_CAST(obj) ((GstClapperImporterLoader *)(obj)) struct _GstClapperImporterLoader { GstObject parent; GModule *last_module; GPtrArray *importers; }; GstClapperImporterLoader * gst_clapper_importer_loader_new (void); GstPadTemplate * gst_clapper_importer_loader_make_sink_pad_template (void); GstCaps * gst_clapper_importer_loader_make_actual_caps (GstClapperImporterLoader *loader); gboolean gst_clapper_importer_loader_find_importer_for_caps (GstClapperImporterLoader *loader, GstCaps *caps, GstClapperImporter **importer); gboolean gst_clapper_importer_loader_find_importer_for_context_query (GstClapperImporterLoader *loader, GstQuery *query, GstClapperImporter **importer); G_END_DECLS clapper-0.5.2/lib/gst/plugin/gstclapperpaintable.c000066400000000000000000000305631425527005600222120ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclapperpaintable.h" #define DEFAULT_PAR_N 1 #define DEFAULT_PAR_D 1 #define GST_CAT_DEFAULT gst_clapper_paintable_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); static void gst_clapper_paintable_iface_init (GdkPaintableInterface *iface); static void gst_clapper_paintable_dispose (GObject *object); static void gst_clapper_paintable_finalize (GObject *object); #define parent_class gst_clapper_paintable_parent_class G_DEFINE_TYPE_WITH_CODE (GstClapperPaintable, gst_clapper_paintable, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, gst_clapper_paintable_iface_init)); static void gst_clapper_paintable_class_init (GstClapperPaintableClass *klass) { GObjectClass *gobject_class = (GObjectClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperpaintable", 0, "Clapper Paintable"); gobject_class->dispose = gst_clapper_paintable_dispose; gobject_class->finalize = gst_clapper_paintable_finalize; } static void gst_clapper_paintable_init (GstClapperPaintable *self) { self->display_width = 1; self->display_height = 1; self->display_aspect_ratio = 1.0; self->par_n = DEFAULT_PAR_N; self->par_d = DEFAULT_PAR_D; g_mutex_init (&self->lock); g_mutex_init (&self->importer_lock); gst_video_info_init (&self->v_info); g_weak_ref_init (&self->widget, NULL); gdk_rgba_parse (&self->bg, "black"); } static void gst_clapper_paintable_dispose (GObject *object) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE (object); GST_CLAPPER_PAINTABLE_LOCK (self); if (self->draw_id > 0) { g_source_remove (self->draw_id); self->draw_id = 0; } GST_CLAPPER_PAINTABLE_UNLOCK (self); GST_CLAPPER_PAINTABLE_IMPORTER_LOCK (self); gst_clear_object (&self->importer); GST_CLAPPER_PAINTABLE_IMPORTER_UNLOCK (self); GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object)); } static void gst_clapper_paintable_finalize (GObject *object) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE (object); GST_TRACE ("Finalize"); g_weak_ref_clear (&self->widget); g_mutex_clear (&self->lock); g_mutex_clear (&self->importer_lock); GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); } static gboolean calculate_display_par (GstClapperPaintable *self, const GstVideoInfo *info) { gint width, height, par_n, par_d, req_par_n, req_par_d; gboolean success; width = GST_VIDEO_INFO_WIDTH (info); height = GST_VIDEO_INFO_HEIGHT (info); /* Cannot apply aspect ratio if there is no video */ if (width == 0 || height == 0) return FALSE; par_n = GST_VIDEO_INFO_PAR_N (info); par_d = GST_VIDEO_INFO_PAR_D (info); req_par_n = self->par_n; req_par_d = self->par_d; if (par_n == 0) par_n = 1; /* Use defaults if user set zero */ if (req_par_n == 0 || req_par_d == 0) { req_par_n = DEFAULT_PAR_N; req_par_d = DEFAULT_PAR_D; } GST_LOG_OBJECT (self, "PAR: %u/%u, DAR: %u/%u", par_n, par_d, req_par_n, req_par_d); if (!(success = gst_video_calculate_display_ratio (&self->display_ratio_num, &self->display_ratio_den, width, height, par_n, par_d, req_par_n, req_par_d))) { GST_ERROR_OBJECT (self, "Could not calculate display ratio values"); } return success; } static void invalidate_paintable_size_internal (GstClapperPaintable *self) { gint video_width, video_height; guint display_ratio_num, display_ratio_den; GST_CLAPPER_PAINTABLE_LOCK (self); video_width = GST_VIDEO_INFO_WIDTH (&self->v_info); video_height = GST_VIDEO_INFO_HEIGHT (&self->v_info); display_ratio_num = self->display_ratio_num; display_ratio_den = self->display_ratio_den; GST_CLAPPER_PAINTABLE_UNLOCK (self); if (video_height % display_ratio_den == 0) { GST_LOG ("Keeping video height"); self->display_width = (guint) gst_util_uint64_scale_int (video_height, display_ratio_num, display_ratio_den); self->display_height = video_height; } else if (video_width % display_ratio_num == 0) { GST_LOG ("Keeping video width"); self->display_width = video_width; self->display_height = (guint) gst_util_uint64_scale_int (video_width, display_ratio_den, display_ratio_num); } else { GST_LOG ("Approximating while keeping video height"); self->display_width = (guint) gst_util_uint64_scale_int (video_height, display_ratio_num, display_ratio_den); self->display_height = video_height; } self->display_aspect_ratio = ((gdouble) self->display_width / (gdouble) self->display_height); GST_DEBUG_OBJECT (self, "Invalidate paintable size, display: %dx%d", self->display_width, self->display_height); gdk_paintable_invalidate_size ((GdkPaintable *) self); } static gboolean invalidate_paintable_size_on_main_cb (GstClapperPaintable *self) { GST_CLAPPER_PAINTABLE_LOCK (self); self->draw_id = 0; GST_CLAPPER_PAINTABLE_UNLOCK (self); invalidate_paintable_size_internal (self); return G_SOURCE_REMOVE; } static gboolean update_paintable_on_main_cb (GstClapperPaintable *self) { gboolean size_changed; GST_CLAPPER_PAINTABLE_LOCK (self); /* Check if we will need to invalidate size */ if ((size_changed = self->pending_resize)) self->pending_resize = FALSE; self->draw_id = 0; GST_CLAPPER_PAINTABLE_UNLOCK (self); if (size_changed) invalidate_paintable_size_internal (self); GST_LOG_OBJECT (self, "Invalidate paintable contents"); gdk_paintable_invalidate_contents ((GdkPaintable *) self); return G_SOURCE_REMOVE; } GstClapperPaintable * gst_clapper_paintable_new (void) { return g_object_new (GST_TYPE_CLAPPER_PAINTABLE, NULL); } void gst_clapper_paintable_set_widget (GstClapperPaintable *self, GtkWidget *widget) { g_weak_ref_set (&self->widget, widget); } void gst_clapper_paintable_set_importer (GstClapperPaintable *self, GstClapperImporter *importer) { GST_CLAPPER_PAINTABLE_IMPORTER_LOCK (self); gst_object_replace ((GstObject **) &self->importer, GST_OBJECT_CAST (importer)); GST_CLAPPER_PAINTABLE_IMPORTER_UNLOCK (self); } void gst_clapper_paintable_queue_draw (GstClapperPaintable *self) { GST_CLAPPER_PAINTABLE_LOCK (self); if (self->draw_id > 0) { GST_CLAPPER_PAINTABLE_UNLOCK (self); GST_TRACE ("Already have pending draw"); return; } self->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc) update_paintable_on_main_cb, self, NULL); GST_CLAPPER_PAINTABLE_UNLOCK (self); } gboolean gst_clapper_paintable_set_video_info (GstClapperPaintable *self, const GstVideoInfo *v_info) { GST_CLAPPER_PAINTABLE_LOCK (self); if (gst_video_info_is_equal (&self->v_info, v_info)) { GST_CLAPPER_PAINTABLE_UNLOCK (self); return TRUE; } /* Reject info if values would cause integer overflow */ if (G_UNLIKELY (!calculate_display_par (self, v_info))) { GST_CLAPPER_PAINTABLE_UNLOCK (self); return FALSE; } self->pending_resize = TRUE; self->v_info = *v_info; GST_CLAPPER_PAINTABLE_UNLOCK (self); return TRUE; } void gst_clapper_paintable_set_pixel_aspect_ratio (GstClapperPaintable *self, gint par_n, gint par_d) { gboolean success; GST_CLAPPER_PAINTABLE_LOCK (self); /* No change */ if (self->par_n == par_n && self->par_d == par_d) { GST_CLAPPER_PAINTABLE_UNLOCK (self); return; } self->par_n = par_n; self->par_d = par_d; /* Check if we can accept new values. This will update * display `ratio_num` and `ratio_den` only when successful */ success = calculate_display_par (self, &self->v_info); /* If paintable update is queued, wait for it, otherwise invalidate * size only for change to be applied even when paused */ if (!success || self->draw_id > 0) { self->pending_resize = success; GST_CLAPPER_PAINTABLE_UNLOCK (self); return; } self->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc) invalidate_paintable_size_on_main_cb, self, NULL); GST_CLAPPER_PAINTABLE_UNLOCK (self); } /* * GdkPaintableInterface */ static void gst_clapper_paintable_snapshot_internal (GstClapperPaintable *self, GdkSnapshot *snapshot, gdouble width, gdouble height, gint widget_width, gint widget_height) { gfloat scale_x, scale_y; GST_LOG_OBJECT (self, "Snapshot"); scale_x = (gfloat) width / self->display_width; scale_y = (gfloat) height / self->display_height; /* Apply black borders when keeping aspect ratio */ if (scale_x == scale_y || abs (scale_x - scale_y) <= FLT_EPSILON) { if (widget_height - height > 0) { /* XXX: Top uses integer to work with GTK rounding (not going offscreen) */ gint top_bar_height = (widget_height - height) / 2; gdouble bottom_bar_height = (widget_height - top_bar_height - height); gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, -top_bar_height)); gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, height, width, bottom_bar_height)); } else if (widget_width - width > 0) { gint left_bar_width = (widget_width - width) / 2; gdouble right_bar_width = (widget_width - left_bar_width - width); gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, -left_bar_width, height)); gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (width, 0, right_bar_width, height)); } } GST_CLAPPER_PAINTABLE_IMPORTER_LOCK (self); if (self->importer) { gst_clapper_importer_snapshot (self->importer, snapshot, width, height); } else { GST_LOG_OBJECT (self, "No texture importer, drawing black"); gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, height)); } GST_CLAPPER_PAINTABLE_IMPORTER_UNLOCK (self); } static void gst_clapper_paintable_snapshot (GdkPaintable *paintable, GdkSnapshot *snapshot, gdouble width, gdouble height) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable); GtkWidget *widget; gint widget_width = 0, widget_height = 0; if ((widget = g_weak_ref_get (&self->widget))) { widget_width = gtk_widget_get_width (widget); widget_height = gtk_widget_get_height (widget); g_object_unref (widget); } gst_clapper_paintable_snapshot_internal (self, snapshot, width, height, widget_width, widget_height); } static GdkPaintable * gst_clapper_paintable_get_current_image (GdkPaintable *paintable) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable); GtkSnapshot *snapshot = gtk_snapshot_new (); /* Snapshot without widget size in order to get * paintable without black borders */ gst_clapper_paintable_snapshot_internal (self, snapshot, self->display_width, self->display_height, 0, 0); return gtk_snapshot_free_to_paintable (snapshot, NULL); } static gint gst_clapper_paintable_get_intrinsic_width (GdkPaintable *paintable) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable); return self->display_width; } static gint gst_clapper_paintable_get_intrinsic_height (GdkPaintable *paintable) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable); return self->display_height; } static gdouble gst_clapper_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable) { GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable); return self->display_aspect_ratio; } static void gst_clapper_paintable_iface_init (GdkPaintableInterface *iface) { iface->snapshot = gst_clapper_paintable_snapshot; iface->get_current_image = gst_clapper_paintable_get_current_image; iface->get_intrinsic_width = gst_clapper_paintable_get_intrinsic_width; iface->get_intrinsic_height = gst_clapper_paintable_get_intrinsic_height; iface->get_intrinsic_aspect_ratio = gst_clapper_paintable_get_intrinsic_aspect_ratio; } clapper-0.5.2/lib/gst/plugin/gstclapperpaintable.h000066400000000000000000000057141425527005600222170ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include #include #include #include "gstclapperimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_PAINTABLE (gst_clapper_paintable_get_type()) G_DECLARE_FINAL_TYPE (GstClapperPaintable, gst_clapper_paintable, GST, CLAPPER_PAINTABLE, GObject) #define GST_CLAPPER_PAINTABLE_CAST(obj) ((GstClapperPaintable *)(obj)) #define GST_CLAPPER_PAINTABLE_GET_LOCK(obj) (&GST_CLAPPER_PAINTABLE_CAST(obj)->lock) #define GST_CLAPPER_PAINTABLE_LOCK(obj) g_mutex_lock (GST_CLAPPER_PAINTABLE_GET_LOCK(obj)) #define GST_CLAPPER_PAINTABLE_UNLOCK(obj) g_mutex_unlock (GST_CLAPPER_PAINTABLE_GET_LOCK(obj)) #define GST_CLAPPER_PAINTABLE_IMPORTER_GET_LOCK(obj) (&GST_CLAPPER_PAINTABLE_CAST(obj)->importer_lock) #define GST_CLAPPER_PAINTABLE_IMPORTER_LOCK(obj) g_mutex_lock (GST_CLAPPER_PAINTABLE_IMPORTER_GET_LOCK(obj)) #define GST_CLAPPER_PAINTABLE_IMPORTER_UNLOCK(obj) g_mutex_unlock (GST_CLAPPER_PAINTABLE_IMPORTER_GET_LOCK(obj)) struct _GstClapperPaintable { GObject parent; GMutex lock; GMutex importer_lock; GstVideoInfo v_info; GdkRGBA bg; GWeakRef widget; GstClapperImporter *importer; /* Sink properties */ gint par_n, par_d; /* Resize */ gboolean pending_resize; guint display_ratio_num; guint display_ratio_den; /* GdkPaintableInterface */ gint display_width; gint display_height; gdouble display_aspect_ratio; /* Pending draw signal id */ guint draw_id; }; GstClapperPaintable * gst_clapper_paintable_new (void); void gst_clapper_paintable_queue_draw (GstClapperPaintable *paintable); void gst_clapper_paintable_set_widget (GstClapperPaintable *paintable, GtkWidget *widget); void gst_clapper_paintable_set_importer (GstClapperPaintable *paintable, GstClapperImporter *importer); gboolean gst_clapper_paintable_set_video_info (GstClapperPaintable *paintable, const GstVideoInfo *v_info); void gst_clapper_paintable_set_pixel_aspect_ratio (GstClapperPaintable *paintable, gint par_n, gint par_d); G_END_DECLS clapper-0.5.2/lib/gst/plugin/gstclappersink.c000066400000000000000000000637171425527005600212260ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclappersink.h" #include "gstgtkutils.h" #define DEFAULT_FORCE_ASPECT_RATIO TRUE #define DEFAULT_PAR_N 1 #define DEFAULT_PAR_D 1 #define DEFAULT_KEEP_LAST_FRAME FALSE #define WINDOW_CSS_CLASS_NAME "clappersinkwindow" enum { PROP_0, PROP_WIDGET, PROP_FORCE_ASPECT_RATIO, PROP_PIXEL_ASPECT_RATIO, PROP_KEEP_LAST_FRAME, PROP_LAST }; #define GST_CAT_DEFAULT gst_clapper_sink_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); static void gst_clapper_sink_navigation_interface_init ( GstNavigationInterface *iface); #define parent_class gst_clapper_sink_parent_class G_DEFINE_TYPE_WITH_CODE (GstClapperSink, gst_clapper_sink, GST_TYPE_VIDEO_SINK, G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION, gst_clapper_sink_navigation_interface_init)); GST_ELEMENT_REGISTER_DEFINE (clappersink, "clappersink", GST_RANK_NONE, GST_TYPE_CLAPPER_SINK); static void window_clear_no_lock (GstClapperSink *self) { if (!self->window) return; GST_TRACE_OBJECT (self, "Window clear"); if (self->window_destroy_id) { g_signal_handler_disconnect (self->window, self->window_destroy_id); self->window_destroy_id = 0; } self->window = NULL; self->presented_window = FALSE; } static void widget_clear_no_lock (GstClapperSink *self) { if (!self->widget) return; GST_TRACE_OBJECT (self, "Widget clear"); if (self->widget_destroy_id) { g_signal_handler_disconnect (self->widget, self->widget_destroy_id); self->widget_destroy_id = 0; } g_clear_object (&self->widget); } static void widget_destroy_cb (GtkWidget *widget, GstClapperSink *self) { GST_CLAPPER_SINK_LOCK (self); widget_clear_no_lock (self); GST_CLAPPER_SINK_UNLOCK (self); } static void window_destroy_cb (GtkWidget *window, GstClapperSink *self) { GST_DEBUG_OBJECT (self, "Window destroy"); GST_CLAPPER_SINK_LOCK (self); widget_clear_no_lock (self); window_clear_no_lock (self); GST_CLAPPER_SINK_UNLOCK (self); } static void calculate_stream_coords (GstClapperSink *self, GtkWidget *widget, gdouble x, gdouble y, gdouble *stream_x, gdouble *stream_y) { GstVideoRectangle result; gint scaled_width, scaled_height, scale_factor; gint video_width, video_height; gboolean force_aspect_ratio; GST_CLAPPER_SINK_LOCK (self); video_width = GST_VIDEO_INFO_WIDTH (&self->v_info); video_height = GST_VIDEO_INFO_HEIGHT (&self->v_info); force_aspect_ratio = self->force_aspect_ratio; GST_CLAPPER_SINK_UNLOCK (self); scale_factor = gtk_widget_get_scale_factor (widget); scaled_width = gtk_widget_get_width (widget) * scale_factor; scaled_height = gtk_widget_get_height (widget) * scale_factor; if (force_aspect_ratio) { GstVideoRectangle src, dst; src.x = 0; src.y = 0; src.w = gdk_paintable_get_intrinsic_width ((GdkPaintable *) self->paintable); src.h = gdk_paintable_get_intrinsic_height ((GdkPaintable *) self->paintable); dst.x = 0; dst.y = 0; dst.w = scaled_width; dst.h = scaled_height; gst_video_center_rect (&src, &dst, &result, TRUE); } else { result.x = 0; result.y = 0; result.w = scaled_width; result.h = scaled_height; } /* Display coordinates to stream coordinates */ *stream_x = (result.w > 0) ? (x - result.x) / result.w * video_width : 0; *stream_y = (result.h > 0) ? (y - result.y) / result.h * video_height : 0; /* Clip to stream size */ *stream_x = CLAMP (*stream_x, 0, video_width); *stream_y = CLAMP (*stream_y, 0, video_height); GST_LOG ("Transform coords %fx%f => %fx%f", x, y, *stream_x, *stream_y); } static void gst_clapper_sink_widget_motion_event (GtkEventControllerMotion *motion, gdouble x, gdouble y, GstClapperSink *self) { GtkWidget *widget; gdouble stream_x, stream_y; gboolean is_inactive; if (x == self->last_pos_x && y == self->last_pos_y) return; GST_OBJECT_LOCK (self); is_inactive = (GST_STATE (self) < GST_STATE_PLAYING); GST_OBJECT_UNLOCK (self); if (is_inactive) return; self->last_pos_x = x; self->last_pos_y = y; widget = gtk_event_controller_get_widget ((GtkEventController *) motion); calculate_stream_coords (self, widget, x, y, &stream_x, &stream_y); GST_LOG ("Event \"mouse-move\", x: %f, y: %f", stream_x, stream_y); gst_navigation_send_mouse_event ((GstNavigation *) self, "mouse-move", 0, stream_x, stream_y); } static void gst_clapper_sink_widget_button_event (GtkGestureClick *click, gint n_press, gdouble x, gdouble y, GstClapperSink *self) { GtkWidget *widget; GdkEvent *event; GdkEventType event_type; const gchar *event_name; gdouble stream_x, stream_y; gboolean is_inactive; GST_OBJECT_LOCK (self); is_inactive = (GST_STATE (self) < GST_STATE_PLAYING); GST_OBJECT_UNLOCK (self); if (is_inactive) return; event = gtk_event_controller_get_current_event ((GtkEventController *) click); event_type = gdk_event_get_event_type (event); /* FIXME: Touchscreen handling should probably use new touch events from GStreamer 1.22 */ event_name = (event_type == GDK_BUTTON_PRESS || event_type == GDK_TOUCH_BEGIN) ? "mouse-button-press" : (event_type == GDK_BUTTON_RELEASE || event_type == GDK_TOUCH_END) ? "mouse-button-release" : NULL; /* Can be NULL on touch */ if (!event_name) return; widget = gtk_event_controller_get_widget ((GtkEventController *) click); calculate_stream_coords (self, widget, x, y, &stream_x, &stream_y); GST_LOG ("Event \"%s\", x: %f, y: %f", event_name, stream_x, stream_y); /* Gesture is set to handle only primary button, so we do not have to check */ gst_navigation_send_mouse_event ((GstNavigation *) self, event_name, 1, stream_x, stream_y); } /* Must call from main thread only with a lock */ static GtkWidget * gst_clapper_sink_get_widget (GstClapperSink *self) { if (G_UNLIKELY (!self->widget)) { GtkEventController *controller; GtkGesture *gesture; /* Make sure GTK is initialized */ if (!gtk_init_check ()) { GST_ERROR_OBJECT (self, "Could not ensure GTK initialization"); return NULL; } self->widget = gtk_picture_new (); /* Otherwise widget in grid will appear as a 1x1px * video which might be misleading for users */ gtk_widget_set_hexpand (self->widget, TRUE); gtk_widget_set_vexpand (self->widget, TRUE); gtk_widget_set_focusable (self->widget, TRUE); gtk_widget_set_can_focus (self->widget, TRUE); controller = gtk_event_controller_motion_new (); g_signal_connect (controller, "motion", G_CALLBACK (gst_clapper_sink_widget_motion_event), self); gtk_widget_add_controller (self->widget, controller); gesture = gtk_gesture_click_new (); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 1); g_signal_connect (gesture, "pressed", G_CALLBACK (gst_clapper_sink_widget_button_event), self); g_signal_connect (gesture, "released", G_CALLBACK (gst_clapper_sink_widget_button_event), self); gtk_widget_add_controller (self->widget, GTK_EVENT_CONTROLLER (gesture)); /* TODO: Implement touch events once we depend on GStreamer 1.22 */ /* Take floating ref */ g_object_ref_sink (self->widget); /* Set widget back pointer */ gst_clapper_paintable_set_widget (self->paintable, self->widget); /* Set earlier remembered property */ gtk_picture_set_keep_aspect_ratio (GTK_PICTURE (self->widget), self->force_aspect_ratio); gtk_picture_set_paintable (GTK_PICTURE (self->widget), GDK_PAINTABLE (self->paintable)); self->widget_destroy_id = g_signal_connect (self->widget, "destroy", G_CALLBACK (widget_destroy_cb), self); } return self->widget; } static GtkWidget * gst_clapper_sink_obtain_widget (GstClapperSink *self) { GtkWidget *widget; GST_CLAPPER_SINK_LOCK (self); widget = gst_clapper_sink_get_widget (self); if (widget) g_object_ref (widget); GST_CLAPPER_SINK_UNLOCK (self); return widget; } static void gst_clapper_sink_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (object); GST_CLAPPER_SINK_LOCK (self); switch (prop_id) { case PROP_WIDGET: if (self->widget) { g_value_set_object (value, self->widget); } else { GtkWidget *widget; GST_CLAPPER_SINK_UNLOCK (self); widget = gst_gtk_invoke_on_main ((GThreadFunc) gst_clapper_sink_obtain_widget, self); GST_CLAPPER_SINK_LOCK (self); g_value_set_object (value, widget); g_object_unref (widget); } break; case PROP_FORCE_ASPECT_RATIO: g_value_set_boolean (value, self->force_aspect_ratio); break; case PROP_PIXEL_ASPECT_RATIO: gst_value_set_fraction (value, self->par_n, self->par_d); break; case PROP_KEEP_LAST_FRAME: g_value_set_boolean (value, self->keep_last_frame); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } GST_CLAPPER_SINK_UNLOCK (self); } static void gst_clapper_sink_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (object); GST_CLAPPER_SINK_LOCK (self); switch (prop_id) { case PROP_FORCE_ASPECT_RATIO: self->force_aspect_ratio = g_value_get_boolean (value); if (self->widget) { gtk_picture_set_keep_aspect_ratio (GTK_PICTURE (self->widget), self->force_aspect_ratio); } break; case PROP_PIXEL_ASPECT_RATIO: self->par_n = gst_value_get_fraction_numerator (value); self->par_d = gst_value_get_fraction_denominator (value); gst_clapper_paintable_set_pixel_aspect_ratio (self->paintable, self->par_n, self->par_d); break; case PROP_KEEP_LAST_FRAME: self->keep_last_frame = g_value_get_boolean (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } GST_CLAPPER_SINK_UNLOCK (self); } static void gst_clapper_sink_navigation_send_event (GstNavigation *navigation, GstStructure *structure) { GstClapperSink *sink = GST_CLAPPER_SINK_CAST (navigation); GstEvent *event; GST_TRACE_OBJECT (sink, "Navigation event: %" GST_PTR_FORMAT, structure); event = gst_event_new_navigation (structure); if (G_LIKELY (event)) { GstPad *pad; pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink)); if (G_LIKELY (pad)) { 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_object_unref (pad); } gst_event_unref (event); } } static gboolean gst_clapper_sink_propose_allocation (GstBaseSink *bsink, GstQuery *query) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); GstCaps *caps; GstVideoInfo info; guint size, min_buffers; gboolean need_pool; gst_query_parse_allocation (query, &caps, &need_pool); if (!caps) { GST_DEBUG_OBJECT (self, "No caps specified"); return FALSE; } if (!gst_video_info_from_caps (&info, caps)) { GST_DEBUG_OBJECT (self, "Invalid caps specified"); return FALSE; } /* Normal size of a frame */ size = GST_VIDEO_INFO_SIZE (&info); /* We keep around current buffer and a pending one */ min_buffers = 3; if (need_pool) { GstBufferPool *pool; GstStructure *config = NULL; GST_DEBUG_OBJECT (self, "Need to create buffer pool"); GST_CLAPPER_SINK_LOCK (self); pool = gst_clapper_importer_create_pool (self->importer, &config); GST_CLAPPER_SINK_UNLOCK (self); if (pool) { /* If we did not get config, use default one */ if (!config) config = gst_buffer_pool_get_config (pool); gst_buffer_pool_config_set_params (config, caps, size, min_buffers, 0); if (!gst_buffer_pool_set_config (pool, config)) { gst_object_unref (pool); GST_ERROR_OBJECT (self, "Failed to set config"); return FALSE; } gst_query_add_allocation_pool (query, pool, size, min_buffers, 0); gst_object_unref (pool); } else if (config) { GST_WARNING_OBJECT (self, "Got config without a pool to apply it"); gst_structure_free (config); } } GST_CLAPPER_SINK_LOCK (self); gst_clapper_importer_add_allocation_metas (self->importer, query); GST_CLAPPER_SINK_UNLOCK (self); return TRUE; } static gboolean gst_clapper_sink_query (GstBaseSink *bsink, GstQuery *query) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); gboolean res = FALSE; GST_CLAPPER_SINK_LOCK (self); if (GST_QUERY_TYPE (query) == GST_QUERY_CONTEXT) { gboolean is_inactive; GST_OBJECT_LOCK (self); is_inactive = (GST_STATE (self) < GST_STATE_PAUSED); GST_OBJECT_UNLOCK (self); /* Some random context query in the middle of playback * should not trigger importer replacement */ if (is_inactive) gst_clapper_importer_loader_find_importer_for_context_query (self->loader, query, &self->importer); if (self->importer) res = gst_clapper_importer_handle_context_query (self->importer, bsink, query); } GST_CLAPPER_SINK_UNLOCK (self); if (res) return TRUE; return GST_BASE_SINK_CLASS (parent_class)->query (bsink, query); } static gboolean gst_clapper_sink_start_on_main (GstClapperSink *self) { GtkWidget *widget; GST_CLAPPER_SINK_LOCK (self); /* Make sure widget is created */ if (!(widget = gst_clapper_sink_get_widget (self))) { GST_CLAPPER_SINK_UNLOCK (self); return FALSE; } /* When no toplevel window, make our own */ if (G_UNLIKELY (!gtk_widget_get_root (widget) && !self->window)) { GtkWidget *toplevel, *parent; GtkCssProvider *provider; gchar *win_title; if ((parent = gtk_widget_get_parent (widget))) { GtkWidget *temp_parent; while ((temp_parent = gtk_widget_get_parent (parent))) parent = temp_parent; } toplevel = (parent) ? parent : widget; self->window = (GtkWindow *) gtk_window_new (); gtk_widget_add_css_class (GTK_WIDGET (self->window), WINDOW_CSS_CLASS_NAME); provider = gtk_css_provider_new (); gtk_css_provider_load_from_data (provider, "." WINDOW_CSS_CLASS_NAME " { background: none; }", -1); gtk_style_context_add_provider_for_display ( gdk_display_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref (provider); win_title = g_strdup_printf ("Clapper Sink - GTK %u.%u.%u Window", gtk_get_major_version (), gtk_get_minor_version (), gtk_get_micro_version ()); /* Set some common default size, adding stock headerbar height * to it in order to display 4:3 aspect video widget */ gtk_window_set_default_size (self->window, 640, 480 + 37); gtk_window_set_title (self->window, win_title); gtk_window_set_child (self->window, toplevel); g_free (win_title); self->window_destroy_id = g_signal_connect (self->window, "destroy", G_CALLBACK (window_destroy_cb), self); } GST_CLAPPER_SINK_UNLOCK (self); return TRUE; } static gboolean window_present_on_main_idle (GtkWindow *window) { GST_INFO ("Presenting window"); gtk_window_present (window); return G_SOURCE_REMOVE; } static gboolean gst_clapper_sink_start (GstBaseSink *bsink) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); GST_INFO_OBJECT (self, "Start"); if (G_UNLIKELY (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) gst_clapper_sink_start_on_main, self)))) { GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND, ("GtkWidget could not be created"), (NULL)); return FALSE; } return TRUE; } static gboolean gst_clapper_sink_stop_on_main (GstClapperSink *self) { GtkWindow *window = NULL; GST_CLAPPER_SINK_LOCK (self); if (self->window) window = g_object_ref (self->window); GST_CLAPPER_SINK_UNLOCK (self); if (window) { gtk_window_destroy (window); g_object_unref (window); } return TRUE; } static gboolean gst_clapper_sink_stop (GstBaseSink *bsink) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); gboolean has_window; GST_INFO_OBJECT (self, "Stop"); GST_CLAPPER_SINK_LOCK (self); has_window = (self->window != NULL); GST_CLAPPER_SINK_UNLOCK (self); if (G_UNLIKELY (has_window)) { return (! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) gst_clapper_sink_stop_on_main, self)); } return TRUE; } static GstStateChangeReturn gst_clapper_sink_change_state (GstElement *element, GstStateChange transition) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (element); GST_DEBUG_OBJECT (self, "Changing state: %s => %s", gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)), gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition))); switch (transition) { case GST_STATE_CHANGE_PAUSED_TO_READY: GST_CLAPPER_SINK_LOCK (self); if (!self->keep_last_frame && self->importer) { gst_clapper_importer_set_buffer (self->importer, NULL); gst_clapper_paintable_queue_draw (self->paintable); } GST_CLAPPER_SINK_UNLOCK (self); break; case GST_STATE_CHANGE_PAUSED_TO_PLAYING: GST_CLAPPER_SINK_LOCK (self); if (G_UNLIKELY (self->window && !self->presented_window)) { g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc) window_present_on_main_idle, g_object_ref (self->window), (GDestroyNotify) g_object_unref); self->presented_window = TRUE; } GST_CLAPPER_SINK_UNLOCK (self); break; default: break; } return GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); } static void gst_clapper_sink_get_times (GstBaseSink *bsink, GstBuffer *buffer, GstClockTime *start, GstClockTime *end) { if (!GST_BUFFER_TIMESTAMP_IS_VALID (buffer)) return; *start = GST_BUFFER_TIMESTAMP (buffer); if (GST_BUFFER_DURATION_IS_VALID (buffer)) { *end = *start + GST_BUFFER_DURATION (buffer); } else { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); gint fps_n, fps_d; GST_CLAPPER_SINK_LOCK (self); fps_n = GST_VIDEO_INFO_FPS_N (&self->v_info); fps_d = GST_VIDEO_INFO_FPS_D (&self->v_info); GST_CLAPPER_SINK_UNLOCK (self); if (fps_n > 0) *end = *start + gst_util_uint64_scale_int (GST_SECOND, fps_d, fps_n); } } static GstCaps * gst_clapper_sink_get_caps (GstBaseSink *bsink, GstCaps *filter) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); GstCaps *result, *tmp; tmp = gst_clapper_importer_loader_make_actual_caps (self->loader); if (filter) { GST_DEBUG ("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; } GST_DEBUG ("Returning caps: %" GST_PTR_FORMAT, result); return result; } static gboolean gst_clapper_sink_set_caps (GstBaseSink *bsink, GstCaps *caps) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink); GST_INFO_OBJECT (self, "Set caps: %" GST_PTR_FORMAT, caps); GST_CLAPPER_SINK_LOCK (self); if (G_UNLIKELY (!self->widget)) { GST_CLAPPER_SINK_UNLOCK (self); GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND, ("Output widget was destroyed"), (NULL)); return FALSE; } if (!gst_clapper_importer_loader_find_importer_for_caps (self->loader, caps, &self->importer)) { GST_CLAPPER_SINK_UNLOCK (self); GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND, ("No importer for given caps found"), (NULL)); return FALSE; } gst_clapper_paintable_set_importer (self->paintable, self->importer); GST_CLAPPER_SINK_UNLOCK (self); return GST_BASE_SINK_CLASS (parent_class)->set_caps (bsink, caps); } static gboolean gst_clapper_sink_set_info (GstVideoSink *vsink, GstCaps *caps, const GstVideoInfo *info) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink); gboolean res; GST_CLAPPER_SINK_LOCK (self); self->v_info = *info; GST_DEBUG_OBJECT (self, "Video info changed"); res = gst_clapper_paintable_set_video_info (self->paintable, info); GST_CLAPPER_SINK_UNLOCK (self); return res; } static GstFlowReturn gst_clapper_sink_show_frame (GstVideoSink *vsink, GstBuffer *buffer) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink); GST_TRACE ("Got %" GST_PTR_FORMAT, buffer); GST_CLAPPER_SINK_LOCK (self); if (G_UNLIKELY (!self->widget)) { GST_CLAPPER_SINK_UNLOCK (self); GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND, ("Output widget was destroyed"), (NULL)); return GST_FLOW_ERROR; } gst_clapper_importer_set_buffer (self->importer, buffer); gst_clapper_paintable_queue_draw (self->paintable); GST_CLAPPER_SINK_UNLOCK (self); return GST_FLOW_OK; } static void gst_clapper_sink_init (GstClapperSink *self) { GObjectClass *gobject_class; gobject_class = (GObjectClass *) GST_CLAPPER_SINK_GET_CLASS (self); /* HACK: install here instead of class init to avoid GStreamer * plugin scanner GObject type conflicts with older GTK versions */ if (!g_object_class_find_property (gobject_class, "widget")) { g_object_class_install_property (gobject_class, PROP_WIDGET, g_param_spec_object ("widget", "GTK Widget", "The GtkWidget to place in the widget hierarchy", GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); } self->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO; self->par_n = DEFAULT_PAR_N; self->par_d = DEFAULT_PAR_D; self->keep_last_frame = DEFAULT_KEEP_LAST_FRAME; g_mutex_init (&self->lock); gst_video_info_init (&self->v_info); self->paintable = gst_clapper_paintable_new (); self->loader = gst_clapper_importer_loader_new (); } static void gst_clapper_sink_dispose (GObject *object) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (object); GST_CLAPPER_SINK_LOCK (self); window_clear_no_lock (self); widget_clear_no_lock (self); g_clear_object (&self->paintable); gst_clear_object (&self->importer); GST_CLAPPER_SINK_UNLOCK (self); GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object)); } static void gst_clapper_sink_finalize (GObject *object) { GstClapperSink *self = GST_CLAPPER_SINK_CAST (object); GST_TRACE ("Finalize"); gst_clear_object (&self->loader); g_mutex_clear (&self->lock); GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); } static void gst_clapper_sink_class_init (GstClapperSinkClass *klass) { GstPadTemplate *sink_pad_templ; GObjectClass *gobject_class = (GObjectClass *) klass; GstElementClass *gstelement_class = (GstElementClass *) klass; GstBaseSinkClass *gstbasesink_class = (GstBaseSinkClass *) klass; GstVideoSinkClass *gstvideosink_class = (GstVideoSinkClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappersink", 0, "Clapper Sink"); gobject_class->get_property = gst_clapper_sink_get_property; gobject_class->set_property = gst_clapper_sink_set_property; gobject_class->dispose = gst_clapper_sink_dispose; gobject_class->finalize = gst_clapper_sink_finalize; 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)); gstelement_class->change_state = gst_clapper_sink_change_state; gstbasesink_class->get_caps = gst_clapper_sink_get_caps; gstbasesink_class->set_caps = gst_clapper_sink_set_caps; gstbasesink_class->get_times = gst_clapper_sink_get_times; gstbasesink_class->propose_allocation = gst_clapper_sink_propose_allocation; gstbasesink_class->query = gst_clapper_sink_query; gstbasesink_class->start = gst_clapper_sink_start; gstbasesink_class->stop = gst_clapper_sink_stop; gstvideosink_class->set_info = gst_clapper_sink_set_info; gstvideosink_class->show_frame = gst_clapper_sink_show_frame; gst_element_class_set_static_metadata (gstelement_class, "Clapper video sink", "Sink/Video", "A GTK4 video sink used by Clapper media player", "Rafał Dzięgiel "); sink_pad_templ = gst_clapper_importer_loader_make_sink_pad_template (); gst_element_class_add_pad_template (gstelement_class, sink_pad_templ); } /* * GstNavigationInterface */ static void gst_clapper_sink_navigation_interface_init (GstNavigationInterface *iface) { /* TODO: Port to "send_event_simple" once we depend on GStreamer 1.22 */ iface->send_event = gst_clapper_sink_navigation_send_event; } clapper-0.5.2/lib/gst/plugin/gstclappersink.h000066400000000000000000000042511425527005600212170ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include #include #include #include #include "gstclapperpaintable.h" #include "gstclapperimporterloader.h" #include "gstclapperimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_SINK (gst_clapper_sink_get_type()) G_DECLARE_FINAL_TYPE (GstClapperSink, gst_clapper_sink, GST, CLAPPER_SINK, GstVideoSink) #define GST_CLAPPER_SINK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_SINK, GstClapperSinkClass)) #define GST_CLAPPER_SINK_CAST(obj) ((GstClapperSink *)(obj)) #define GST_CLAPPER_SINK_GET_LOCK(obj) (&GST_CLAPPER_SINK_CAST(obj)->lock) #define GST_CLAPPER_SINK_LOCK(obj) g_mutex_lock (GST_CLAPPER_SINK_GET_LOCK(obj)) #define GST_CLAPPER_SINK_UNLOCK(obj) g_mutex_unlock (GST_CLAPPER_SINK_GET_LOCK(obj)) struct _GstClapperSink { GstVideoSink parent; GMutex lock; GstClapperPaintable *paintable; GstClapperImporterLoader *loader; GstClapperImporter *importer; GstVideoInfo v_info; GtkWidget *widget; GtkWindow *window; gboolean presented_window; /* Properties */ gboolean force_aspect_ratio; gint par_n, par_d; gboolean keep_last_frame; /* Position coords */ gdouble last_pos_x; gdouble last_pos_y; gulong widget_destroy_id; gulong window_destroy_id; }; GST_ELEMENT_REGISTER_DECLARE (clappersink); G_END_DECLS clapper-0.5.2/lib/gst/plugin/gstgdkformats.h000066400000000000000000000027011425527005600210430ustar00rootroot00000000000000/* * Copyright (C) 2022 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 #if G_BYTE_ORDER == G_LITTLE_ENDIAN #define GST_GDK_MEMORY_ENDIAN_FORMATS "RGBA64_LE" #define GST_GDK_GL_TEXTURE_ENDIAN_FORMATS "RGBA64_LE" #elif G_BYTE_ORDER == G_BIG_ENDIAN #define GST_GDK_MEMORY_ENDIAN_FORMATS "RGBA64_BE" #define GST_GDK_GL_TEXTURE_ENDIAN_FORMATS "RGBA64_BE" #endif #define GST_GDK_MEMORY_FORMATS \ GST_GDK_MEMORY_ENDIAN_FORMATS ", " \ "ABGR, BGRA, ARGB, RGBA, BGRx, RGBx, BGR, RGB" /* Formats that `GdkGLTexture` supports */ #define GST_GDK_GL_TEXTURE_FORMATS \ GST_GDK_GL_TEXTURE_ENDIAN_FORMATS ", " \ "RGBA, RGBx, RGB" clapper-0.5.2/lib/gst/plugin/gstgtkutils.c000066400000000000000000000100471425527005600205450ustar00rootroot00000000000000/* * GStreamer * Copyright (C) 2015 Matthew Waters * Copyright (C) 2015 Thibault Saunier * Copyright (C) 2022 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 "gstgtkutils.h" #define _IS_FRAME_PREMULTIPLIED(f) (GST_VIDEO_INFO_FLAG_IS_SET (&(f)->info, GST_VIDEO_FLAG_PREMULTIPLIED_ALPHA)) 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; } static GdkMemoryFormat gst_gdk_memory_format_from_frame (GstVideoFrame *frame) { switch (GST_VIDEO_FRAME_FORMAT (frame)) { case GST_VIDEO_FORMAT_RGBA64_LE: case GST_VIDEO_FORMAT_RGBA64_BE: return (_IS_FRAME_PREMULTIPLIED (frame)) ? GDK_MEMORY_R16G16B16A16_PREMULTIPLIED : GDK_MEMORY_R16G16B16A16; case GST_VIDEO_FORMAT_RGBA: return (_IS_FRAME_PREMULTIPLIED (frame)) ? GDK_MEMORY_R8G8B8A8_PREMULTIPLIED : GDK_MEMORY_R8G8B8A8; case GST_VIDEO_FORMAT_BGRA: return (_IS_FRAME_PREMULTIPLIED (frame)) ? GDK_MEMORY_B8G8R8A8_PREMULTIPLIED : GDK_MEMORY_B8G8R8A8; case GST_VIDEO_FORMAT_ARGB: return (_IS_FRAME_PREMULTIPLIED (frame)) ? GDK_MEMORY_A8R8G8B8_PREMULTIPLIED : GDK_MEMORY_A8R8G8B8; case GST_VIDEO_FORMAT_ABGR: /* GTK is missing premultiplied ABGR support */ return GDK_MEMORY_A8B8G8R8; case GST_VIDEO_FORMAT_RGBx: return GDK_MEMORY_R8G8B8A8_PREMULTIPLIED; case GST_VIDEO_FORMAT_BGRx: return GDK_MEMORY_B8G8R8A8_PREMULTIPLIED; case GST_VIDEO_FORMAT_RGB: return GDK_MEMORY_R8G8B8; case GST_VIDEO_FORMAT_BGR: return GDK_MEMORY_B8G8R8; default: break; } /* This should never happen as long as above switch statement * is updated when new formats are added to caps */ g_assert_not_reached (); /* Fallback format */ return GDK_MEMORY_R8G8B8A8_PREMULTIPLIED; } GdkTexture * gst_video_frame_into_gdk_texture (GstVideoFrame *frame) { GdkTexture *texture; GBytes *bytes; bytes = g_bytes_new_with_free_func ( GST_VIDEO_FRAME_PLANE_DATA (frame, 0), GST_VIDEO_FRAME_HEIGHT (frame) * GST_VIDEO_FRAME_PLANE_STRIDE (frame, 0), (GDestroyNotify) gst_buffer_unref, gst_buffer_ref (frame->buffer)); texture = gdk_memory_texture_new ( GST_VIDEO_FRAME_WIDTH (frame), GST_VIDEO_FRAME_HEIGHT (frame), gst_gdk_memory_format_from_frame (frame), bytes, GST_VIDEO_FRAME_PLANE_STRIDE (frame, 0)); g_bytes_unref (bytes); return texture; } clapper-0.5.2/lib/gst/plugin/gstgtkutils.h000066400000000000000000000023221425527005600205470ustar00rootroot00000000000000/* * GStreamer * Copyright (C) 2015 Matthew Waters * Copyright (C) 2015 Thibault Saunier * Copyright (C) 2022 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. */ #pragma once #include #include #include G_BEGIN_DECLS gpointer gst_gtk_invoke_on_main (GThreadFunc func, gpointer data); GdkTexture * gst_video_frame_into_gdk_texture (GstVideoFrame *frame); G_END_DECLS clapper-0.5.2/lib/gst/plugin/gstplugin.c000066400000000000000000000025261425527005600202000ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclappersink.h" static gboolean plugin_init (GstPlugin *plugin) { if (!g_module_supported ()) return FALSE; gst_plugin_add_dependency_simple (plugin, NULL, CLAPPER_SINK_IMPORTER_PATH, NULL, GST_PLUGIN_DEPENDENCY_FLAG_NONE); return GST_ELEMENT_REGISTER (clappersink, plugin); } GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, clapper, "Clapper elements", plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) clapper-0.5.2/lib/gst/plugin/importers/000077500000000000000000000000001425527005600200375ustar00rootroot00000000000000clapper-0.5.2/lib/gst/plugin/importers/gstclapperglbaseimporter.c000066400000000000000000000522411425527005600253130ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclapperglbaseimporter.h" #include "gst/plugin/gstgdkformats.h" #include "gst/plugin/gstgtkutils.h" #include #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND #include #include #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11 #include #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_GLX #include #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL || GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_EGL #include #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32 #include #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_MACOS #include #endif #define GST_CAT_DEFAULT gst_clapper_gl_base_importer_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); #define parent_class gst_clapper_gl_base_importer_parent_class G_DEFINE_TYPE (GstClapperGLBaseImporter, gst_clapper_gl_base_importer, GST_TYPE_CLAPPER_IMPORTER); static GstGLContext * wrap_current_gl (GstGLDisplay *display, GdkGLAPI gdk_gl_api, GstGLPlatform platform) { GstGLAPI gst_gl_api = GST_GL_API_NONE; switch (gdk_gl_api) { case GDK_GL_API_GL: gst_gl_api = GST_GL_API_OPENGL | GST_GL_API_OPENGL3; break; case GDK_GL_API_GLES: gst_gl_api = GST_GL_API_GLES2; break; default: g_assert_not_reached (); break; } if (gst_gl_api != GST_GL_API_NONE) { guintptr gl_handle; gst_gl_display_filter_gl_api (display, gst_gl_api); if ((gl_handle = gst_gl_context_get_current_gl_context (platform))) return gst_gl_context_new_wrapped (display, gl_handle, platform, gst_gl_api); } return NULL; } static gboolean retrieve_gl_context_on_main (GstClapperGLBaseImporter *self) { GstClapperGLBaseImporterClass *gl_bi_class = GST_CLAPPER_GL_BASE_IMPORTER_GET_CLASS (self); GdkDisplay *gdk_display; GdkGLContext *gdk_context; GError *error = NULL; GdkGLAPI gdk_gl_api; GstGLPlatform platform = GST_GL_PLATFORM_NONE; gint gl_major = 0, gl_minor = 0; if (!gtk_init_check ()) { GST_ERROR_OBJECT (self, "Could not ensure GTK initialization"); return FALSE; } /* Make sure we are clean here, otherwise data sharing * between GL-based importers may lead to leaks */ gst_clear_object (&self->wrapped_context); g_clear_object (&self->gdk_context); gst_clear_object (&self->gst_display); gdk_display = gdk_display_get_default (); if (G_UNLIKELY (!gdk_display)) { GST_ERROR_OBJECT (self, "Could not retrieve Gdk display"); return FALSE; } if (!(gdk_context = gdk_display_create_gl_context (gdk_display, &error))) { GST_ERROR_OBJECT (self, "Error creating Gdk GL context: %s", error ? error->message : "No error set by Gdk"); g_clear_error (&error); return FALSE; } if (!gl_bi_class->gdk_context_realize (self, gdk_context)) { GST_ERROR_OBJECT (self, "Could not realize Gdk context: %" GST_PTR_FORMAT, gdk_context); g_object_unref (gdk_context); return FALSE; } gdk_gl_api = gdk_gl_context_get_api (gdk_context); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); self->gdk_context = gdk_context; #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND if (GDK_IS_WAYLAND_DISPLAY (gdk_display)) { struct wl_display *wayland_display = gdk_wayland_display_get_wl_display (gdk_display); self->gst_display = (GstGLDisplay *) gst_gl_display_wayland_new_with_display (wayland_display); } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11 if (GDK_IS_X11_DISPLAY (gdk_display)) { gpointer display_ptr; #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL display_ptr = gdk_x11_display_get_egl_display (gdk_display); if (display_ptr) { self->gst_display = (GstGLDisplay *) gst_gl_display_egl_new_with_egl_display (display_ptr); } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_GLX if (!self->gst_display) { display_ptr = gdk_x11_display_get_xdisplay (gdk_display); self->gst_display = (GstGLDisplay *) gst_gl_display_x11_new_with_display (display_ptr); } } #endif #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32 if (GDK_IS_WIN32_DISPLAY (gdk_display)) { #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_EGL gpointer display_ptr = gdk_win32_display_get_egl_display (gdk_display); if (display_ptr) { self->gst_display = (GstGLDisplay *) gst_gl_display_egl_new_with_egl_display (display_ptr); } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_WGL if (!self->gst_display) { self->gst_display = gst_gl_display_new_with_type (GST_GL_DISPLAY_TYPE_WIN32); } } #endif #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_MACOS if (GDK_IS_MACOS_DISPLAY (gdk_display)) { self->gst_display = gst_gl_display_new_with_type (GST_GL_DISPLAY_TYPE_COCOA); } #endif /* Fallback to generic display */ if (G_UNLIKELY (!self->gst_display)) { GST_WARNING_OBJECT (self, "Unknown Gdk display!"); self->gst_display = gst_gl_display_new (); } #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND if (GST_IS_GL_DISPLAY_WAYLAND (self->gst_display)) { platform = GST_GL_PLATFORM_EGL; GST_INFO_OBJECT (self, "Using EGL on Wayland"); goto have_display; } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL if (GST_IS_GL_DISPLAY_EGL (self->gst_display) && GDK_IS_X11_DISPLAY (gdk_display)) { platform = GST_GL_PLATFORM_EGL; GST_INFO_OBJECT (self, "Using EGL on x11"); goto have_display; } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_GLX if (GST_IS_GL_DISPLAY_X11 (self->gst_display)) { platform = GST_GL_PLATFORM_GLX; GST_INFO_OBJECT (self, "Using GLX on x11"); goto have_display; } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_EGL if (GST_IS_GL_DISPLAY_EGL (self->gst_display) && GDK_IS_WIN32_DISPLAY (gdk_display)) { platform = GST_GL_PLATFORM_EGL; GST_INFO_OBJECT (self, "Using EGL on Win32"); goto have_display; } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_WGL if (gst_gl_display_get_handle_type (self->gst_display) == GST_GL_DISPLAY_TYPE_WIN32) { platform = GST_GL_PLATFORM_WGL; GST_INFO_OBJECT (self, "Using WGL on Win32"); goto have_display; } #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_MACOS if (gst_gl_display_get_handle_type (self->gst_display) == GST_GL_DISPLAY_TYPE_COCOA) { platform = GST_GL_PLATFORM_CGL; GST_INFO_OBJECT (self, "Using CGL on macOS"); goto have_display; } #endif g_clear_object (&self->gdk_context); gst_clear_object (&self->gst_display); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); GST_ERROR_OBJECT (self, "Unsupported GL platform"); return FALSE; have_display: gdk_gl_context_make_current (self->gdk_context); self->wrapped_context = wrap_current_gl (self->gst_display, gdk_gl_api, platform); if (!self->wrapped_context) { GST_ERROR ("Could not retrieve Gdk OpenGL context"); gdk_gl_context_clear_current (); g_clear_object (&self->gdk_context); gst_clear_object (&self->gst_display); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); return FALSE; } GST_INFO ("Retrieved Gdk OpenGL context %" GST_PTR_FORMAT, self->wrapped_context); gst_gl_context_activate (self->wrapped_context, TRUE); if (!gst_gl_context_fill_info (self->wrapped_context, &error)) { GST_ERROR ("Failed to fill Gdk context info: %s", error->message); g_clear_error (&error); gst_gl_context_activate (self->wrapped_context, FALSE); gst_clear_object (&self->wrapped_context); g_clear_object (&self->gdk_context); gst_clear_object (&self->gst_display); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); return FALSE; } gst_gl_context_get_gl_version (self->wrapped_context, &gl_major, &gl_minor); GST_INFO ("Using OpenGL%s %i.%i", (gdk_gl_api == GDK_GL_API_GLES) ? " ES" : "", gl_major, gl_minor); /* Deactivate in both places */ gst_gl_context_activate (self->wrapped_context, FALSE); gdk_gl_context_clear_current (); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); return TRUE; } static gboolean retrieve_gst_context (GstClapperGLBaseImporter *self) { GstGLDisplay *gst_display = NULL; GstGLContext *gst_context = NULL; GError *error = NULL; GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); gst_display = gst_object_ref (self->gst_display); /* GstGLDisplay operations require display object lock to be held */ GST_OBJECT_LOCK (gst_display); if (!self->gst_context) { GST_TRACE_OBJECT (self, "Creating new GstGLContext"); if (!gst_gl_display_create_context (gst_display, self->wrapped_context, &self->gst_context, &error)) { GST_WARNING ("Could not create OpenGL context: %s", error ? error->message : "Unknown"); g_clear_error (&error); GST_OBJECT_UNLOCK (gst_display); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); return FALSE; } } gst_context = gst_object_ref (self->gst_context); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); gst_gl_display_add_context (gst_display, gst_context); GST_OBJECT_UNLOCK (gst_display); gst_object_unref (gst_display); gst_object_unref (gst_context); return TRUE; } static gboolean gst_clapper_gl_base_importer_prepare (GstClapperImporter *importer) { GstClapperGLBaseImporter *self = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); gboolean need_invoke; GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); need_invoke = (!self->gdk_context || !self->gst_display || !self->wrapped_context); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); if (need_invoke) { if (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) retrieve_gl_context_on_main, self))) return FALSE; } if (!retrieve_gst_context (self)) return FALSE; if (!GST_CLAPPER_IMPORTER_CLASS (parent_class)->prepare) return TRUE; return GST_CLAPPER_IMPORTER_CLASS (parent_class)->prepare (importer); } static void gst_clapper_gl_base_importer_share_data (GstClapperImporter *importer, GstClapperImporter *dest_importer) { GstClapperGLBaseImporter *self = GST_CLAPPER_GL_BASE_IMPORTER (importer); if (GST_IS_CLAPPER_GL_BASE_IMPORTER (dest_importer)) { GstClapperGLBaseImporter *dest = GST_CLAPPER_GL_BASE_IMPORTER (dest_importer); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (dest); /* Successfully prepared GL importer should have all three */ if (self->gdk_context && self->gst_display && self->wrapped_context) { g_clear_object (&dest->gdk_context); dest->gdk_context = g_object_ref (self->gdk_context); gst_clear_object (&dest->gst_display); dest->gst_display = gst_object_ref (self->gst_display); gst_clear_object (&dest->wrapped_context); dest->wrapped_context = gst_object_ref (self->wrapped_context); } /* This context is not required, we can create it ourselves * using gst_display and wrapped_context */ if (self->gst_context) { gst_clear_object (&dest->gst_context); dest->gst_context = gst_object_ref (self->gst_context); } GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (dest); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); } if (GST_CLAPPER_IMPORTER_CLASS (parent_class)->share_data) GST_CLAPPER_IMPORTER_CLASS (parent_class)->share_data (importer, dest_importer); } static gboolean gst_clapper_gl_base_importer_handle_context_query (GstClapperImporter *importer, GstBaseSink *bsink, GstQuery *query) { GstClapperGLBaseImporter *self = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); gboolean res; GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); res = gst_gl_handle_context_query (GST_ELEMENT_CAST (bsink), query, self->gst_display, self->gst_context, self->wrapped_context); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); return res; } static GstBufferPool * gst_clapper_gl_base_importer_create_pool (GstClapperImporter *importer, GstStructure **config) { GstClapperGLBaseImporter *self = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); GstBufferPool *pool; GST_DEBUG_OBJECT (self, "Creating new GL buffer pool"); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); pool = gst_gl_buffer_pool_new (self->gst_context); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); *config = gst_buffer_pool_get_config (pool); gst_buffer_pool_config_add_option (*config, GST_BUFFER_POOL_OPTION_VIDEO_META); gst_buffer_pool_config_add_option (*config, GST_BUFFER_POOL_OPTION_GL_SYNC_META); return pool; } static void gst_clapper_gl_base_importer_add_allocation_metas (GstClapperImporter *importer, GstQuery *query) { GstClapperGLBaseImporter *self = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); gst_query_add_allocation_meta (query, GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, NULL); gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); if (self->gst_context->gl_vtable->FenceSync) gst_query_add_allocation_meta (query, GST_GL_SYNC_META_API_TYPE, NULL); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); } static gboolean _realize_gdk_context_with_api (GdkGLContext *gdk_context, GdkGLAPI api, gint maj, gint min) { GError *error = NULL; gboolean success; gdk_gl_context_set_allowed_apis (gdk_context, api); gdk_gl_context_set_required_version (gdk_context, maj, min); GST_DEBUG ("Trying to realize %s context, min ver: %i.%i", (api & GDK_GL_API_GL) ? "GL" : "GLES", maj, min); if (!(success = gdk_gl_context_realize (gdk_context, &error))) { GST_DEBUG ("Could not realize Gdk context with %s: %s", (api & GDK_GL_API_GL) ? "GL" : "GLES", error->message); g_clear_error (&error); } return success; } static gboolean gst_clapper_gl_base_importer_gdk_context_realize (GstClapperGLBaseImporter *self, GdkGLContext *gdk_context) { GdkGLAPI preferred_api = GDK_GL_API_GL; GdkDisplay *gdk_display; const gchar *gl_env; gboolean success; GST_DEBUG_OBJECT (self, "Realizing GdkGLContext with default implementation"); /* Use single "GST_GL_API" env to also influence Gdk GL selection */ if ((gl_env = g_getenv ("GST_GL_API"))) { preferred_api = (g_str_has_prefix (gl_env, "gles")) ? GDK_GL_API_GLES : g_str_has_prefix (gl_env, "opengl") ? GDK_GL_API_GL : GDK_GL_API_GL | GDK_GL_API_GLES; /* With requested by user API, we either use it or give up */ return _realize_gdk_context_with_api (gdk_context, preferred_api, 0, 0); } gdk_display = gdk_gl_context_get_display (gdk_context); GST_DEBUG_OBJECT (self, "Auto selecting GL API for display: %s", gdk_display_get_name (gdk_display)); /* Apple decoder uses rectangle texture-target, which GLES does not support. * For Linux we prefer EGL + GLES in order to get direct HW colorspace conversion. * Windows will try EGL + GLES setup first and auto fallback to WGL. */ #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND if (GDK_IS_WAYLAND_DISPLAY (gdk_display)) preferred_api = GDK_GL_API_GLES; #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL if (GDK_IS_X11_DISPLAY (gdk_display) && gdk_x11_display_get_egl_display (gdk_display)) preferred_api = GDK_GL_API_GLES; #endif #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_EGL if (GDK_IS_WIN32_DISPLAY (gdk_display) && gdk_win32_display_get_egl_display (gdk_display)) preferred_api = GDK_GL_API_GLES; #endif /* FIXME: Remove once GStreamer can handle DRM modifiers. This tries to avoid * "scrambled" image on Linux with Intel GPUs that are mostly used together with * x86 CPUs at the expense of using slightly slower non-direct DMABuf import. * See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1236 */ #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND || GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL #if !defined(HAVE_GST_PATCHES) && (defined(__i386__) || defined(_M_IX86) || defined(__x86_64__) || defined(_M_X64)) preferred_api = GDK_GL_API_GL; #endif #endif /* Continue with GLES only if it should have "GL_EXT_texture_norm16" * extension, as we need it to handle P010_10LE, etc. */ if ((preferred_api == GDK_GL_API_GLES) && _realize_gdk_context_with_api (gdk_context, GDK_GL_API_GLES, 3, 1)) return TRUE; /* If not using GLES 3.1, try with core GL 3.2 that GTK4 defaults to */ if (_realize_gdk_context_with_api (gdk_context, GDK_GL_API_GL, 3, 2)) return TRUE; /* Try with what we normally prefer first, otherwise use fallback */ if (!(success = _realize_gdk_context_with_api (gdk_context, preferred_api, 0, 0))) { GdkGLAPI fallback_api; fallback_api = (GDK_GL_API_GL | GDK_GL_API_GLES); fallback_api &= ~preferred_api; success = _realize_gdk_context_with_api (gdk_context, fallback_api, 0, 0); } return success; } static void gst_clapper_gl_base_importer_init (GstClapperGLBaseImporter *self) { g_mutex_init (&self->lock); } static void gst_clapper_gl_base_importer_finalize (GObject *object) { GstClapperGLBaseImporter *self = GST_CLAPPER_GL_BASE_IMPORTER_CAST (object); g_clear_object (&self->gdk_context); gst_clear_object (&self->gst_display); gst_clear_object (&self->wrapped_context); gst_clear_object (&self->gst_context); g_mutex_clear (&self->lock); GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); } static void gst_clapper_gl_base_importer_class_init (GstClapperGLBaseImporterClass *klass) { GObjectClass *gobject_class = (GObjectClass *) klass; GstClapperImporterClass *importer_class = (GstClapperImporterClass *) klass; GstClapperGLBaseImporterClass *gl_bi_class = (GstClapperGLBaseImporterClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperglbaseimporter", 0, "Clapper GL Base Importer"); gobject_class->finalize = gst_clapper_gl_base_importer_finalize; importer_class->prepare = gst_clapper_gl_base_importer_prepare; importer_class->share_data = gst_clapper_gl_base_importer_share_data; importer_class->handle_context_query = gst_clapper_gl_base_importer_handle_context_query; importer_class->create_pool = gst_clapper_gl_base_importer_create_pool; importer_class->add_allocation_metas = gst_clapper_gl_base_importer_add_allocation_metas; gl_bi_class->gdk_context_realize = gst_clapper_gl_base_importer_gdk_context_realize; } GstCaps * gst_clapper_gl_base_importer_make_supported_gdk_gl_caps (void) { GstCaps *caps, *tmp; tmp = gst_caps_from_string ( GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_GL_MEMORY, "{ " GST_GDK_GL_TEXTURE_FORMATS " }") ", " "texture-target = (string) { " GST_GL_TEXTURE_TARGET_2D_STR " }"); caps = gst_caps_copy (tmp); gst_caps_set_features_simple (caps, gst_caps_features_new ( GST_CAPS_FEATURE_MEMORY_GL_MEMORY, GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, NULL)); gst_caps_append (caps, tmp); return caps; } GStrv gst_clapper_gl_base_importer_make_gl_context_types (void) { GStrv context_types; GStrvBuilder *builder = g_strv_builder_new (); g_strv_builder_add (builder, GST_GL_DISPLAY_CONTEXT_TYPE); g_strv_builder_add (builder, "gst.gl.app_context"); g_strv_builder_add (builder, "gst.gl.local_context"); context_types = g_strv_builder_end (builder); g_strv_builder_unref (builder); return context_types; } GdkTexture * gst_clapper_gl_base_importer_make_gl_texture (GstClapperGLBaseImporter *self, GstBuffer *buffer, GstVideoInfo *v_info) { GdkTexture *texture; GstGLSyncMeta *sync_meta; GstVideoFrame frame; if (G_UNLIKELY (!gst_video_frame_map (&frame, v_info, buffer, GST_MAP_READ | GST_MAP_GL))) { GST_ERROR_OBJECT (self, "Could not map input buffer for reading"); return NULL; } GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); /* Must have context active here for both sync meta * and Gdk texture format auto-detection to work */ gdk_gl_context_make_current (self->gdk_context); gst_gl_context_activate (self->wrapped_context, TRUE); sync_meta = gst_buffer_get_gl_sync_meta (buffer); /* Wait for all previous OpenGL commands to complete, * before we start using the input texture */ if (sync_meta) { gst_gl_sync_meta_set_sync_point (sync_meta, self->gst_context); gst_gl_sync_meta_wait (sync_meta, self->wrapped_context); } texture = gdk_gl_texture_new ( self->gdk_context, *(guint *) GST_VIDEO_FRAME_PLANE_DATA (&frame, 0), GST_VIDEO_FRAME_WIDTH (&frame), GST_VIDEO_FRAME_HEIGHT (&frame), (GDestroyNotify) gst_buffer_unref, gst_buffer_ref (buffer)); gst_gl_context_activate (self->wrapped_context, FALSE); gdk_gl_context_clear_current (); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); gst_video_frame_unmap (&frame); return texture; } clapper-0.5.2/lib/gst/plugin/importers/gstclapperglbaseimporter.h000066400000000000000000000102331425527005600253130ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include #include "gst/plugin/gstclapperimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_GL_BASE_IMPORTER (gst_clapper_gl_base_importer_get_type()) #define GST_IS_CLAPPER_GL_BASE_IMPORTER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_GL_BASE_IMPORTER)) #define GST_IS_CLAPPER_GL_BASE_IMPORTER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_GL_BASE_IMPORTER)) #define GST_CLAPPER_GL_BASE_IMPORTER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_GL_BASE_IMPORTER, GstClapperGLBaseImporterClass)) #define GST_CLAPPER_GL_BASE_IMPORTER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_GL_BASE_IMPORTER, GstClapperGLBaseImporter)) #define GST_CLAPPER_GL_BASE_IMPORTER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_GL_BASE_IMPORTER, GstClapperGLBaseImporterClass)) #define GST_CLAPPER_GL_BASE_IMPORTER_CAST(obj) ((GstClapperGLBaseImporter *)(obj)) #define GST_CLAPPER_GL_BASE_IMPORTER_GET_LOCK(obj) (&GST_CLAPPER_GL_BASE_IMPORTER_CAST(obj)->lock) #define GST_CLAPPER_GL_BASE_IMPORTER_LOCK(obj) g_mutex_lock (GST_CLAPPER_GL_BASE_IMPORTER_GET_LOCK(obj)) #define GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK(obj) g_mutex_unlock (GST_CLAPPER_GL_BASE_IMPORTER_GET_LOCK(obj)) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND (GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11 (GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_GLX (GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11 && GST_GL_HAVE_PLATFORM_GLX) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL (GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11 && GST_GL_HAVE_PLATFORM_EGL) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32 (GST_GL_HAVE_WINDOW_WIN32 && defined (GDK_WINDOWING_WIN32)) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_WGL (GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32 && GST_GL_HAVE_PLATFORM_WGL) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_EGL (GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32 && GST_GL_HAVE_PLATFORM_EGL) #define GST_CLAPPER_GL_BASE_IMPORTER_HAVE_MACOS (GST_GL_HAVE_WINDOW_COCOA && defined (GDK_WINDOWING_MACOS) && GST_GL_HAVE_PLATFORM_CGL) typedef struct _GstClapperGLBaseImporter GstClapperGLBaseImporter; typedef struct _GstClapperGLBaseImporterClass GstClapperGLBaseImporterClass; #ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstClapperGLBaseImporter, gst_object_unref) #endif struct _GstClapperGLBaseImporter { GstClapperImporter parent; GMutex lock; GdkGLContext *gdk_context; GstGLDisplay *gst_display; GstGLContext *wrapped_context; GstGLContext *gst_context; }; struct _GstClapperGLBaseImporterClass { GstClapperImporterClass parent_class; gboolean (* gdk_context_realize) (GstClapperGLBaseImporter *gl_bi, GdkGLContext *gdk_context); }; GType gst_clapper_gl_base_importer_get_type (void); GstCaps * gst_clapper_gl_base_importer_make_supported_gdk_gl_caps (void); GStrv gst_clapper_gl_base_importer_make_gl_context_types (void); GdkTexture * gst_clapper_gl_base_importer_make_gl_texture (GstClapperGLBaseImporter *self, GstBuffer *buffer, GstVideoInfo *v_info); G_END_DECLS clapper-0.5.2/lib/gst/plugin/importers/gstclapperglimporter.c000066400000000000000000000043261425527005600244610ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclapperglimporter.h" #define GST_CAT_DEFAULT gst_clapper_gl_importer_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); #define parent_class gst_clapper_gl_importer_parent_class GST_CLAPPER_IMPORTER_DEFINE (GstClapperGLImporter, gst_clapper_gl_importer, GST_TYPE_CLAPPER_GL_BASE_IMPORTER); static GdkTexture * gst_clapper_gl_importer_generate_texture (GstClapperImporter *importer, GstBuffer *buffer, GstVideoInfo *v_info) { GstClapperGLBaseImporter *gl_bi = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); return gst_clapper_gl_base_importer_make_gl_texture (gl_bi, buffer, v_info); } static void gst_clapper_gl_importer_init (GstClapperGLImporter *self) { } static void gst_clapper_gl_importer_class_init (GstClapperGLImporterClass *klass) { GstClapperImporterClass *importer_class = (GstClapperImporterClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperglimporter", 0, "Clapper GL Importer"); importer_class->generate_texture = gst_clapper_gl_importer_generate_texture; } GstClapperImporter * make_importer (void) { return g_object_new (GST_TYPE_CLAPPER_GL_IMPORTER, NULL); } GstCaps * make_caps (gboolean is_template, GstRank *rank, GStrv *context_types) { *rank = GST_RANK_SECONDARY; *context_types = gst_clapper_gl_base_importer_make_gl_context_types (); return gst_clapper_gl_base_importer_make_supported_gdk_gl_caps (); } clapper-0.5.2/lib/gst/plugin/importers/gstclapperglimporter.h000066400000000000000000000023361425527005600244650ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include "gstclapperglbaseimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_GL_IMPORTER (gst_clapper_gl_importer_get_type()) G_DECLARE_FINAL_TYPE (GstClapperGLImporter, gst_clapper_gl_importer, GST, CLAPPER_GL_IMPORTER, GstClapperGLBaseImporter) #define GST_CLAPPER_GL_IMPORTER_CAST(obj) ((GstClapperGLImporter *)(obj)) struct _GstClapperGLImporter { GstClapperGLBaseImporter parent; }; G_END_DECLS clapper-0.5.2/lib/gst/plugin/importers/gstclappergluploader.c000066400000000000000000000230361425527005600244320ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclappergluploader.h" #include "gst/plugin/gstgtkutils.h" #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11 #include #endif #define GST_CAT_DEFAULT gst_clapper_gl_uploader_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); #define parent_class gst_clapper_gl_uploader_parent_class GST_CLAPPER_IMPORTER_DEFINE (GstClapperGLUploader, gst_clapper_gl_uploader, GST_TYPE_CLAPPER_GL_BASE_IMPORTER); static void _update_elements_caps_locked (GstClapperGLUploader *self, GstCaps *upload_sink_caps) { GstClapperGLBaseImporter *gl_bi = GST_CLAPPER_GL_BASE_IMPORTER_CAST (self); GstCaps *upload_src_caps, *color_sink_caps, *color_src_caps, *gdk_sink_caps; GST_INFO_OBJECT (self, "Input caps: %" GST_PTR_FORMAT, upload_sink_caps); upload_src_caps = gst_gl_upload_transform_caps (self->upload, gl_bi->gst_context, GST_PAD_SINK, upload_sink_caps, NULL); upload_src_caps = gst_caps_fixate (upload_src_caps); GST_INFO_OBJECT (self, "GLUpload caps: %" GST_PTR_FORMAT, upload_src_caps); gst_gl_upload_set_caps (self->upload, upload_sink_caps, upload_src_caps); gdk_sink_caps = gst_clapper_gl_base_importer_make_supported_gdk_gl_caps (); color_sink_caps = gst_gl_color_convert_transform_caps (gl_bi->gst_context, GST_PAD_SRC, upload_src_caps, gdk_sink_caps); gst_caps_unref (gdk_sink_caps); /* Second caps arg is transfer-full */ color_src_caps = gst_gl_color_convert_fixate_caps (gl_bi->gst_context, GST_PAD_SINK, upload_src_caps, color_sink_caps); GST_INFO_OBJECT (self, "GLColorConvert caps: %" GST_PTR_FORMAT, color_src_caps); gst_gl_color_convert_set_caps (self->color_convert, upload_src_caps, color_src_caps); self->has_pending_v_info = gst_video_info_from_caps (&self->pending_v_info, color_src_caps); gst_caps_unref (upload_src_caps); gst_caps_unref (color_src_caps); } static void gst_clapper_gl_uploader_set_caps (GstClapperImporter *importer, GstCaps *caps) { GstClapperGLUploader *self = GST_CLAPPER_GL_UPLOADER_CAST (importer); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); _update_elements_caps_locked (self, caps); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); } static void _uploader_reconfigure_locked (GstClapperGLUploader *self) { GstCaps *in_caps = NULL; GST_DEBUG_OBJECT (self, "Reconfiguring upload"); gst_gl_upload_get_caps (self->upload, &in_caps, NULL); if (G_LIKELY (in_caps)) { _update_elements_caps_locked (self, in_caps); gst_caps_unref (in_caps); } } static gboolean gst_clapper_gl_uploader_prepare (GstClapperImporter *importer) { gboolean res = GST_CLAPPER_IMPORTER_CLASS (parent_class)->prepare (importer); if (res) { GstClapperGLUploader *self = GST_CLAPPER_GL_UPLOADER_CAST (importer); GstClapperGLBaseImporter *gl_bi = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); if (!self->upload) self->upload = gst_gl_upload_new (gl_bi->gst_context); if (!self->color_convert) self->color_convert = gst_gl_color_convert_new (gl_bi->gst_context); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); } return res; } static GstBuffer * _upload_perform_locked (GstClapperGLUploader *self, GstBuffer *buffer) { GstBuffer *upload_buf = NULL; GstGLUploadReturn ret; ret = gst_gl_upload_perform_with_buffer (self->upload, buffer, &upload_buf); if (G_UNLIKELY (ret != GST_GL_UPLOAD_DONE)) { switch (ret) { case GST_GL_UPLOAD_RECONFIGURE: _uploader_reconfigure_locked (self); /* Retry with the same buffer after reconfiguring */ return _upload_perform_locked (self, buffer); default: GST_ERROR_OBJECT (self, "Could not upload input buffer, returned: %i", ret); break; } } return upload_buf; } static GstBufferPool * gst_clapper_gl_uploader_create_pool (GstClapperImporter *importer, GstStructure **config) { /* Since GLUpload API provides a ready to use propose_allocation method, * we will use it with our query in add_allocation_metas instead of * making pool here ourselves */ return NULL; } static void gst_clapper_gl_uploader_add_allocation_metas (GstClapperImporter *importer, GstQuery *query) { GstClapperGLUploader *self = GST_CLAPPER_GL_UPLOADER_CAST (importer); GstGLUpload *upload; GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); upload = gst_object_ref (self->upload); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); gst_gl_upload_propose_allocation (upload, NULL, query); gst_object_unref (upload); GST_CLAPPER_IMPORTER_CLASS (parent_class)->add_allocation_metas (importer, query); } static GdkTexture * gst_clapper_gl_uploader_generate_texture (GstClapperImporter *importer, GstBuffer *buffer, GstVideoInfo *v_info) { GstClapperGLUploader *self = GST_CLAPPER_GL_UPLOADER_CAST (importer); GstClapperGLBaseImporter *gl_bi = GST_CLAPPER_GL_BASE_IMPORTER_CAST (importer); GstBuffer *upload_buf, *color_buf; GdkTexture *texture; /* XXX: We both upload and perform color conversion here, thus we skip * upload for buffers that are not going to be shown and gain more free * CPU time to prepare the next one. Improves performance on weak HW. */ GST_LOG_OBJECT (self, "Uploading %" GST_PTR_FORMAT, buffer); GST_CLAPPER_GL_BASE_IMPORTER_LOCK (self); upload_buf = _upload_perform_locked (self, buffer); if (G_UNLIKELY (!upload_buf)) { GST_ERROR_OBJECT (self, "Could not perform upload on input buffer"); GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); return NULL; } GST_LOG_OBJECT (self, "Uploaded into %" GST_PTR_FORMAT, upload_buf); color_buf = gst_gl_color_convert_perform (self->color_convert, upload_buf); gst_buffer_unref (upload_buf); /* Use video info associated with converted buffer */ if (self->has_pending_v_info) { self->v_info = self->pending_v_info; self->has_pending_v_info = FALSE; } GST_CLAPPER_GL_BASE_IMPORTER_UNLOCK (self); if (G_UNLIKELY (!color_buf)) { GST_ERROR_OBJECT (self, "Could not perform color conversion on input buffer"); return NULL; } GST_LOG_OBJECT (self, "Color converted into %" GST_PTR_FORMAT, color_buf); texture = gst_clapper_gl_base_importer_make_gl_texture (gl_bi, color_buf, &self->v_info); gst_buffer_unref (color_buf); return texture; } static void gst_clapper_gl_uploader_init (GstClapperGLUploader *self) { gst_video_info_init (&self->pending_v_info); gst_video_info_init (&self->v_info); } static void gst_clapper_gl_uploader_finalize (GObject *object) { GstClapperGLUploader *self = GST_CLAPPER_GL_UPLOADER_CAST (object); gst_clear_object (&self->upload); gst_clear_object (&self->color_convert); GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object)); } static void gst_clapper_gl_uploader_class_init (GstClapperGLUploaderClass *klass) { GObjectClass *gobject_class = (GObjectClass *) klass; GstClapperImporterClass *importer_class = (GstClapperImporterClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappergluploader", 0, "Clapper GL Uploader"); gobject_class->finalize = gst_clapper_gl_uploader_finalize; importer_class->prepare = gst_clapper_gl_uploader_prepare; importer_class->set_caps = gst_clapper_gl_uploader_set_caps; importer_class->create_pool = gst_clapper_gl_uploader_create_pool; importer_class->add_allocation_metas = gst_clapper_gl_uploader_add_allocation_metas; importer_class->generate_texture = gst_clapper_gl_uploader_generate_texture; } GstClapperImporter * make_importer (void) { return g_object_new (GST_TYPE_CLAPPER_GL_UPLOADER, NULL); } #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_GLX static gboolean _filter_glx_caps_cb (GstCapsFeatures *features, GstStructure *structure, gpointer user_data) { return !gst_caps_features_contains (features, "memory:DMABuf"); } static gboolean _update_glx_caps_on_main (GstCaps *caps) { GdkDisplay *gdk_display; if (!gtk_init_check ()) return FALSE; gdk_display = gdk_display_get_default (); if (G_UNLIKELY (!gdk_display)) return FALSE; if (GDK_IS_X11_DISPLAY (gdk_display)) { gboolean using_glx = TRUE; #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL using_glx = (gdk_x11_display_get_egl_display (gdk_display) == NULL); #endif if (using_glx) { gst_caps_filter_and_map_in_place (caps, (GstCapsFilterMapFunc) _filter_glx_caps_cb, NULL); } } return TRUE; } #endif GstCaps * make_caps (gboolean is_template, GstRank *rank, GStrv *context_types) { GstCaps *caps = gst_gl_upload_get_input_template_caps (); #if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_GLX if (!is_template && !(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) _update_glx_caps_on_main, caps))) gst_clear_caps (&caps); #endif if (G_UNLIKELY (!caps)) return NULL; *rank = GST_RANK_MARGINAL + 1; *context_types = gst_clapper_gl_base_importer_make_gl_context_types (); return caps; } clapper-0.5.2/lib/gst/plugin/importers/gstclappergluploader.h000066400000000000000000000025411425527005600244350ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include "gstclapperglbaseimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_GL_UPLOADER (gst_clapper_gl_uploader_get_type()) G_DECLARE_FINAL_TYPE (GstClapperGLUploader, gst_clapper_gl_uploader, GST, CLAPPER_GL_UPLOADER, GstClapperGLBaseImporter) #define GST_CLAPPER_GL_UPLOADER_CAST(obj) ((GstClapperGLUploader *)(obj)) struct _GstClapperGLUploader { GstClapperGLBaseImporter parent; GstGLUpload *upload; GstGLColorConvert *color_convert; GstVideoInfo pending_v_info, v_info; gboolean has_pending_v_info; }; G_END_DECLS clapper-0.5.2/lib/gst/plugin/importers/gstclapperrawimporter.c000066400000000000000000000067611425527005600246550ustar00rootroot00000000000000/* * Copyright (C) 2022 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 "gstclapperrawimporter.h" #include "gst/plugin/gstgtkutils.h" #include "gst/plugin/gstgdkformats.h" #define GST_CAT_DEFAULT gst_clapper_raw_importer_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); #define parent_class gst_clapper_raw_importer_parent_class GST_CLAPPER_IMPORTER_DEFINE (GstClapperRawImporter, gst_clapper_raw_importer, GST_TYPE_CLAPPER_IMPORTER); static GstBufferPool * gst_clapper_raw_importer_create_pool (GstClapperImporter *importer, GstStructure **config) { GstClapperRawImporter *self = GST_CLAPPER_RAW_IMPORTER_CAST (importer); GstBufferPool *pool; GST_DEBUG_OBJECT (self, "Creating new buffer pool"); pool = gst_video_buffer_pool_new (); *config = gst_buffer_pool_get_config (pool); gst_buffer_pool_config_add_option (*config, GST_BUFFER_POOL_OPTION_VIDEO_META); return pool; } static void gst_clapper_raw_importer_add_allocation_metas (GstClapperImporter *importer, GstQuery *query) { gst_query_add_allocation_meta (query, GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, NULL); gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL); } static GdkTexture * gst_clapper_raw_importer_generate_texture (GstClapperImporter *importer, GstBuffer *buffer, GstVideoInfo *v_info) { GdkTexture *texture; GstVideoFrame frame; if (G_UNLIKELY (!gst_video_frame_map (&frame, v_info, buffer, GST_MAP_READ))) { GST_ERROR_OBJECT (importer, "Could not map input buffer for reading"); return NULL; } texture = gst_video_frame_into_gdk_texture (&frame); gst_video_frame_unmap (&frame); return texture; } static void gst_clapper_raw_importer_init (GstClapperRawImporter *self) { } static void gst_clapper_raw_importer_class_init (GstClapperRawImporterClass *klass) { GstClapperImporterClass *importer_class = (GstClapperImporterClass *) klass; GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperrawimporter", 0, "Clapper RAW Importer"); importer_class->create_pool = gst_clapper_raw_importer_create_pool; importer_class->add_allocation_metas = gst_clapper_raw_importer_add_allocation_metas; importer_class->generate_texture = gst_clapper_raw_importer_generate_texture; } GstClapperImporter * make_importer (void) { return g_object_new (GST_TYPE_CLAPPER_RAW_IMPORTER, NULL); } GstCaps * make_caps (gboolean is_template, GstRank *rank, GStrv *context_types) { *rank = GST_RANK_MARGINAL; return gst_caps_from_string ( GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_SYSTEM_MEMORY ", " GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, "{ " GST_GDK_MEMORY_FORMATS " }") "; " GST_VIDEO_CAPS_MAKE ( "{ " GST_GDK_MEMORY_FORMATS " }")); } clapper-0.5.2/lib/gst/plugin/importers/gstclapperrawimporter.h000066400000000000000000000023371425527005600246550ustar00rootroot00000000000000/* * Copyright (C) 2022 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. */ #pragma once #include "gst/plugin/gstclapperimporter.h" G_BEGIN_DECLS #define GST_TYPE_CLAPPER_RAW_IMPORTER (gst_clapper_raw_importer_get_type()) G_DECLARE_FINAL_TYPE (GstClapperRawImporter, gst_clapper_raw_importer, GST, CLAPPER_RAW_IMPORTER, GstClapperImporter) #define GST_CLAPPER_RAW_IMPORTER_CAST(obj) ((GstClapperRawImporter *)(obj)) struct _GstClapperRawImporter { GstClapperImporter parent; }; G_END_DECLS clapper-0.5.2/lib/gst/plugin/importers/meson.build000066400000000000000000000100061425527005600221760ustar00rootroot00000000000000gst_clapper_gl_base_importer_dep = dependency('', required: false) all_importers = [ 'glimporter', 'gluploader', 'rawimporter', ] build_glbase = ( not get_option('glimporter').disabled() or not get_option('gluploader').disabled() ) gl_support_required = ( get_option('glimporter').enabled() or get_option('gluploader').enabled() ) # We cannot build any importers without sink that they depend on if not gst_clapper_sink_dep.found() foreach imp : all_importers if get_option(imp).enabled() error('"@0@" option was enabled, but it requires building gstreamer plugin'.format(imp)) endif endforeach endif gst_plugin_gl_base_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_base_deps += gtk_x11_dep if gst_gl_have_platform_glx gst_plugin_gl_base_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_base_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_base_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_base_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_glbase = false endif if gst_gl_have_platform_egl gst_plugin_gl_base_deps += gstglegl_dep endif foreach dep : gst_plugin_gl_base_deps if not dep.found() if gl_support_required error('GL-based importer was enabled, but required dependencies were not found') endif build_glbase = false endif endforeach if build_glbase gst_clapper_gl_base_importer_dep = declare_dependency( link_with: library('gstclapperglbaseimporter', 'gstclapperglbaseimporter.c', c_args: gst_clapper_plugin_args, include_directories: configinc, dependencies: gst_plugin_gl_base_deps, version: libversion, install: true, ), include_directories: configinc, dependencies: gst_plugin_gl_base_deps, ) endif build_glimporter = ( not get_option('glimporter').disabled() and gst_clapper_gl_base_importer_dep.found() ) if build_glimporter library( 'gstclapperglimporter', 'gstclapperglimporter.c', dependencies: gst_clapper_gl_base_importer_dep, include_directories: configinc, c_args: gst_clapper_plugin_args, install: true, install_dir: gst_clapper_importers_libdir, ) endif build_gluploader = ( not get_option('gluploader').disabled() and gst_clapper_gl_base_importer_dep.found() ) if build_gluploader library( 'gstclappergluploader', 'gstclappergluploader.c', dependencies: gst_clapper_gl_base_importer_dep, include_directories: configinc, c_args: gst_clapper_plugin_args, install: true, install_dir: gst_clapper_importers_libdir, ) endif # No need to auto build rawimporter if we are building gluploader build_rawimporter = ( not get_option('rawimporter').disabled() and (not build_gluploader or get_option('rawimporter').enabled()) and gst_clapper_sink_dep.found() ) if build_rawimporter library( 'gstclapperrawimporter', 'gstclapperrawimporter.c', dependencies: gst_clapper_sink_dep, include_directories: configinc, c_args: gst_clapper_plugin_args, install: true, install_dir: gst_clapper_importers_libdir, ) endif clapper-0.5.2/lib/gst/plugin/meson.build000066400000000000000000000027571425527005600201700ustar00rootroot00000000000000gst_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_clapper_plugin_deps = [ gtk4_dep, gst_dep, gstbase_dep, gstvideo_dep, gmodule_dep, ] build_gst_plugin = not get_option('gst-plugin').disabled() foreach dep : gst_clapper_plugin_deps if not dep.found() if get_option('gst-plugin').enabled() error('GStreamer plugin was enabled, but required dependencies were not found') endif build_gst_plugin = false endif endforeach if get_option('default_library') == 'static' gst_clapper_plugin_args += ['-DGST_STATIC_COMPILATION'] endif gst_clapper_plugin_sources = [ 'gstclappersink.c', 'gstclapperpaintable.c', 'gstgtkutils.c', 'gstplugin.c', 'gstclapperimporter.c', 'gstclapperimporterloader.c', ] if build_gst_plugin gst_clapper_sink_dep = declare_dependency( link_with: library('gstclapper', gst_clapper_plugin_sources, c_args: gst_clapper_plugin_args, include_directories: configinc, dependencies: gst_clapper_plugin_deps, install: true, install_dir: gst_plugins_libdir, ), include_directories: configinc, dependencies: gst_clapper_plugin_deps, ) endif subdir('importers') clapper-0.5.2/lib/meson.build000066400000000000000000000225151425527005600160670ustar00rootroot00000000000000glib_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) clapper-0.5.2/meson.build000066400000000000000000000020631425527005600153150ustar00rootroot00000000000000project('com.github.rafostar.Clapper', 'c', 'cpp', version: '0.5.2', meson_version: '>= 0.50.0', license: 'GPL-3.0-or-later', default_options: [ 'warning_level=1', 'buildtype=debugoptimized' ] ) gnome = import('gnome') 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')) pkglibdir = join_paths(libdir, meson.project_name()) pkgdatadir = join_paths(datadir, meson.project_name()) subdir('lib') if get_option('player') subdir('bin') subdir('data') subdir('po') install_subdir('src', install_dir: pkgdatadir) install_subdir('extras', install_dir: pkgdatadir) install_subdir('css', install_dir: pkgdatadir) install_subdir('ui', install_dir: pkgdatadir) python_bin = python.find_installation('python3') if not python_bin.found() error('No valid python3 binary found') endif meson.add_install_script('build-aux/meson/postinstall.py') endif clapper-0.5.2/meson_options.txt000066400000000000000000000016611425527005600166130ustar00rootroot00000000000000option('player', type: 'boolean', value: true, description: 'Build Clapper player' ) option('lib', type: 'boolean', value: true, description: 'Build GstClapper lib' ) option('gst-plugin', type: 'feature', value: 'auto', description: 'Build GStreamer plugin (includes GTK video sink element)' ) option('glimporter', type: 'feature', value: 'auto', description: 'Build GL memory importer for clappersink' ) option('gluploader', type: 'feature', value: 'auto', description: 'Build GL uploader for clappersink' ) option('rawimporter', type: 'feature', 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' ) clapper-0.5.2/pkgs/000077500000000000000000000000001425527005600141165ustar00rootroot00000000000000clapper-0.5.2/pkgs/flatpak/000077500000000000000000000000001425527005600155405ustar00rootroot00000000000000clapper-0.5.2/pkgs/flatpak/.gitignore000066400000000000000000000001661425527005600175330ustar00rootroot00000000000000build/ builddir/ repo/ .flatpak-builder/ com.github.rafostar.Clapper.flatpak flathub/com.github.rafostar.Clapper.json clapper-0.5.2/pkgs/flatpak/com.github.rafostar.Clapper-nightly.json000066400000000000000000000036611425527005600253610ustar00rootroot00000000000000{ "app-id": "com.github.rafostar.Clapper", "runtime": "org.gnome.Platform", "runtime-version": "master", "sdk": "org.gnome.Sdk", "sdk-extensions": [ "org.freedesktop.Sdk.Extension.rust-nightly", "org.freedesktop.Sdk.Extension.llvm13" ], "command": "com.github.rafostar.Clapper", "finish-args": [ "--share=ipc", "--socket=fallback-x11", "--socket=wayland", "--device=dri", "--socket=pulseaudio", "--share=network", "--device=all", "--filesystem=xdg-run/pipewire-0:ro", "--filesystem=xdg-videos", "--filesystem=xdg-run/gvfsd", "--own-name=org.mpris.MediaPlayer2.Clapper", "--talk-name=org.gtk.vfs.*", "--talk-name=org.gnome.Shell", "--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0" ], "build-options": { "append-path": "/usr/lib/sdk/rust-nightly/bin:/usr/lib/sdk/llvm13/bin", "prepend-ld-library-path": "/usr/lib/sdk/llvm13/lib" }, "modules": [ "flathub/shared-modules/gudev/gudev.json", "flathub/lib/libsass.json", "flathub/lib/sassc.json", "flathub/lib/libadwaita.json", "flathub/lib/liba52.json", "flathub/lib/libmpeg2.json", "flathub/lib/libdv.json", "flathub/lib/libdvdcss.json", "flathub/lib/libdvdread.json", "flathub/lib/libdvdnav.json", "flathub/lib/libass.json", "flathub/lib/ffmpeg.json", "testing/gstreamer.json", "testing/gst-plugins-rs.json", "testing/gtuber.json", { "name": "clapper", "buildsystem": "meson", "sources": [ { "type": "dir", "path": "../../." } ] } ], "cleanup-commands": [ "ln -s /lib/$FLATPAK_ARCH-linux-*/gstreamer-1.0/libgstpipewire.so /app/lib/gstreamer-1.0/" ] } clapper-0.5.2/pkgs/flatpak/com.github.rafostar.Clapper.json000066400000000000000000000034131425527005600237000ustar00rootroot00000000000000{ "app-id": "com.github.rafostar.Clapper", "runtime": "org.gnome.Platform", "runtime-version": "42", "sdk": "org.gnome.Sdk", "command": "com.github.rafostar.Clapper", "finish-args": [ "--share=ipc", "--socket=fallback-x11", "--socket=wayland", "--device=dri", "--socket=pulseaudio", "--share=network", "--device=all", "--filesystem=xdg-run/pipewire-0:ro", "--filesystem=xdg-videos", "--filesystem=xdg-run/gvfsd", "--own-name=org.mpris.MediaPlayer2.Clapper", "--talk-name=org.gtk.vfs.*", "--talk-name=org.gnome.Shell", "--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0" ], "modules": [ "flathub/shared-modules/gudev/gudev.json", "flathub/lib/libsass.json", "flathub/lib/sassc.json", "flathub/lib/liba52.json", "flathub/lib/libmpeg2.json", "flathub/lib/libdv.json", "flathub/lib/libdvdcss.json", "flathub/lib/libdvdread.json", "flathub/lib/libdvdnav.json", "flathub/lib/libass.json", "flathub/lib/ffmpeg.json", "flathub/lib/uchardet.json", "flathub/gstreamer-1.0/gstreamer.json", "flathub/lib/gtk4.json", "flathub/lib/libadwaita.json", "testing/gtuber.json", { "name": "clapper", "buildsystem": "meson", "config-opts": [ "-Dc_args=\"-DHAVE_GST_PATCHES=1\"" ], "sources": [ { "type": "dir", "path": "../../." } ] } ], "cleanup-commands": [ "ln -s /lib/$FLATPAK_ARCH-linux-*/gstreamer-1.0/libgstpipewire.so /app/lib/gstreamer-1.0/" ] } clapper-0.5.2/pkgs/flatpak/flathub/000077500000000000000000000000001425527005600171655ustar00rootroot00000000000000clapper-0.5.2/pkgs/flatpak/testing/000077500000000000000000000000001425527005600172155ustar00rootroot00000000000000clapper-0.5.2/pkgs/flatpak/testing/gst-plugins-rs.json000066400000000000000000000010451425527005600230060ustar00rootroot00000000000000{ "name": "gst-plugins-rs", "buildsystem": "simple", "build-options": { "build-args": [ "--share=network" ], "env": { "CARGO_HOME": "/run/build/gst-plugins-rs/cargo" } }, "sources": [ { "type": "git", "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git", "branch": "main" } ], "build-commands": [ "cargo install cargo-c", "cargo cinstall --prefix=/app -p gst-plugin-dav1d" ] } clapper-0.5.2/pkgs/flatpak/testing/gstreamer.json000066400000000000000000000030741425527005600221050ustar00rootroot00000000000000{ "name": "gstreamer", "buildsystem": "meson", "config-opts": [ "--wrap-mode=nodownload", "-Dbase=enabled", "-Dgood=enabled", "-Dbad=enabled", "-Dugly=enabled", "-Dlibav=enabled", "-Dvaapi=enabled", "-Dsharp=disabled", "-Drs=disabled", "-Dpython=disabled", "-Ddevtools=disabled", "-Dges=disabled", "-Drtsp_server=disabled", "-Dgst-examples=disabled", "-Dqt5=disabled", "-Dtests=disabled", "-Dexamples=disabled", "-Dintrospection=enabled", "-Ddoc=disabled", "-Dgtk_doc=disabled", "-Dgpl=enabled", "-Dgstreamer:benchmarks=disabled", "-Dgst-plugins-base:gl_api=opengl,gles2", "-Dgst-plugins-base:gl_platform=egl,glx", "-Dgst-plugins-good:gtk3=disabled", "-Dgst-plugins-bad:vulkan=disabled", "-Dgst-plugins-bad:webrtc=disabled", "-Dgst-plugins-bad:wasapi=disabled", "-Dgst-plugins-bad:wasapi2=disabled", "-Dgst-plugins-bad:winks=disabled", "-Dgst-plugins-bad:winscreencap=disabled", "-Dgst-plugins-bad:assrender=enabled", "-Dgst-plugins-bad:nvcodec=enabled", "-Dgst-plugins-bad:v4l2codecs=enabled", "-Dgst-plugins-bad:va=enabled", "-Dgst-plugins-ugly:mpeg2dec=enabled" ], "sources": [ { "type": "git", "url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git", "branch": "main", "disable-submodules": true } ] } clapper-0.5.2/pkgs/flatpak/testing/gtuber.json000066400000000000000000000006251425527005600214030ustar00rootroot00000000000000{ "name": "gtuber", "buildsystem": "meson", "config-opts": [ "-Dintrospection=disabled", "-Dvapi=disabled", "-Dgst-gtuber=enabled" ], "cleanup": [ "/include", "/lib/pkgconfig" ], "sources": [ { "type": "git", "url": "https://github.com/Rafostar/gtuber.git", "branch": "main" } ] } clapper-0.5.2/po/000077500000000000000000000000001425527005600135705ustar00rootroot00000000000000clapper-0.5.2/po/LINGUAS000066400000000000000000000000771425527005600146210ustar00rootroot00000000000000ar ca cs de es eu fr he hu it ja nl pl pt pt_BR ru sv tr zh_CN clapper-0.5.2/po/POTFILES000066400000000000000000000002731425527005600147420ustar00rootroot00000000000000ui/clapper.ui ui/elapsed-time-button.ui ui/help-overlay.ui ui/preferences-plugin-ranking-subpage.ui ui/preferences-window.ui src/buttons.js src/dialogs.js src/revealers.js src/widget.js clapper-0.5.2/po/af.po000066400000000000000000000164701425527005600145260ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Afrikaans\n" "Language: af_ZA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: af\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/ar.po000066400000000000000000000240701425527005600145350ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Arabic\n" "Language: ar_SA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: ar\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "فتح الملفات…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "فتح عنوان URL…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "الإعدادات" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "الاختصارات" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "حول Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "السرعة" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "عادي" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "عام" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "إظهار الاختصارات" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "تبديل ملء الشاشة" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "نقرة مزدوجة" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "مغادرة ملء الشاشة" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "إظهار المعلومات (ملء الشاشة فقط)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "نقرة" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "خروج" #: ui/help-overlay.ui:47 msgid "Media" msgstr "الوسائط" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "افتح ملفًا" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "فتح عنوان URL" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "قوائم التشغيل" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "المحتوى التالي" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "نقر مزدوج (الجانب الأيمن)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "المحتوى السابق" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "نقر مزدوج (الجانب الأيسر)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "تغيير وضع التكرار" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "التصدير إلى مِلَفّ" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "المشغل" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "بَدْءّ / إيقاف" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "اضغط مطولاً | انقر بزر الفائرة الأيمن" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "التقدم للأمام" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "مرر لليمين" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "الرجوع للوراء" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "مرر لليسار" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "رفع مستوى الصوت" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "مرر لأعلى" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "خفض مستوى الصوت" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "مرر لأسفل" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "كتم الصوت" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "الفصل التالي" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "الفصل السابق" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "برامج فك التشفير" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "العودة إلى الإعدادات" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "السلوك" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "ملء الشاشة تلقائياً" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "أدخل ملء الشاشة عند استبدال قائمة التشغيل باستثناء الوضع العائم" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "اطلب استئناف الوسائط السابقة" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "عائم في جميع مساحات العمل" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "هذا الخِيار يعمل فقط على GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "بعد انتهاء" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "لا تفعل شيئًا" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "تجميد الأخر لقطة" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "أغلق التطبيق" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "الصوت" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "قيمة الافتراضية مخصصة" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "تعيين مستوى صوت مخصص عند بَدْء التشغيل بدلاً من إعادته" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "حجم الصوت" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "الوضع" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "النمط" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "دَقيق" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "سريع" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "الوحدة" #: ui/preferences-window.ui:97 msgid "Second" msgstr "ثواني" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "دقائق" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "النسبة المئوية" #: ui/preferences-window.ui:107 msgid "Value" msgstr "القيمة" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "الصوت" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "إزاحة بالمللي ثانية" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "تنسيقات الصوت الأصلية فقط" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "التَّرْجَمَةً" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "الخط الافتراضي" #: ui/preferences-window.ui:153 msgid "Network" msgstr "الشبكة" #: ui/preferences-window.ui:157 msgid "Client" msgstr "العميل" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "التخزين المؤقت للتنزيل بالتدريج" #: ui/preferences-window.ui:168 msgid "Server" msgstr "الخادم" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "التحكم بالوسائط عن بعد" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "منفذ الاستماع" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "تعديلات" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "إعدادات المظهر" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "المظهر الداكن" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "عرض ظلال النافذة" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "تعطيل لزيادة الأداء عند وضع النافذة" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "أعدادات الإضافات" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "تغيير الأعدادات الافتراضية للأضافات GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "استخدام playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "يتطلب إعادة التشغيل" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "تجريبية" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "استخدام PipeWire لإخراج الصوت" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "فك التشفير: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "أدخل أو الصق URI هنا" #: src/dialogs.js:157 msgid "Cancel" msgstr "إلغاء" #: src/dialogs.js:158 msgid "Open" msgstr "فتح" #: src/dialogs.js:226 msgid "Title" msgstr "العنوان" #: src/dialogs.js:227 msgid "Completed" msgstr "تم مشاهدة" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "استئناف التشغيل؟" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "إصدار GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "إصدار Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "إصدار GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "إصدار GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "مشغل وسائط GNOME مدعوم من GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Yousef Fawaz" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "ينتهي في: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "غير محدّد" #: src/widget.js:242 msgid "Channels" msgstr "قنوات" #: src/widget.js:260 msgid "Disabled" msgstr "مُعطّل" clapper-0.5.2/po/ca.po000066400000000000000000000226611425527005600145220ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Catalan\n" "Language: ca_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: ca\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Obre fitxers…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Obre l'URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Preferències" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Dreceres" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Quant al Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Velocitat" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "General" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Mostra les dreceres" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Commuta la pantalla completa" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Prem dues vegades | Doble clic" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Surt de la pantalla completa" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Mostra l'OSD (només pantalla completa)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Toca" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Surt" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Mèdia" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Obre fitxers" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Obre l'URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Llista de reproducció" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Element següent" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Prem dues vegades (part dreta)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Element anterior" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Prem dues vegades (part esquerra)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Canvia el mode de repetició" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exporta a un fitxer" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Reproducció" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Commuta la reproducció" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Prem llarga | Clic dret" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Avança" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Llisca el dit a la dreta | Desplaça a la dreta" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Retrocedeix" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Llisca el dit a l'esquerra | Desplaça a l'esquerra" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Apuja el volum" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Llisca el dit cap amunt | Desplaça amunt" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Baixa el volum" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Llisca el dit cap avall | Desplaça cap avall" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Commuta el silenci" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Capítol següent" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Capítol anterior" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Descodificadors" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Torna a les preferències" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Comportament" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Pantalla completa automàtica" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Entra a pantalla completa quan se substitueix la llista de reproducció excepte el mode flotant" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Pregunta per reprendre recents" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Flota en tots els espais de treball" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Aquesta opció només funciona amb el GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Després de la reproducció" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "No facis res" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Congela l'últim fotograma" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Surt de l'aplicació" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volum" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Valor inicial personalitzat" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Estableix el volum personalitzat a l'inici en lloc de restaurar-lo" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Percentatge de volum" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Cerca" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Mode" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Acurat" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Ràpid" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unitat" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Segon" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minut" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Percentatge" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Valor" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Àudio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Desplaçament en mil·lisegons" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Només formats d'àudio natius" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Subtítols" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Tipus de lletra per defecte" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Xarxa" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Client" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Memòria intermèdia de baixada progressiva" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Servidor" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Controla el reproductor remotament" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Port d'escolta" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Ajustaments" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Aparença" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Tema fosc" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Renderitza les ombres de la finestra" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Inhabilita per augmentar el rendiment en mode finestra" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Classificació dels connectors" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Altera els rangs predeterminats dels connectors del GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Usa playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Requereix reiniciar el reproductor" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimental" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Usa PipeWire per la sortida d'àudio" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Descodificador: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Escriu o deixa anar una URI aquí" #: src/dialogs.js:157 msgid "Cancel" msgstr "Cancel·la" #: src/dialogs.js:158 msgid "Open" msgstr "Obre" #: src/dialogs.js:226 msgid "Title" msgstr "Títol" #: src/dialogs.js:227 msgid "Completed" msgstr "Completat" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Reprèn la reproducció?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Versió del GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Versió del Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Versió del GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Versió del GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Un reproductor multimèdia del GNOME que usa GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Ícar Nin Solana , 2021" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Acaba a la/les %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Sense determinar" #: src/widget.js:242 msgid "Channels" msgstr "Canals" #: src/widget.js:260 msgid "Disabled" msgstr "Deshabilitat" clapper-0.5.2/po/com.github.rafostar.Clapper.pot000066400000000000000000000165511425527005600215700ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the com.github.rafostar.Clapper package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: com.github.rafostar.Clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/cs.po000066400000000000000000000227571425527005600145520ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Czech\n" "Language: cs_CZ\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: cs\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Otevřít soubory…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Otevřít URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Předvolby" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Klávesové zkratky" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "O aplikaci Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Rychlost" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normální" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Obecné" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Zobrazit klávesové zkratky" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Celá obrazovka" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Dvojitým poklepáním | Dvojitým kliknutím" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Opustit režim celé obrazovky" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Odhalit OSD (pouze při zobrazení na celou obrazovku)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Klepnutím" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Ukončit" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Média" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Otevřít soubory" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Otevřít URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Playlisty" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Další položka" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Dvojité poklepání (pravá strana)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Předchozí položka" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Dvojité poklepání (levá strana)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Změnit režim opakování" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exportovat do souboru" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Přehrávání" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Přepnout přehrávání" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Dlouhým stisknutím | Klepnutím pravým tlačítkem" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Přetočit vpřed" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Přejetím vpravo | Scrollováním vpravo" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Přetočit zpět" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Přejetím vlevo | Scrollováním vlevo" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Zesílit" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Přejetím nahoru | Scrollováním nahoru" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Zeslabit" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Přejetím dolů | Scrollováním dolů" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Vypnout/zapnout zvuk" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Další kapitola" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Předchozí kapitola" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Dekodéry" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Zpět do předvoleb" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Chování" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Automaticky na celou obrazovku" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Přejít do režimu na celou obrazovku při nahrazení seznamu skladeb s výjimkou plovoucího režimu" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Zeptat se na obnovení nedávných médií" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Plovoucí režim na všech pracovních plochách" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Tato možnost funguje pouze na GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Po skončení přehrávání" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Nedělat nic" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Zůstat na posledním snímku" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Zavřít aplikaci" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Hlasitost" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Vlastní počáteční hodnota" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Nastavit vlastní hlasitost při spuštění namísto obnovení předchozí hodnoty" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Procento hlasitosti" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Přetáčení" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Režim" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Přesný" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Rychlý" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Jednotka" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Sekundy" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuty" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Procenta" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Hodnota" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Zvuk" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Posunutí v milisekundách" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Pouze nativní formáty zvuku" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Titulky" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Výchozí styl písma" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Síť" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Klient" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Postupné stahování do vyrovnávací paměti" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Server" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Vzdáleně ovládat přehrávač" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Naslouchat na portu" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Vylepšení" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Vzhled" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Tmavý motiv" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Vykreslovat stíny okna" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Zakázat pro zvýšení výkonu" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Pořadí pluginů" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Změna výchozího pořadí zásuvných modulů GStreameru" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Použít playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Vyžaduje restart přehrávače" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimentální" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Použít PipeWire pro zvukový výstup" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Dekodér: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Zde zadejte nebo přetáhněte URI" #: src/dialogs.js:157 msgid "Cancel" msgstr "Zrušit" #: src/dialogs.js:158 msgid "Open" msgstr "Otevřít" #: src/dialogs.js:226 msgid "Title" msgstr "Název" #: src/dialogs.js:227 msgid "Completed" msgstr "Přehráno" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Pokračovat v přehrávání?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Verze GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Verze Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Verze GStreameru: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Verze GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Přehrávač médií pro GNOME postavený na GStreameru" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Vojtěch Perník " #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Končí v: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Neurčeno" #: src/widget.js:242 msgid "Channels" msgstr "Kanály" #: src/widget.js:260 msgid "Disabled" msgstr "Vypnuto" clapper-0.5.2/po/da.po000066400000000000000000000164651425527005600145300ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Danish\n" "Language: da_DK\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: da\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/de.po000066400000000000000000000226641425527005600145320ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-27 16:27\n" "Last-Translator: \n" "Language-Team: German\n" "Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: de\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Dateien öffnen…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Webquelle öffnen…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Einstellungen" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Tastenkombinationen" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Über Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Wiedergabegeschwindigkeit" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Allgemein" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Tastenkombinationen zeigen" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "In Vollbildmodus wechseln" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Doppelt tippen bzw. klicken" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Vollbildmodus beenden" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "OSD zeigen (nur im Vollbildmodus)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tippen" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Clapper beenden" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Medien" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Dateien öffnen" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Webquelle öffnen" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Wiedergabeliste" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Nächster Titel" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Doppelt auf der rechten Seite tippen" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Vorheriger Titel" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Doppelt auf der linken Seite tippen" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Wiederholmodus ändern" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Als Datei exportieren" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Wiedergabe" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Wiedergabe umschalten" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Lange Bildschirm berühren oder Rechtsklick" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Vorwärts springen" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Nach rechts wischen bzw. scrollen" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Rückwärts springen" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Nach links wischen bzw. scrollen" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Lautstärke erhöhen" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Nach oben wischen bzw. scrollen" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Lautstärke verringern" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Nach unten wischen bzw. scrollen" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "In Vollbildmodus wechseln" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Nächstes Kapitel" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Vorheriges Kapitel" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Decodierer" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Zurück zu den Einstellungen" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Verhalten" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Automatischer Vollbildmodus" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Vollbildmodus betreten, wenn Wiedergabeliste ersetzt wird. (Außer Clapper befindet sich im Schwebemodus)" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Nachfragen ob kürzlich wiedergegebene Medien weitergespielt werden sollen" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Schwebemodus auf allen Arbeitsflächen" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Diese Option funktioniert nur auf GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Nach Wiedergabe" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Nichts tun" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Letztes Bild einfrieren" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Anwendung beenden" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Lautstärke" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Benutzerdefinierter Anfangswert" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Lautstärke auf einen fixierten Wert beim Start setzen, anstelle ihn wiederherzustellen." #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Lautstärke (in Prozent)" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Springen" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modus" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Genau" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Schnell" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Einheit" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Sekunden" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuten" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Prozentsatz" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Wert" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Verschiebung in Millisekunden" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Nur systemeigene Audioformate" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Untertitel" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Standartschriftart" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Netzwerk" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Client" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Fortschreitender Download-Puffer" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Server" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Clapper fernsteuern" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Empfangsport" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Optimierungen" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Erscheinungsbild" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Dunkler Modus" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Fensterschatten darstellen" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Deaktivieren, um Performance im Fenstermodus zu verbessern" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Plugin-Reihenfolge" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Standard GStreamer Reihenfolge verändern" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Playbin3 benutzen" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Benötigt Neustart von Clapper" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimentell" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "PipeWire für Audioausgabe verwenden" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Decodierer: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "URI hier einfügen" #: src/dialogs.js:157 msgid "Cancel" msgstr "Abbrechen" #: src/dialogs.js:158 msgid "Open" msgstr "Öffnen" #: src/dialogs.js:226 msgid "Title" msgstr "Titel" #: src/dialogs.js:227 msgid "Completed" msgstr "Abgespielt" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Wiedergabe fortfahren?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTK Version: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaita Version: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer Version: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS Version: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Ein von GStreamer betriebener GNOME Multimedia-Player" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Florian \"sp1rit\" " #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Ended um %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Unbestimmet" #: src/widget.js:242 msgid "Channels" msgstr "Audio-Kanäle" #: src/widget.js:260 msgid "Disabled" msgstr "Deaktiviert" clapper-0.5.2/po/el.po000066400000000000000000000164641425527005600145430ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Greek\n" "Language: el_GR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: el\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/es.po000066400000000000000000000224051425527005600145420ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Spanish\n" "Language: es_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: es-ES\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Abrir archivos…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Abrir URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Preferencias" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Atajos" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Acerca de Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Velocidad" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "General" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Mostrar atajos" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Fijar a pantalla completa" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Doble toque | Doble clic " #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Salir de pantalla completa" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Revelar OSD (solo pantalla completa)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tocar" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Renunciar" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Media" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Abrir archivos" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Abrir URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Lista de reproducción" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Elemento siguiente" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Tocar doble (lado derecho)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Elemento anterior" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Tocar doble (lado izquierdo)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Cambiar a modo repetir" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exportar a archivo" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Reproducir" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Fijar a reproducir" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Pulsación larga | Clic derecho" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Buscar siguiente" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Deslizar a derecha | Desplazar a derecha" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Buscar anterior" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Deslizar a izquierda | Desplazar a izquierda" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Volumen +" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Deslizar + | Desplazar +" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Volumen -" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Deslizar - | Desplazar -" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Fijar a mudo" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Capítulo siguiente" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Capítulo anterior" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Decodificadores" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Regresar a preferencias" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Configuraciones" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Pantalla completa automática" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Entra a pantalla completa cuando se reemplaza la lista de reproducción, excepto en modo flotante" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Preguntar al reanudar los medios recientes" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Flotar todos los espacios de trabajo" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Esta opción solo funciona en GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Después de reproducción " #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Nada por hacer" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Detener último fotograma" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Cerrar la aplicación" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volumen" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Valor inicial personalizado" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Establece un volumen personalizado al inicio en lugar de restaurarlo" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Porcentaje de volumen" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Buscando" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modo" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Precisión" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Rápido" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unidad" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Segundo" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuto" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Porcentaje" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Valor" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Compensación en milisegundos" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Solo formatos de audio nativos" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Subtítulos" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Fuente por defecto" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Red" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Cliente" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Bajada progresiva de búfer" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Servidor" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Controlar el reproductor remotamente" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Puerto de escucha" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Retoques" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Apariencia" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Tema oscuro" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Renderizar sombras de ventana" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Deshabilitado aumenta el rendimiento cuando se abre en ventana" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Rango de enchufes" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Altera los rangos predeterminados de los enchufes de GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Usar Playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Require reiniciar el reproductor" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimental" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Usar PipeWire" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Decodificador: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Introducir la URI" #: src/dialogs.js:157 msgid "Cancel" msgstr "Cancelar" #: src/dialogs.js:158 msgid "Open" msgstr "Abrir" #: src/dialogs.js:226 msgid "Title" msgstr "Título" #: src/dialogs.js:227 msgid "Completed" msgstr "Completado" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Resumir el reproductor?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Versión GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Versión LibAdwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Versión GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Versión GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Un reproductor multimedia de GNOME impulsado por GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "carlosgonz" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Termina en: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Indeterminado" #: src/widget.js:242 msgid "Channels" msgstr "Canales" #: src/widget.js:260 msgid "Disabled" msgstr "Deshabilitar" clapper-0.5.2/po/eu.po000066400000000000000000000225321425527005600145450ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Basque\n" "Language: eu_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: eu\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Artxiboak ireki…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Ireki URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Hobespenak" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Lasterbideak" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Clapperi buruz" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Abiadura" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normala" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Orokorra" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Erakutsi lasterbideak" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Aktibatu pantaila osoa" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Ukitu bikoitza / Bikoitza klik" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Pantaila osoa utzi" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "OSD errebelatu (pantaila osoan bakarrik)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Ukitu" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Irten" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Media" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Artxiboak ireki" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Ireki URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Erreprodukzio-zerrenda" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Hurrengo elementua" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Ukitu bikoitza (eskuineko aldea)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Aurreko elementua" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Ukitu bikoitza (ezkerreko aldea)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Errepikapen modua aldatu" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Esportatu fitxategira" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Erreprodukzioa" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Erreprodukzioa aktibatu" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Pultsazio luzea / Eskuineko klik" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Aurrerapena bilatu" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Eskuinera irristatu / Eskuinera joan" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Atzerantz bilatu" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Ezkerrera irristatu / Ezkerrera mugitu" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Bolumena igo" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Gorantz irristatu / Gorantz mugitu" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Bolumena jeitsi" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Beherantz irristatu / Beherantz mugitu" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Aktibatu isiltasuna" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Hurrengo kapitulua" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Aurreko kapitulua" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Deskodetzaileak" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Itzuli ezarpenetara" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Portaera" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Pantaila osoa automatikoa" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Pantaila osoan sartu erreprodukzio-zerrenda ordezten denean, flotatzeko modua izan ezik" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Azkenaldiko media berriro ekiteko eskatzea" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Laneko espazio guztietan flotatzea" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Aukera honek GNOMEn bakarrik funtzionatzen du" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Erreprodukzioaren ondoren" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Ez egin ezer" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Izoztu azken fotograma" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Aplikazioa itxi" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Bolumena" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Hasierako balio pertsonalizatua" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Hasieran bolumen pertsonalizatua ezartzea, lehengoratu beharrean" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Bolumenaren ehunekoa" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Bilaketa" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modua" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Zehatza" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Azkarra" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unitatea" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Segundu" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minutu" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Ehunekoa" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Balioa" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audioa" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Desplazamendua milisegundotan" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Jatorrizko audio-formatuak bakarrik" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Azpitituluak" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Hizki lehenetsia" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Sarea" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Bezeroa" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Deskargak pixkanaka bufferizatzea" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Zerbitzaria" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Urrutiko erreproduzitzailea kontrolatzea" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Entzuteko ataka" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Ukituak" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Itxura" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Gai iluna" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Leihoetako itzalak errenderizatzea" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Desaktibatu leihoa erabiltzen denean errendimendua handitzeko" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Pluginen sailkapena" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Aldatu GStreamer-en pluginen lehenetsitako mailak" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Playbin3 erabili" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Berrabiaraztea eskatzen du" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Esperimentala" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Erabili PipeWire audio-irteerarako" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Deskodetzailea: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Sartu edo utzi URIa erortzen hemen" #: src/dialogs.js:157 msgid "Cancel" msgstr "Ezeztatu" #: src/dialogs.js:158 msgid "Open" msgstr "Ireki" #: src/dialogs.js:226 msgid "Title" msgstr "Titulua" #: src/dialogs.js:227 msgid "Completed" msgstr "Osatuta" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Jarraitu erreprodukzioarekin?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTK bertsioa: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaitaren bertsioa: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer-en bertsioa: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS bertsioa: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "GNOMEren multimedia erreproduzitzaile bat GStreamer-ekin" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Sergio Varela (@IngrownMink4)" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Amaiera: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Zehaztugabea" #: src/widget.js:242 msgid "Channels" msgstr "Kanalak" #: src/widget.js:260 msgid "Disabled" msgstr "Desgaituta" clapper-0.5.2/po/fi.po000066400000000000000000000164661425527005600145430ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Finnish\n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: fi\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/fr.po000066400000000000000000000227721425527005600145510ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: French\n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: fr\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Ouvrir un fichier…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Ouvrir une URL…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Préférences" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Raccourcis clavier" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Á propos de Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Vitesse" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Général" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Montrer les raccourcis" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Basculer en plein écran" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Tapoter/cliquer deux fois" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Sortir du plein écran" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Afficher les commandes de lecture (seulement en plein écran)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Taper" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Quitter" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Média" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Ouvrir un fichier" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Ouvrir une URL" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Playlist" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Prochain média" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Tapoter deux fois (côté droit)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Média précédent" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Tapoter deux fois (côté gauche)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Changer le mode de répétition" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exporter vers un fichier" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Lecture" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Relancer/stopper la lecture" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Longue pression | Clic droit" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Avancer dans la lecture" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Glisser/Faire défiler vers la droite" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Reculer dans la lecture" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Glisser/Faire défiler vers la gauche" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Augmenter le volume" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Glisser/Défiler vers le haut" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Baisser le volume" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Glisser/Défiler vers le bas" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Basculer le mode silencieux" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Prochain chapitre" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Chapitre précédent" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Décodeurs" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Retourner aux préférences" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Comportement" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Lecture automatique en plein écran" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Basculer en plein écran quand la playlisyt est remplacée sauf si le mode flottant est activé" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Demander pour reprendre à la position des médias récents" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Flotter sur tous les bureaux virtuels" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Cette option ne marche qu'avec GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Après la lecture" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Ne rien faire" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Geler la dernière image" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Fermer l'application" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volume" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Valeur initiale personnalisée" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Régler une valeur personnalisée du volume au démarrage au lien de restorer la valeur précédente" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Pourcentage du volume" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Avancement" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Comportement" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Précis" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Rapide" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unité des sauts" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Seconde" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minute" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Pourcentage" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Longueur du saut" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Décalage en millisecondes" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Seulement des formats audios natifs" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Sous-titres" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Police par défaut" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Réseau" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Client" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Téléchargement progressif dans le tampon" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Serveur" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Controler le lecteur à distance" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Écouter sur le port" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Réglages" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Apparence" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Thème sombre" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Afficher les ombres de la fenêtre" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Désactiver pour améliorer les performances quand fenêtré" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Liste des plugins" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Changer les options par défaut de plugins GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Utiliser playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Requiert le redémarrage du lecteur" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Expérimental" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Utiliser PipeWire pour la sortie audio" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Décodeur: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Entrer ou déposer une URL ici" #: src/dialogs.js:157 msgid "Cancel" msgstr "Annuler" #: src/dialogs.js:158 msgid "Open" msgstr "Ouvrir" #: src/dialogs.js:226 msgid "Title" msgstr "Titre" #: src/dialogs.js:227 msgid "Completed" msgstr "Terminé" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Reprendre la lecture?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Version de GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Version d'Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Version de GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Version de GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Un lecteur multimédia pour GNOME propulsé par GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Robin Verdenal-Tallieux" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Finit à: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Indéterminé" #: src/widget.js:242 msgid "Channels" msgstr "Chaines" #: src/widget.js:260 msgid "Disabled" msgstr "Désactivé" clapper-0.5.2/po/he.po000066400000000000000000000235531425527005600145340ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-06-01 08:42\n" "Last-Translator: \n" "Language-Team: Hebrew\n" "Language: he_IL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: he\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "פתיחת קבצים…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "פתיחת כתובת…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "העדפות" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "צירופי מקשים" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "על אודות Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "מהירות" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "רגילה" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "כללי" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "הצגת צירופי מקשים" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "שינוי מצב מסך מלא" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "נגיעה כפולה | לחיצה כפולה" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "יציאה ממסך מלא" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "הצגת מידע וכפתורים (מסך מלא בלבד)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "נגיעה" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "יציאה" #: ui/help-overlay.ui:47 msgid "Media" msgstr "מדיה" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "פתיחת קבצים" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "פתיחת כתובת" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "רשימת השמעה" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "פריט הבא" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "נגיעה כפולה (צד ימין)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "פריט קודם" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "נגיעה כפולה (צד שמאל)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "שינוי מצב חזרה" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "ייצוא לקובץ" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "השמעה" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "הפעלה/השהיה" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "לחיצה ארוכה | לחצן ימני" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "דילוג קדימה" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "החלקה ימינה | גלילה ימינה" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "דילוג לאחור" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "החלקה שמאלה | גלילה שמאלה" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "הגברת עצמת השמע" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "החלקה למעלה | גלילה למעלה" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "הנמכת עצמת השמע" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "החלקה למעלה | גלילה למעלה" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "השתקה/ביטול השתקה" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "פרק הבא" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "פרק קודם" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "מפענחים" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "חזרה להגדרות" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "התנהגות" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "מסך מלא אוטומטי" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "כניסה למצב מסך מלא עם החלפת רשימת השמעה מלבד במצב ציפה" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "לשאול אם להפעיל מחדש את המדיה האחרונה" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "ציפה בכל מרחבי העבודה" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "אפשרות זו פועלת רק ב־GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "בסיום צפייה" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "לא לעשות דבר" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "הקפאת התמונה האחרונה" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "סגירת היישום" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "עצמת שמע" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "התאמת ערך התחלתי" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "הגדרת עצמת השמע עם ההפעלה במקום שחזור" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "עצמת שמע באחוזים" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "דילוג" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "מצב" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "מדויק" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "מהיר" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "יחידה" #: ui/preferences-window.ui:97 msgid "Second" msgstr "שניה" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "דקה" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "אחוז" #: ui/preferences-window.ui:107 msgid "Value" msgstr "ערך" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "שמע" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "היסט במילי־שניות" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "תבניות שמע רגילות בלבד" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "כתוביות" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "גופן ברירת מחדל" #: ui/preferences-window.ui:153 msgid "Network" msgstr "רשת" #: ui/preferences-window.ui:157 msgid "Client" msgstr "לקוח" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "שימוש בחוצץ הורדה" #: ui/preferences-window.ui:168 msgid "Server" msgstr "שרת" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "שליטה בנגן מרחוק" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "פתחת האזנה" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "התאמות" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "מראה" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "מראה כהה" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "עיבוד צל לחלון" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "השבתה לצמצום צריכת החלונות" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "דרגות תוספים" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "שינוי דירוג ברירת המחדל של תוספי GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "שימוש ב־playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "דורש הפעלה מחדש של הנגן" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "ניסיוני" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "שימוש ב־PipeWire לפלט השמע" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "מפענח: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "יש להזין או להשליך כתובת לכאן" #: src/dialogs.js:157 msgid "Cancel" msgstr "ביטול" #: src/dialogs.js:158 msgid "Open" msgstr "פתיחה" #: src/dialogs.js:226 msgid "Title" msgstr "כותרת" #: src/dialogs.js:227 msgid "Completed" msgstr "הושלם" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "להמשיך בצפייה?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "גרסת GTK: ‏%s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "גרסת Adwaita: ‏%s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "גרסת GStreamer: ‏%s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "גרסת GJS: ‏%s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "נגן מדיה עבור GNOME המופעל על ידי GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "יוסף אור בוצ׳קו " #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "שעת סיום: ‏%s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "לא מוגדר" #: src/widget.js:242 msgid "Channels" msgstr "ערוצים" #: src/widget.js:260 msgid "Disabled" msgstr "מושבת" clapper-0.5.2/po/hu.po000066400000000000000000000231551425527005600145520ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Hungarian\n" "Language: hu_HU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: hu\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Fájlok megnyitása…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "URI megnyitása…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Beállítások" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Gyorsbillentyűk" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "A Clapper -ről" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Sebesség" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normál" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Általános beállítások" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Gyorsbillentyűk megjelenítése" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Teljes képernyő be/ki" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Dupla koppintás | Dupla kattintás" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Teljes képernyő be/ki" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Képernyőn megjelenő kijelző (OSD) mutatása (csak teljes képernyős módban)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Koppintás" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Kilépés" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Média" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Fájlok megnyitása" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "URI megnyitása" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Lejátszólista" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Következő listaelem" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Dupla koppintás (jobb oldalon)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Előző listaelem" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Dupla koppintás (bal oldalon)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Ismétlési típus változtatása" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exportálás fájlba" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Lejátszás" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Lejátszás indítása/megállítása" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Hosszú nyomás | Jobb kattintás" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Előretekerés" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Húzás jobbra | Görgetés jobbra" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Visszatekerés" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Húzás balra | Görgetés balra" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Hangerő növelése" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Húzás felfelé | Görgetés felfelé" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Hangerő csökkentése" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Húzás lefelé | Görgetés lefelé" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Némítás be-/kikapcsolása" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Következő jelenet" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Előző jelenet" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Dekóderek" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Visszatérés a beállításokhoz" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Viselkedés" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Automatikus teljes képernyős mód" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Teljes képernyős módra váltás a lejátszólista megváltoztatásakor (kivétel ha lebegőablakos módban vagyunk)" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Rákérdezés a legutóbbi média folytatására" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Lebegő ablak az összes munkaasztalon" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Ez az opció csak GNOME asztali környezettel működik" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Lejátszás után" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Ne csináljon semmit" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Utolsó képkocka mutatása" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Alkalmazás bezárása" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Hangerő" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Egyedi kezdeti érték" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Induláskor egyedi hangerő az utolsó visszaállítása helyett" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Hangerő százalék" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Tekerés" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Mód" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Precíz" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Gyors" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Mértékegység" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Másodperc" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Perc" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Százalék" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Érték" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Hang" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Eltolás miliszekundumban" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Csak natív hang formátumok" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Feliratok" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Alapértelmezett betűtípus" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Hálózat" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Kliens" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Progresszív letöltéspufferelés" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Szerver" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Lejátszó távirányítása" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Port figyelése" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Finomhangolások" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Megjelenés" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Sötét téma" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Ablak árnyékok renderelése" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Kikapcsolása javítja a teljesítményt ablakban futtatás esetén" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Bővítményrangsor" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Az alapértelmezett GStreamer bővítményrangsor megváltoztatása" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "A playbin3 használata" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "A lejátszó újraindítását igényli" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Kísérleti funkció" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "A PipeWire használata hang kimenetként" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Dekóder: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Adjon meg egy URI -t itt, vagy húzza és ejtse ide" #: src/dialogs.js:157 msgid "Cancel" msgstr "Mégsem" #: src/dialogs.js:158 msgid "Open" msgstr "Megnyitás" #: src/dialogs.js:226 msgid "Title" msgstr "Cím" #: src/dialogs.js:227 msgid "Completed" msgstr "Lejátszva" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Lejátszás folytatása?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTK verzió: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaita verzió: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer verzió: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS verzió: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "GNOME médialejátszó a GStreamer képességeivel" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Ferenc Géczi" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "%s-kor ér véget" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Meghatározatlan" #: src/widget.js:242 msgid "Channels" msgstr "Csatornák" #: src/widget.js:260 msgid "Disabled" msgstr "Kikapcsolva" clapper-0.5.2/po/it.po000066400000000000000000000223101425527005600145420ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-27 12:50\n" "Last-Translator: \n" "Language-Team: Italian\n" "Language: it_IT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: it\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Apri i File…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Apri URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Preferenze" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Scorciatoie" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Informazioni su Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Velocità" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normale" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Generale" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Mostra scorciatoie" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Attiva/Disattiva schermo intero" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Doppio tocco | Doppio click" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Disattiva schermo intero" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Rivela controlli (solo a schermo intero)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tocco" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Esci" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Media" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Apri i file" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Apri URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Playlist" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Elemento successivo" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Doppio tocco (lato destro)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Elemento precedente" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Doppio tocco (lato sinistro)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Cambia modalità di ripetizione" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Esporta su file" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Riproduzione" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Attiva/Disattiva riproduzione" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Tocco prolungato | Click destro" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Scorri avanti" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Scorri a destra" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Scorri indietro" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Scorri a sinistra" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Aumenta volume" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Scorri in alto" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Abbassa volume" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Scorri in basso" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Attiva/Disattiva muto" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Capitolo successivo" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Capitolo precedente" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Decodificatori" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Ritorna alle preferenze" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Comportamento" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Schermo intero automatico" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Attiva schermo intero quando la playlist viene sostituita, tranne in modalità fluttuante" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Chiedi di riprendere i media recenti" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Fluttuante su tutti i workspace" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Questa opzione funziona solo su GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Dopo la riproduzione" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Non fare nulla" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Mostra l'ultimo fotogramma" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Chiudi l'app" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volume" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Valore iniziale personalizato" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Imposta un volume all'avvio personalizzato invece di ripristinarlo" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Percentuale volume" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Scorrimento" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modalità" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Accurata" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Veloce" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unità" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Secondo" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuto" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Percentuale" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Valore" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Ritardo/Anticipo in millisecondi" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Solo formati audio nativi" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Sottotitoli" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Carattere predefinito" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Rete" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Client" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Download buffering progressivo" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Server" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Controlla la riproduzione da remoto" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Porta in ascolto" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Aggiustamenti" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Aspetto" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Tema scuro" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Ombreggiatura della finestra" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Disabilita per aumentare le prestazioni in modalità finestra" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Ordine dei plugin" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Altera l'ordine predefinito dei plugin di GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Usa playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Richiede il riavvio del player" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Sperimentale" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Usa PipeWire per l'uscita audio" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Decoder: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Inserisci o rilascia URI qui" #: src/dialogs.js:157 msgid "Cancel" msgstr "Cancella" #: src/dialogs.js:158 msgid "Open" msgstr "Apri" #: src/dialogs.js:226 msgid "Title" msgstr "Titolo" #: src/dialogs.js:227 msgid "Completed" msgstr "Completato" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Riprendere la riproduzione?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Versione GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Versione Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Versione GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Versione GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Un riproduttore multimediale per GNOME basato su GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Uniformbuffer" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Termina alle: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Indeterminato" #: src/widget.js:242 msgid "Channels" msgstr "Canali" #: src/widget.js:260 msgid "Disabled" msgstr "Disabilitato" clapper-0.5.2/po/ja.po000066400000000000000000000236221425527005600145270ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Japanese\n" "Language: ja_JP\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: ja\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "ファイルを開く…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "URIを開く…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "設定" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "ショートカット" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Clapperについて" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "速度" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "標準" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "一般" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "ショートカットを表示する" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "フルスクリーン切り替え" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "ダブルタップ | ダブルクリック" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "フルスクリーンを終了" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "OSDを表示 (全画面表示の場合のみ)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "タップ" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "終了" #: ui/help-overlay.ui:47 msgid "Media" msgstr "メディア" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "ファイルを開く" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "URIを開く" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "プレイリスト" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "次の項目" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "ダブルタップ(右端)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "前の項目" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "ダブルタップ(左端)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "リピートモードを変更" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "ファイルへエクスポート" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "プレイバック" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "一時停止・再生" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Long press | Right click" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "早送り" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "右スワイプ | 右スクロール" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "巻き戻し" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "左スワイプ | 左スクロール" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "音量を上げる" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "上スワイプ | 上スクロール" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "音量を下げる" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "下スワイプ | 下スクロール" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "ミュート切り替え" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "次のチャプターへ" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "前のチャプターへ\"" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "デコーダー" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "設定へ戻る" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "挙動" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "自動的にフルスクリーンに切り替える" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "フローティングモードを除き、プレイリストを入れ替えた場合に自動的にフルスクリーンにする" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "前回再生したメディアを続きから再生するか尋ねる" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "すべてのワークスペースにフロートを配置する" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "このオプションはGnomeでのみ機能します" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "再生終了後" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "何もしない" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "最後のフレームで停止する" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "アプリを閉じる" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "音量" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "初期音量を設定する" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "前回使用した音量を使用するのではなく、起動時に音量をカスタム値に設定します" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "音量(%)" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "シーキング" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "モード" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "精密" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "高速" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "ユニット" #: ui/preferences-window.ui:97 msgid "Second" msgstr "秒" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "分" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "パーセント" #: ui/preferences-window.ui:107 msgid "Value" msgstr "値" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "オ−ディオ" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "オフセット(ミリ秒)" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "ネイティブオーディオフォーマットのみを使用する" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "字幕" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "既定のフォント" #: ui/preferences-window.ui:153 msgid "Network" msgstr "ネットワーク" #: ui/preferences-window.ui:157 msgid "Client" msgstr "クライアント" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "プログレッシブなバッファリングを使用する" #: ui/preferences-window.ui:168 msgid "Server" msgstr "サーバー" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "プレーヤーをリモートでコントロールする" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "待ち受けポート" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "詳細設定" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "外観" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "ダークテーマ" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "ウィンドウの影をレンダリングする" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "無効化するとウィンドウモードでのパフォーマンスが向上します" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "プライグインの優先順位" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "GStreamerプラグインの優先順位を変更します" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "playbin3を使用" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "プレイヤーの再起動が必要です" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "試験運用機能" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "オーディオ出力にPipeWireを使用する" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "デコーダー: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "ここにURIを入力またはドロップ" #: src/dialogs.js:157 msgid "Cancel" msgstr "キャンセル" #: src/dialogs.js:158 msgid "Open" msgstr "開く" #: src/dialogs.js:226 msgid "Title" msgstr "タイトル" #: src/dialogs.js:227 msgid "Completed" msgstr "再生済み" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "前回の続きから再生しますか?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTKバージョン: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaitaバージョン: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer バージョン: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS バージョン: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "GStreamerを使用したGNOME向けのメディアプレーヤー" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "翻訳者" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "再生終了時刻: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "不明" #: src/widget.js:242 msgid "Channels" msgstr "チャンネル" #: src/widget.js:260 msgid "Disabled" msgstr "Disabled" clapper-0.5.2/po/ko.po000066400000000000000000000164561425527005600145550ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Korean\n" "Language: ko_KR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: ko\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/meson.build000066400000000000000000000000631425527005600157310ustar00rootroot00000000000000i18n.gettext(meson.project_name(), preset: 'glib') clapper-0.5.2/po/nl.po000066400000000000000000000224171425527005600145470ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Dutch\n" "Language: nl_NL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: nl\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Bestanden openen…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "URI openen…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Voorkeuren" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Sneltoetsen" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Over Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Snelheid" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normaal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Algemeen" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Sneltoetsen tonen" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Beeldvullende modus aan/uit" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Dubbeltikken/Dubbelklikken" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Beeldvullende modus verlaten" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "OSD tonen (alleen in beeldvullende modus)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tikken" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Afsluiten" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Media" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Bestanden openen" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "URI openen" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Afspeellijst" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Volgend item" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Dubbeltikken (rechterkant)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Vorig item" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Dubbeltikken (linkerkant)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Herhaalmodus wijzigen" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exporteren naar bestand" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Afspelen" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Afspelen/Pauzeren" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Lang ingedrukt houden/Rechtsklikken" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Vooruitspoelen" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Naar rechts vegen/scrollen" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Terugspoelen" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Naar links vegen/scrollen" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Volume omhoog" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Omhoog vegen/scrollen" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Volume omlaag" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Omlaag vegen/scrollen" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Geluid dempen" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Volgend hoofdstuk" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Vorig hoofdstuk" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Decodering" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Terug naar voorkeuren" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Gedrag" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Beeldvullende modus automatisch inschakelen" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Schakel de beeldvullende modus automatisch in als de afspeellijst wordt vervangen (behalve in de zwevende modus)" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Vragen om onlangs afgespeelde media te hervatten" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Zweven op alle werkbladen" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Deze optie werkt alleen op GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Actie na het afspelen:" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Niets doen" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Laatste frame bevriezen" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Toepassing afsluiten" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volume" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Aangepaste initiële waarde" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Stel een aangepast volumeniveau in bij het opstarten in plaats van het vorige te herstellen" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Volumeniveau" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Spoelen" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modus" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Nauwkeurig" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Snel" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Eenheid" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Seconde" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuut" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Percentage" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Waarde" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Verschuiving (in milliseconden)" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Alleen ingebouwde audioformaten" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Ondertiteling" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Standaardlettertype" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Netwerk" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Client" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Progressieve downloadbuffering" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Server" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Speler op afstand bedienen" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Luisteren op poort:" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Aanpassingen" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Vormgeving" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Donker thema" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Vensterschaduwen tonen" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Schakel uit om de prestaties te verbeteren in de zwevende modus" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Plug-involgorde" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Pas de standaardvolgorde van GStreamer-plug-ins aan" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "playbin3 gebruiken" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Herstart de speler om de wijziging toe te passen" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimenteel" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "PipeWire gebruiken voor audio-uitvoer" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Decoder: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Voer een uri in of sleep een uri hierheen" #: src/dialogs.js:157 msgid "Cancel" msgstr "Annuleren" #: src/dialogs.js:158 msgid "Open" msgstr "Openen" #: src/dialogs.js:226 msgid "Title" msgstr "Titel" #: src/dialogs.js:227 msgid "Completed" msgstr "Voltooid" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Afspelen hervatten?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTK versie: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaita-versie: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer-versie: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS-versie: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Een door GStreamer aangedreven GNOME-mediaspeler" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Heimen Stoffels " #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Eindigt op: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "onbekend" #: src/widget.js:242 msgid "Channels" msgstr "Kanalen" #: src/widget.js:260 msgid "Disabled" msgstr "Uitgeschakeld" clapper-0.5.2/po/no.po000066400000000000000000000164701425527005600145540ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Norwegian\n" "Language: no_NO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: no\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/pl.po000066400000000000000000000227611425527005600145530ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Polish\n" "Language: pl_PL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: pl\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Otwórz pliki…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Otwórz URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Preferencje" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Skróty" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "O programie" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Prędkość" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normalny" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Główne" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Pokaż skróty" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Przełącz tryb pełnoekranowy" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Podwójne dotknięcie | Podwójne kliknięcie" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Opuść pełny ekran" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Pokaż OSD (tylko w trybie pełnoekranowym)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Stuknij" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Zamknij" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Media" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Otwórz pliki" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Otwórz URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Playlista" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Następna pozycja" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Dwukrotne dotknięcie (po prawej)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Poprzednia pozycja" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Podwójne dotknięcie (po lewej stronie)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Zmień tryb powtarzania" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Eksportuj do pliku" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Odtwarzanie" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Przełącz odtwarzanie" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Długie naciśnięcie | Prawy przycisk" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Przewiń do przodu" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Przesuń w prawo | Przewiń w prawo" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Przewiń do tyłu" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Przesuń w lewo | Przewiń w lewo" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Zwiększ głośność" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Przesuń w górę | Przewiń w górę" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Zmniejsz głośność" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Przesuń w dół | Przewiń w dół" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Przełącz wyciszenie" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Następny rozdział" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Poprzedni rozdział" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Dekodery" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Wróć do preferencji" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Zachowanie" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Automatyczny tryb pełnoekranowy" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Wejdź do trybu pełnoekranowego, gdy lista odtwarzania zostanie zastąpiona z wyjątkiem trybu pływającego" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Pytaj o wznowienie ostatnich mediów" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Pływaj na wszystkich obszarach roboczych" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Ta opcja działa tylko na GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Po odtwarzaniu" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Nic nie rób" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Zamroź ostatnią klatkę" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Zamknij aplikację" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Głośność" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Wartość początkowa" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Ustaw własną głośność początkową zamiast ją przywracać" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Procent głośności" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Przewijanie" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Tryb" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Dokładny" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Szybki" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Jednostka" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Sekunda" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuta" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Procent" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Wartość" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Przesunięcie w milisekundach" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Tylko natywne formaty audio" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Napisy" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Domyślna czcionka" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Sieć" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Klient" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Buforuj pobierając progresywnie" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Serwer" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Zdalne sterowanie odtwarzaczem" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Port połączeń przychodzących" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Dostrajanie" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Wygląd" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Ciemny motyw" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Renderuj cienie okna" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Wyłącz, aby zwiększyć wydajność w trybie okienkowym" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Ranking pluginów" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Zmień domyślne rangi pluginów GStreamera" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Użyj playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Wymaga restartu odtwarzacza" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Eksperyment" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Użyj PipeWire do wyprowadzenia dźwięku" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Dekoder: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Wprowadź lub upuść URI tutaj" #: src/dialogs.js:157 msgid "Cancel" msgstr "Anuluj" #: src/dialogs.js:158 msgid "Open" msgstr "Otwórz" #: src/dialogs.js:226 msgid "Title" msgstr "Tytuł" #: src/dialogs.js:227 msgid "Completed" msgstr "Ukończono" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Wznowić odtwarzanie?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Wersja GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Wersja Adwaity: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Wersja GStreamera: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Wersja GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Odtwarzacz multimedialny dla GNOME zasilany przez GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Rafał Dzięgiel " #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Koniec o: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Nieokreślony" #: src/widget.js:242 msgid "Channels" msgstr "Kanały" #: src/widget.js:260 msgid "Disabled" msgstr "Wyłączony" clapper-0.5.2/po/pt.po000066400000000000000000000226541425527005600145640ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Portuguese\n" "Language: pt_PT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: pt-PT\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Abrir ficheiros…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Abrir URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Preferências" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Atalhos" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Sobre o Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Velocidade" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Predefinido" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Geral" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Mostrar atalhos" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Mudar modo de ecrã" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Toque duplo duplo Clique duplo" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Sair do modo de ecrã completo" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Revelar OSD (apenas em tela cheia)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tocar" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Sair" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Multimédia" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Abrir ficheiro" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Abrir URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Lista de reprodução" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Próximo item" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Toque duplo (lado direito)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Item anterior" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Toque duplo (lado esquerdo)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Alterar modo de repetição" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exportar para ficheiro" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Reproduzir" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Alternar reprodução" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Toque longo | Clique com o botão direito" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Procurar para a frente" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Deslizar para a direita | Deslocar para direita" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Procurar para trás" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Deslizar para a esquerda | Deslocar para a esquerda" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Aumentar o volume" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Deslizar para cima | Deslocar para cima" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Diminuir o volume" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Deslizar para baixo | Deslocar para baixo" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Ativar/Desativar Som" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Capítulo seguinte" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Capítulo anterior" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Descodificadores" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Voltar para as preferências" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Comportamento" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Ecrã inteiro automático" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Entrar em ecrã inteiro quando a lista de reprodução é substituída, excepto no modo flutuante" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Pedir para retomar o ficheiro recente" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Flutuar em todas as áreas de trabalho" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Esta opção apenas funciona no GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Após reprodução" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Não fazer nada" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Suster o fotograma" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Fechar a aplicação" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volume" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Valor inicial personalizado" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Definir volume personalizado no arranque ao invés de restaurá-lo" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Percentagem de volume" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Procurando" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modo" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Preciso" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Rápido" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unidade" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Segundo" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuto" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Percentagem" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Valor" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Áudio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Deslocamento em milissegundos" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Apenas formatos de áudio nativos" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Legendas" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Tipo de letra predefinida" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Rede" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Cliente" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Buffering de transferência progressiva" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Servidor" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Controlar reprodutor remotamente" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Porta de escuta" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Ajustes" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Aparência" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Tema escuro" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Renderizar sombras das janelas" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Desativar para aumentar o desempenho quando em janela" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Classificação do plugin" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Altera as classificações predefinidas dos plugins GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Usar playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Requer o reinício do reprodutor" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimental" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Usar o PipeWire para a saída áudio" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Descodificador: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Introduzir ou largar o URI aqui" #: src/dialogs.js:157 msgid "Cancel" msgstr "Cancelar" #: src/dialogs.js:158 msgid "Open" msgstr "Abrir" #: src/dialogs.js:226 msgid "Title" msgstr "Título" #: src/dialogs.js:227 msgid "Completed" msgstr "Concluído" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Retomar a reprodução?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Versão do GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Versão do Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Versão do GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Versão do GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Um reprodutor multimédia GNOME desenvolvido por GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Hugo Carvalho " #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Termina às: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Indeterminado" #: src/widget.js:242 msgid "Channels" msgstr "Canais" #: src/widget.js:260 msgid "Disabled" msgstr "Desativado" clapper-0.5.2/po/pt_BR.po000066400000000000000000000224671425527005600151510ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Portuguese, Brazilian\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: pt-BR\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Abrir Arquivos" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Abrir URI" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Preferências" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Atalhos" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Sobre o Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Velocidade" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Geral" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Mostrar atalhos" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Tela Cheia" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Toque duplo duplo Clique duplo" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Sair de tela cheia" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Revelar OSD (apenas em tela cheia)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tocar" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Sair" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Mídia" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Abrir Arquivos" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Abrir o URL" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Lista de reprodução" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Próximo" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Toque duplo (lado direito)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Anterior" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Toque duplo (lado esquerdo)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Alternar modo de repetição" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exportar arquivo" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Reprodução" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Alternar video" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Toque longo (Clique com o botão direito)" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Avançar para frente" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Deslizar para a Direita Scroll para Direita" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Volta video" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Deslizar para a esquerda Scroll para a Esquerda" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Aumentar o volume" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Deslizar para cima Scroll para cima" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Diminuir o volume" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Deslizar para baixo ► Rolar para baixo" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Ativar/Desativar Som" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Próximo Capítulo" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Capítulo Anterior" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Decodificadores" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Voltar para as preferências" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Comportamento" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Tela cheia automaticamente" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Insira em tela cheia quando a lista de reprodução for substituída, exceto o modo flutuante" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Pedir para retomar o video recente" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Flutuar em todas as áreas de trabalho" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Esta opção só funciona no GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Pós reprodução" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Não fazer nada" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Congelar o último quadro" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Fechar aplicativo" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volume" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Valor inicial personalizado" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Definir volume personalizado na inicialização ao invés de restaurá-lo" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Percentual do volume" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Buscando" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Modo" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Preciso" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Rápido" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Unidade" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Segundo" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minuto" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Porcentagem" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Valor" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Audio" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "tempo de desvanecimento em milissegundos" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Apenas formatos de áudio nativos" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Legendas" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Fonte padrão" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Rede" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Cliente" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "buffering progressivo de download" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Servidor" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Controle o video remotamente " #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Porta de escuta" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Ajustes" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Aparência" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Tema Escuro" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Renderizar sombras das janelas" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Desabilite para aumentar o desempenho em placas gráficas mais fracas." #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Classificação do plugin" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Altera os ranks padrão dos plugins do GStreamer" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Usar playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Requer reinicialização do Clapper" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Experimental" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Usar PipeWire para saída de áudio" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Decodificador: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Digite ou solte o URI aqui" #: src/dialogs.js:157 msgid "Cancel" msgstr "Cancelar" #: src/dialogs.js:158 msgid "Open" msgstr "Abrir" #: src/dialogs.js:226 msgid "Title" msgstr "Título" #: src/dialogs.js:227 msgid "Completed" msgstr "Completado" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Retomar a reprodução?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Versão do GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaita versão:" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Versão do GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Versão do GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Um player de mídia GNOME desenvolvido por GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "KevenDoriaLinuxBR" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "termina em %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Indeterminado" #: src/widget.js:242 msgid "Channels" msgstr "Canais" #: src/widget.js:260 msgid "Disabled" msgstr "Desabilitado" clapper-0.5.2/po/ro.po000066400000000000000000000165431425527005600145610ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Romanian\n" "Language: ro_RO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: ro\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/ru.po000066400000000000000000000262521425527005600145650ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Russian\n" "Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: ru\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Открыть файлы…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Открыть URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Параметры" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Комбинации клавиш" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "О приложении" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Скорость" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Обычная" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Общие" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Комбинации клавиш" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Включить полноэкранный режим" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Двойное нажатие | Двойной клик" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Выйти из полноэкранного режима" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Показывать OSD (только в полноэкранном режиме)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Нажатие" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Выйти из приложения" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Медиа" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Открыть файлы" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Открыть URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Плейлист" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Следующий элемент" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Двойное нажатие (правая сторона)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Предыдущий элемент" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Двойное нажатие (левая сторона)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Изменить режим повторения" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Экспорт в файл" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Воспроизведение" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Управление воспроизведением" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Длительное нажатие | Щелкните правой кнопкой мыши" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Перемотка вперед" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Проведите вправо | Прокрутите вправо" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Перемотка назад" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Проведите влево | Прокрутите влево" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Увеличить громкость" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Проведите вверх | Прокрутите вверх" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Уменьшить громкость" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Проведите вниз | Прокрутить вниз" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Отключить звук" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Следующая глава" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Предыдущая глава" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Декодеры" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Вернуться к настройкам" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Поведение" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Автоматический полноэкранный режим" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Переход в полноэкранный режим при замене списка воспроизведения, кроме плавающего режима" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Спрашивать о возобновление недавние медиа" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Показывать «плавающий режим» на всех рабочих столах" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Эта опция работает только в GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "После воспроизведения" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Ничего не делать" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Заморозить последний кадр" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Закрыть приложение" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Громкость" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Пользовательское начальное значение" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Установите настраиваемую громкость при запуске вместо ее восстановления" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Процент громкости" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Перемотка" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Режим" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Точный" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Быстрый" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Ед. изм" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Секунды" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Минуты" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Проценты" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Значение" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Аудио" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Смещение в миллисекундах" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Только нативные аудиоформаты" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Субтитры" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Шрифт по умолчанию" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Сеть" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Клиент" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Прогрессивная буферизация загрузки" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Сервер" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Удаленное управление плеером" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Порт прослушивания" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Твики" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Внешний вид" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Тёмная тема" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Рендеринг оконных теней" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Отключить, чтобы повысить производительность в оконном режиме" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Рейтинг плагинов" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Изменить ранги плагинов GStreamer по умолчанию" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Использовать playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Требуется перезапуск плеера" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Экспериментально" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Использовать PipeWire для вывода звука" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Декодер: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Введите URI" #: src/dialogs.js:157 msgid "Cancel" msgstr "Отмена" #: src/dialogs.js:158 msgid "Open" msgstr "Открыть" #: src/dialogs.js:226 msgid "Title" msgstr "Заголовок" #: src/dialogs.js:227 msgid "Completed" msgstr "Завершенный" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Возобновить воспроизведение?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "Версия GTK: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Версия Adwaita: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "Версия GStreamer: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "Версия GJS: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "Медиаплеер для GNOME на базе GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "Игорь Дятлов" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Окончание: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Неопределенный" #: src/widget.js:242 msgid "Channels" msgstr "Каналы" #: src/widget.js:260 msgid "Disabled" msgstr "Отключено" clapper-0.5.2/po/sr.po000066400000000000000000000166131425527005600145630ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Serbian (Cyrillic)\n" "Language: sr_SP\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: sr\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/sv.po000066400000000000000000000221631425527005600145640ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Swedish\n" "Language: sv_SE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: sv-SE\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Öppna filer…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "Öppna URL…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Inställningar" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Tangentbordsgenvägar" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Om Clapper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Hastighet" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Allmänt" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Visa tangentbordsgenvägar" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Växla helskärmsläge" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Dubbeltryck | Dubbelklicka" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Lämna helskärmsläge" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Visa OSD (endast helskärmsläge)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Tryck" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Avsluta" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Media" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Öppna filer" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "Öppna URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Spellista" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Nästa föremål" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Dubbeltryck (höger sida)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Föregående föremål" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Dubbeltryck (vänster sida)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Ändra upprepningsläge" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Exportera till fil" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Uppspelning" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Spela/pausa" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Långtryck | Högerklicka" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "Spola framåt" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Svep höger | Skrolla höger" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Spola bakåt" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Svep vänster | Skrolla vänster" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Höj volymen" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Svep uppåt | Skrolla uppåt" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Sänk volymen" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Svep nedåt | Skrolla nedåt" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Växla ljudet på/av" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Nästa kapitel" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Föregående kapitel" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Avkodare" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Återgå till inställningarna" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Beteende" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Automatiskt helskärmsläge" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "Växla till fullskärmsläge när en spellista ersätts, förutom i flytande läge" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "Be att återuppta senaste media" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "Flyt på alla arbetsytor" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "Det här alternativet fungerar endast i GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "Efter uppspelning" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "Gör ingenting" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "Frys sista bildruta" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "Stäng appen" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "Volym" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "Anpassat startvärde" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "Ställ in anpassad volym vid start istället för att återställa den" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "Volymprocent" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "Spolning" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "Läge" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "Riktig" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "Snabbt" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "Enhet" #: ui/preferences-window.ui:97 msgid "Second" msgstr "Sekund" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "Minut" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "Procent" #: ui/preferences-window.ui:107 msgid "Value" msgstr "Värde" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "Ljud" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "Förskjutning i millisekunder" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "Endast inbyggda ljudformat" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "Undertexter" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "Standardteckensnitt" #: ui/preferences-window.ui:153 msgid "Network" msgstr "Nätverk" #: ui/preferences-window.ui:157 msgid "Client" msgstr "Klient" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "Progressiv nedladdningsbuffert" #: ui/preferences-window.ui:168 msgid "Server" msgstr "Server" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "Fjärrstyra spelaren" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "Lyssningsport" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "Tweaks" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "Utseende" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "Mörkt Tema" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "Rendera fönsterskuggor" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "Inaktivera för att öka prestanda när fönsterläge är på" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "Rangordning av plugin" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "Ändra standardrankningar för GStreamer-plugins" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "Använd playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "Kräver omstart av spelaren" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "Exprimentalt" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "Använd PipeWire för Ljudutgång" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "Avkodare: %s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "Ange eller släpp URI här" #: src/dialogs.js:157 msgid "Cancel" msgstr "Avbryt" #: src/dialogs.js:158 msgid "Open" msgstr "Öppna" #: src/dialogs.js:226 msgid "Title" msgstr "Titel" #: src/dialogs.js:227 msgid "Completed" msgstr "Klar" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "Återuppta uppspelningen?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTK version: %s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaita version: %s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer version: %s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS version: %s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "En media spelare för GNOME som drivs av GStreamer" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "SA ST (sastofficial)" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "Slutar vid: %s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "Obestämd" #: src/widget.js:242 msgid "Channels" msgstr "Kanaler" #: src/widget.js:260 msgid "Disabled" msgstr "Avstängd" clapper-0.5.2/po/tr.po000066400000000000000000000177511425527005600145700ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Turkish\n" "Language: tr_TR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: tr\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "Dosya Aç…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "URL Aç…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "Tercihler" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "Kısayollar" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "Clapper Hakkında" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "Hız" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "Normal" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "Genel" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "Kısayolları göster" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "Tam ekran" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "Çift dokun | Çift tıkla" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "Tam ekrandan çık" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "Pencere Düğmelerini Göster (yalnızca tam ekran)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "Dokun" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "Çık" #: ui/help-overlay.ui:47 msgid "Media" msgstr "Ortam" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "Dosyaları Aç" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "URL Aç" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "Oynatma listesi" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "Sonraki öğe" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "Çift tıkla (sağ tarafa)" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "Önceki öğe" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "Çift tıkla (sol tarafa)" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "Tekrarlı oynatma modunu değiştir" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "Dosyaya aktar" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "Oynatma" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "Oynatmayı başlat" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "Uzun bas | Sağ tıkla" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "İleri sar" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "Sağa sürükle | Sağa kaydır" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "Geri sar" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "Sola sürükle | Sola kaydır" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "Sesi artır" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "Yukarı sürükle | Yukarı kaydır" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "Sesi kıs" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "Aşağı sürükle | Aşağı kaydır" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "Sesi kapat" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "Sonraki parça" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "Önceki parça" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "Çözücüler" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "Tercihlere geri dön" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "Davranış" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "Otomatik tam ekran" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/uk.po000066400000000000000000000167351425527005600145630ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Ukrainian\n" "Language: uk_UA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: uk\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/vi.po000066400000000000000000000164621425527005600145570ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Vietnamese\n" "Language: vi_VN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: vi\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/po/zh_CN.po000066400000000000000000000204231425527005600151320ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Chinese Simplified\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: zh-CN\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "打开文件…" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "打开 URI…" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "选项" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "快捷键" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "关于 Claper" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "速度" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "一般" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "常规​​​​​" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "显示快捷键" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "切换全屏" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "离开全屏" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "显示 OSD(仅全屏)" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "退出" #: ui/help-overlay.ui:47 msgid "Media" msgstr "媒体" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "打开文件" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "打开 URI" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "播放列表" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "下一项" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "上一项" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "更改循环模式" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "导出至文件" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "播放" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "长按 | 右键点击" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "快进" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "快退" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "提高音量" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "向上滑动 | 向上滚动" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "降低音量" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "向下滑动 | 向下滚动" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "切换静音" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "解码器" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "返回首选项" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "行为" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "自动全屏" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "此选项仅适用于 GNOME" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "播放结束后" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "不执行任何操作" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "停留在最后一帧" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "关闭应用程序" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "音量" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "自定义初始值" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "音量百分比" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "定位播放" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "模式" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "精确" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "快速" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "单位" #: ui/preferences-window.ui:97 msgid "Second" msgstr "秒钟" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "分钟" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "百分比" #: ui/preferences-window.ui:107 msgid "Value" msgstr "值" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "音频" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "字幕" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "默认字体" #: ui/preferences-window.ui:153 msgid "Network" msgstr "网络" #: ui/preferences-window.ui:157 msgid "Client" msgstr "客户端" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "服务器" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "远程控制播放器" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "监听端口" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "调整" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "界面" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "暗色主题" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "渲染窗口阴影" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "使用 playbin3" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "需要重启播放器" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "实验性功能" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "使用 PipeWire 输出音频" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "解码器:%s" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "取消" #: src/dialogs.js:158 msgid "Open" msgstr "打开" #: src/dialogs.js:226 msgid "Title" msgstr "标题" #: src/dialogs.js:227 msgid "Completed" msgstr "已播放" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "继续播放?" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "GTK 版本:%s" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "Adwaita 版本:%s" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "GStreamer 版本:%s" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "GJS 版本:%s" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "刘韬" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "结束于:%s" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "声道" #: src/widget.js:260 msgid "Disabled" msgstr "禁用" clapper-0.5.2/po/zh_TW.po000066400000000000000000000164761425527005600152010ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: clapper\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-26 18:41+0200\n" "PO-Revision-Date: 2022-05-26 16:50\n" "Last-Translator: \n" "Language-Team: Chinese Traditional\n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Crowdin-Project: clapper\n" "X-Crowdin-Project-ID: 473374\n" "X-Crowdin-Language: zh-TW\n" "X-Crowdin-File: /master/po/com.github.rafostar.Clapper.pot\n" "X-Crowdin-File-ID: 31\n" #: ui/clapper.ui:6 msgid "Open Files…" msgstr "" #: ui/clapper.ui:10 msgid "Open URI…" msgstr "" #: ui/clapper.ui:16 ui/preferences-window.ui:4 msgid "Preferences" msgstr "" #: ui/clapper.ui:20 msgid "Shortcuts" msgstr "" #: ui/clapper.ui:26 msgid "About Clapper" msgstr "" #: ui/elapsed-time-button.ui:27 msgid "Speed" msgstr "" #: ui/elapsed-time-button.ui:41 ui/preferences-window.ui:82 msgid "Normal" msgstr "" #: ui/help-overlay.ui:10 ui/preferences-window.ui:11 msgid "General" msgstr "" #: ui/help-overlay.ui:13 msgid "Show shortcuts" msgstr "" #: ui/help-overlay.ui:19 msgid "Toggle fullscreen" msgstr "" #: ui/help-overlay.ui:20 msgid "Double tap | Double click" msgstr "" #: ui/help-overlay.ui:26 msgid "Leave fullscreen" msgstr "" #: ui/help-overlay.ui:32 msgid "Reveal OSD (fullscreen only)" msgstr "" #: ui/help-overlay.ui:33 msgid "Tap" msgstr "" #: ui/help-overlay.ui:39 msgid "Quit" msgstr "" #: ui/help-overlay.ui:47 msgid "Media" msgstr "" #: ui/help-overlay.ui:50 msgid "Open files" msgstr "" #: ui/help-overlay.ui:56 src/dialogs.js:137 msgid "Open URI" msgstr "" #: ui/help-overlay.ui:64 msgid "Playlist" msgstr "" #: ui/help-overlay.ui:67 msgid "Next item" msgstr "" #: ui/help-overlay.ui:68 msgid "Double tap (right side)" msgstr "" #: ui/help-overlay.ui:74 msgid "Previous item" msgstr "" #: ui/help-overlay.ui:75 msgid "Double tap (left side)" msgstr "" #: ui/help-overlay.ui:81 msgid "Change repeat mode" msgstr "" #: ui/help-overlay.ui:87 msgid "Export to file" msgstr "" #: ui/help-overlay.ui:95 ui/preferences-window.ui:118 msgid "Playback" msgstr "" #: ui/help-overlay.ui:98 msgid "Toggle play" msgstr "" #: ui/help-overlay.ui:99 msgid "Long press | Right click" msgstr "" #: ui/help-overlay.ui:105 msgid "Seek forward" msgstr "" #: ui/help-overlay.ui:106 msgid "Swipe right | Scroll right" msgstr "" #: ui/help-overlay.ui:112 msgid "Seek backward" msgstr "" #: ui/help-overlay.ui:113 msgid "Swipe left | Scroll left" msgstr "" #: ui/help-overlay.ui:119 msgid "Volume up" msgstr "" #: ui/help-overlay.ui:120 msgid "Swipe up | Scroll up" msgstr "" #: ui/help-overlay.ui:126 msgid "Volume down" msgstr "" #: ui/help-overlay.ui:127 msgid "Swipe down | Scroll down" msgstr "" #: ui/help-overlay.ui:133 msgid "Toggle mute" msgstr "" #: ui/help-overlay.ui:139 msgid "Next chapter" msgstr "" #: ui/help-overlay.ui:145 msgid "Previous chapter" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:11 msgid "Decoders" msgstr "" #: ui/preferences-plugin-ranking-subpage.ui:18 msgid "Return to the preferences" msgstr "" #: ui/preferences-window.ui:15 msgid "Behavior" msgstr "" #: ui/preferences-window.ui:18 msgid "Auto fullscreen" msgstr "" #: ui/preferences-window.ui:19 msgid "Enter fullscreen when playlist is replaced except floating mode" msgstr "" #: ui/preferences-window.ui:25 msgid "Ask to resume recent media" msgstr "" #: ui/preferences-window.ui:31 msgid "Float on all workspaces" msgstr "" #: ui/preferences-window.ui:32 msgid "This option only works on GNOME" msgstr "" #: ui/preferences-window.ui:38 msgid "After playback" msgstr "" #: ui/preferences-window.ui:43 msgid "Do nothing" msgstr "" #: ui/preferences-window.ui:44 msgid "Freeze last frame" msgstr "" #: ui/preferences-window.ui:45 msgid "Close the app" msgstr "" #: ui/preferences-window.ui:55 msgid "Volume" msgstr "" #: ui/preferences-window.ui:58 msgid "Custom initial value" msgstr "" #: ui/preferences-window.ui:59 msgid "Set custom volume at startup instead of restoring it" msgstr "" #: ui/preferences-window.ui:63 msgid "Volume percentage" msgstr "" #: ui/preferences-window.ui:74 msgid "Seeking" msgstr "" #: ui/preferences-window.ui:77 msgid "Mode" msgstr "" #: ui/preferences-window.ui:83 msgid "Accurate" msgstr "" #: ui/preferences-window.ui:84 msgid "Fast" msgstr "" #: ui/preferences-window.ui:92 msgid "Unit" msgstr "" #: ui/preferences-window.ui:97 msgid "Second" msgstr "" #: ui/preferences-window.ui:98 msgid "Minute" msgstr "" #: ui/preferences-window.ui:99 msgid "Percentage" msgstr "" #: ui/preferences-window.ui:107 msgid "Value" msgstr "" #: ui/preferences-window.ui:122 msgid "Audio" msgstr "" #: ui/preferences-window.ui:125 msgid "Offset in milliseconds" msgstr "" #: ui/preferences-window.ui:132 msgid "Only native audio formats" msgstr "" #: ui/preferences-window.ui:140 msgid "Subtitles" msgstr "" #: ui/preferences-window.ui:143 msgid "Default font" msgstr "" #: ui/preferences-window.ui:153 msgid "Network" msgstr "" #: ui/preferences-window.ui:157 msgid "Client" msgstr "" #: ui/preferences-window.ui:160 msgid "Progressive download buffering" msgstr "" #: ui/preferences-window.ui:168 msgid "Server" msgstr "" #: ui/preferences-window.ui:171 msgid "Control player remotely" msgstr "" #: ui/preferences-window.ui:175 msgid "Listening port" msgstr "" #: ui/preferences-window.ui:188 msgid "Tweaks" msgstr "" #: ui/preferences-window.ui:192 msgid "Appearance" msgstr "" #: ui/preferences-window.ui:195 msgid "Dark theme" msgstr "" #: ui/preferences-window.ui:201 msgid "Render window shadows" msgstr "" #: ui/preferences-window.ui:202 msgid "Disable to increase performance when windowed" msgstr "" #: ui/preferences-window.ui:213 msgid "Plugin ranking" msgstr "" #: ui/preferences-window.ui:214 msgid "Alter default ranks of GStreamer plugins" msgstr "" #: ui/preferences-window.ui:219 msgid "Use playbin3" msgstr "" #: ui/preferences-window.ui:220 ui/preferences-window.ui:229 msgid "Requires player restart" msgstr "" #: ui/preferences-window.ui:222 ui/preferences-window.ui:231 msgid "Experimental" msgstr "" #: ui/preferences-window.ui:228 msgid "Use PipeWire for audio output" msgstr "" #: src/buttons.js:201 #, javascript-format msgid "Decoder: %s" msgstr "" #: src/dialogs.js:152 msgid "Enter or drop URI here" msgstr "" #: src/dialogs.js:157 msgid "Cancel" msgstr "" #: src/dialogs.js:158 msgid "Open" msgstr "" #: src/dialogs.js:226 msgid "Title" msgstr "" #: src/dialogs.js:227 msgid "Completed" msgstr "" #: src/dialogs.js:235 msgid "Resume playback?" msgstr "" #: src/dialogs.js:289 #, javascript-format msgid "GTK version: %s" msgstr "" #: src/dialogs.js:290 #, javascript-format msgid "Adwaita version: %s" msgstr "" #: src/dialogs.js:291 #, javascript-format msgid "GStreamer version: %s" msgstr "" #: src/dialogs.js:292 #, javascript-format msgid "GJS version: %s" msgstr "" #: src/dialogs.js:300 msgid "A GNOME media player powered by GStreamer" msgstr "" #. TRANSLATORS: Put your name(s) here for credits or leave untranslated #: src/dialogs.js:305 msgid "translator-credits" msgstr "" #: src/revealers.js:170 #, javascript-format msgid "Ends at: %s" msgstr "" #: src/widget.js:226 src/widget.js:235 src/widget.js:241 src/widget.js:247 msgid "Undetermined" msgstr "" #: src/widget.js:242 msgid "Channels" msgstr "" #: src/widget.js:260 msgid "Disabled" msgstr "" clapper-0.5.2/src/000077500000000000000000000000001425527005600137415ustar00rootroot00000000000000clapper-0.5.2/src/actions.js000066400000000000000000000064611425527005600157460ustar00rootroot00000000000000const { 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: null, 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; } } clapper-0.5.2/src/app.js000066400000000000000000000063711425527005600150660ustar00rootroot00000000000000const { 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); } }); clapper-0.5.2/src/buttons.js000066400000000000000000000137761425527005600160130ustar00rootroot00000000000000const { 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`; } }); clapper-0.5.2/src/controls.js000066400000000000000000000372431425527005600161530ustar00rootroot00000000000000const { 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); } }); clapper-0.5.2/src/dbus.js000066400000000000000000000024111425527005600152320ustar00rootroot00000000000000const { 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); } ); } clapper-0.5.2/src/debug.js000066400000000000000000000022011425527005600153600ustar00rootroot00000000000000const { 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); } clapper-0.5.2/src/dialogs.js000066400000000000000000000212331425527005600157220ustar00rootroot00000000000000const { 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(); } }); clapper-0.5.2/src/fileOps.js000066400000000000000000000151521425527005600157040ustar00rootroot00000000000000const { 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()); }); } clapper-0.5.2/src/headerbar.js000066400000000000000000000176031425527005600162230ustar00rootroot00000000000000const { 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; } }); clapper-0.5.2/src/main.js000066400000000000000000000012151425527005600152220ustar00rootroot00000000000000imports.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); } clapper-0.5.2/src/misc.js000066400000000000000000000126361425527005600152420ustar00rootroot00000000000000const { 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; } } clapper-0.5.2/src/player.js000066400000000000000000000511271425527005600156010ustar00rootroot00000000000000const { 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; } } }); clapper-0.5.2/src/playlist.js000066400000000000000000000225101425527005600161400ustar00rootroot00000000000000const { 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; } }); clapper-0.5.2/src/prefs.js000066400000000000000000000327761425527005600154350ustar00rootroot00000000000000const { 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(); } }); clapper-0.5.2/src/revealers.js000066400000000000000000000265151425527005600163000ustar00rootroot00000000000000const { 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); } }); clapper-0.5.2/src/webHelpers.js000066400000000000000000000012011425527005600163710ustar00rootroot00000000000000const { 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]; } clapper-0.5.2/src/webServer.js000066400000000000000000000064071425527005600162520ustar00rootroot00000000000000const { 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_local(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; } }); clapper-0.5.2/src/widget.js000066400000000000000000001006021425527005600155610ustar00rootroot00000000000000const { 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; } }); clapper-0.5.2/test/000077500000000000000000000000001425527005600141315ustar00rootroot00000000000000clapper-0.5.2/test/keyboard.js000077500000000000000000000043001425527005600162670ustar00rootroot00000000000000#!/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([]); clapper-0.5.2/ui/000077500000000000000000000000001425527005600135675ustar00rootroot00000000000000clapper-0.5.2/ui/clapper.ui000066400000000000000000000017271425527005600155630ustar00rootroot00000000000000
Open Files… app.open_local Open URI… app.open_uri
Preferences app.prefs Shortcuts app.shortcuts
About Clapper app.about
clapper-0.5.2/ui/elapsed-time-button.ui000066400000000000000000000042761425527005600200210ustar00rootroot00000000000000 vertical True True False True Speed horizontal bottom False 2 True center speed_adjustment 0.25× Normal 0.01 2 1 0.1 clapper-0.5.2/ui/help-overlay.ui000066400000000000000000000154301425527005600165400ustar00rootroot00000000000000 True app General Show shortcuts F1 <Ctrl>question 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 clapper-0.5.2/ui/popover-separator.ui000066400000000000000000000016221425527005600176170ustar00rootroot00000000000000 clapper-0.5.2/ui/preferences-plugin-ranking-subpage.ui000066400000000000000000000020541425527005600227770ustar00rootroot00000000000000 clapper-0.5.2/ui/preferences-window.ui000066400000000000000000000276641425527005600177530ustar00rootroot00000000000000 0 99 1 1 0 150 1 1 -1000 1000 25 1 1024 65535 1 1 1024 65535 1 1 clapper-0.5.2/ui/track-select-button.ui000066400000000000000000000024151425527005600200220ustar00rootroot00000000000000 vertical True True True True vertical clapper-0.5.2/ui/volume-button.ui000066400000000000000000000035231425527005600167510ustar00rootroot00000000000000 vertical True True vertical True top False True volume_adjustment 0% 100% 150% 0 1.5 0.05 0.05