pax_global_header00006660000000000000000000000064147541531320014517gustar00rootroot0000000000000052 comment=9d9589f3e42c88546b3489133f6b605b5ee4aa0e bino-2.5/000077500000000000000000000000001475415313200123145ustar00rootroot00000000000000bino-2.5/CMakeLists.txt000066400000000000000000000114771475415313200150660ustar00rootroot00000000000000# Copyright (C) 2022, 2023, 2024, 2025 # Martin Lambers # # Copying and distribution of this file, with or without modification, are # permitted in any medium without royalty provided the copyright notice and this # notice are preserved. This file is offered as-is, without any warranty. cmake_minimum_required(VERSION 3.12) set(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL) set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) set(CMAKE_AUTOMOC ON) project(Bino LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) # Required: Qt6 find_package(Qt6 6.6.0 REQUIRED COMPONENTS OpenGLWidgets Multimedia LinguistTools) # Optional: QVR for Virtual Reality support find_package(QVR 4.1.0 QUIET) if(QVR_FOUND) add_definitions(-DWITH_QVR) include_directories(${QVR_INCLUDE_DIRS}) link_directories(${QVR_LIBRARY_DIRS}) endif() # The executable add_executable(bino src/main.cpp src/version.hpp src/log.hpp src/log.cpp src/tools.hpp src/tools.cpp src/screen.hpp src/screen.cpp src/tiny_obj_loader.h src/modes.hpp src/modes.cpp src/metadata.hpp src/metadata.cpp src/playlist.hpp src/playlist.cpp src/videoframe.hpp src/videoframe.cpp src/videosink.hpp src/videosink.cpp src/bino.hpp src/bino.cpp src/qvrapp.hpp src/qvrapp.cpp src/widget.hpp src/widget.cpp src/commandinterpreter.hpp src/commandinterpreter.cpp src/playlisteditor.hpp src/playlisteditor.cpp src/gui.hpp src/gui.cpp src/urlloader.hpp src/urlloader.cpp src/digestiblemedia.hpp src/digestiblemedia.cpp src/appicon.rc) qt6_add_translations(bino TS_FILES i18n/bino_de.ts i18n/bino_ka.ts i18n/bino_zh.ts) qt6_add_resources(bino "misc" PREFIX "/" FILES src/shader-color.vert.glsl src/shader-color.frag.glsl src/shader-view.vert.glsl src/shader-view.frag.glsl src/shader-display.vert.glsl src/shader-display.frag.glsl src/shader-vrdevice.vert.glsl src/shader-vrdevice.frag.glsl res/bino-logo-small.svg res/bino-logo-small-512.png) set_target_properties(bino PROPERTIES WIN32_EXECUTABLE TRUE) target_link_libraries(bino PRIVATE Qt6::OpenGLWidgets Qt6::Multimedia ${QVR_LIBRARIES}) install(TARGETS bino RUNTIME DESTINATION bin) # The manual and man page (optional, only if pandoc is found) find_program(PANDOC NAMES pandoc DOC "pandoc executable") if(PANDOC) add_custom_command(OUTPUT "${CMAKE_BINARY_DIR}/bino-manual.html" COMMAND ${PANDOC} "-s" "-t" "html" "--toc" "--css" "bino-manual.css" "${CMAKE_SOURCE_DIR}/doc/bino-manual.md" "-o" "${CMAKE_BINARY_DIR}/bino-manual.html" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" DEPENDS "${CMAKE_SOURCE_DIR}/doc/bino-manual.md" COMMENT "Generating HTML manual with pandoc" VERBATIM) add_custom_target(manual ALL DEPENDS "${CMAKE_BINARY_DIR}/bino-manual.html") install(FILES "doc/bino-manual.css" "${CMAKE_BINARY_DIR}/bino-manual.html" DESTINATION share/doc/bino) add_custom_command(OUTPUT "${CMAKE_BINARY_DIR}/bino.1" COMMAND ${PANDOC} "-s" "-t" "man" "${CMAKE_SOURCE_DIR}/doc/bino-manual.md" "-o" "${CMAKE_BINARY_DIR}/bino.1" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" DEPENDS "${CMAKE_SOURCE_DIR}/doc/bino-manual.md" COMMENT "Generating man page with pandoc" VERBATIM) add_custom_target(manpage ALL DEPENDS "${CMAKE_BINARY_DIR}/bino.1") install(FILES "${CMAKE_BINARY_DIR}/bino.1" DESTINATION share/man/man1) endif() # Add auxiliary files for Linux-ish systems if(UNIX) install(FILES "res/bino-logo-small-16.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/16x16/apps) install(FILES "res/bino-logo-small-22.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/22x22/apps) install(FILES "res/bino-logo-small-32.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/32x32/apps) install(FILES "res/bino-logo-small-48.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/48x48/apps) install(FILES "res/bino-logo-small-64.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/64x64/apps) install(FILES "res/bino-logo-small-128.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/128x128/apps) install(FILES "res/bino-logo-small-256.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/256x256/apps) install(FILES "res/bino-logo-small-512.png" RENAME "org.bino3d.bino.png" DESTINATION share/icons/hicolor/512x512/apps) install(FILES "res/bino-logo-small.svg" RENAME "org.bino3d.bino.svg" DESTINATION share/icons/hicolor/scalable/apps) install(FILES "res/org.bino3d.bino.desktop" DESTINATION share/applications) install(FILES "res/org.bino3d.bino.metainfo.xml" DESTINATION share/metainfo) endif() if (QVR_FOUND) message(STATUS "Build Bino with QVR support: YES") else() message(STATUS "Build Bino with QVR support: NO") endif() if (PANDOC) message(STATUS "Build manual and man page with pandoc: YES") else() message(STATUS "Build manual and man page with pandoc: NO") endif() bino-2.5/LICENSE.md000066400000000000000000001041441475415313200137240ustar00rootroot00000000000000### 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 . bino-2.5/NEWS.md000066400000000000000000000113431475415313200134140ustar00rootroot00000000000000Version 2.5: - Improved meta data detection via ffprobe (if available). Version 2.4: - Added support for HDR video input. - Added support for zooming in surround videos. - Increased minimum Qt version to 6.6. Version 2.3: - Added support for slideshow mode. - Improved support for image file formats, including MPO. - Added new capabilities to `--vr-screen` for easy configuration of two-screen stereo setups. - Made `--opengles` the default on ARM platforms. - Lowered minimum Qt version to 6.4. - Lowered minimum OpenGL ES requirements. Version 2.2: - Added support for capturing windows and screens. - Added support for HighDPI output. - Added support for 3D HDMI frame-pack output mode. - Improved performance in multi-host/multi-GPU Virtual Reality setups. - Improved compatibility for various OpenGL implementations. Version 2.1: - Added support for loading/saving/editing play lists, with loop modes off/one/all. - Added support for 180° videos, with and without 3D. - Added 3D output modes left-right(-half), right-left(-half), top-bottom(-half), bottom-top(-half). - Added support for rendering controller models in VR mode. - Improved GUI menu structure. - Fixed color space conversion for several pixel formats, mostly used by image file formats. - Fixed various scripting mode problems. Version 2.0: - This is a complete rewrite of Bino. All code is new and based on Qt6: - Builds with CMake instead of autoconf - Media input is handled by Qt Multimedia instead of FFmpeg and libass - Media output is handled by Qt abstractions instead of OpenAL, GLEW and plain OpenGL - Virtual Reality support is handled by QVR instead of Equalizer - Translations use Qt mechanisms instead of gettext - New features: - Support for 360° videos, with and without 3D - Simpler and more powerful VR mode, with autodetection of SteamVR - Support for both desktop OpenGL and OpenGL ES - GPU accelerated video decoding - Changed features: - Scripting commands have changed, see the manual - Multi-screen fullscreen now always requires VR mode, but is simple to set up with QVR. - LIRC support is not built in anymore but can be achieved by writing scripting commands to a named pipe - Removed features that might be added back if there is enough interest: - Cropping and zooming - Parallax adjustment - Crosstalk ghostbusting - Removed features that depend on support being added to Qt Multimedia: - Advanced subtitle formats - Separate left/right video tracks Version 1.6.8: - Improved support for using multiple cameras - New options and script commands to set a vertical pixel shift Version 1.6.7: - Fixed a loop mode problem where the beginning of a video was skipped. - Fixed building with latest FFmpeg - OpenGL stereo output mode is re-enabled with Qt >= 5.10 Version 1.6.6: - Fixed the preferences dialog for multi-display fullscreen mode. - Work with plain libglew again, without libglewmx. Version 1.6.5: - Fixed a bug that prevented Bino from working correctly in some languages. Version 1.6.4: - Current FFmpeg libraries are supported. - Build problems were fixed. - A few bugs were fixed. - Current Qt versions are supported, but "OpenGL stereo" support is disabled with Qt >= 5.7 until a proper fix is found. Version 1.6.3: - Build problems were fixed. Version 1.6.2: - The preferences dialogues were restructured. - Qt5 is now the default instead of Qt4. Version 1.6.0: - Support for Qt5 (but Qt4 remains the default for now). - New option to force a source aspect ratio. - Support for newer versions of FFmpeg/Libav and Equalizer. Version 1.4.0: - Support for the new output mode "Left/right view alternating", also known as "frame sequential". This is intended for 120Hz active stereo projectors and displays when "OpenGL stereo" is not available. - Better support for audio control. Volume, mute, and delay can be adjusted, and the output device can be selected. - Support for scripting via script files or named pipes. - Much improved support for older graphics cards. - Support for video output via SDI on NVIDIA Quadro cards. - Automatic support for high precision color input and output (30 bits per pixel). - Single-frame stepping via the '.' key. - An adjustable zoom mode for videos that are wider than the screen. - Support for opening multiple input devices, and for requesting MJPEG data from input devices. - Support for the MPO, JPS, and PNS file formats for stereoscopic images. - Support for DLP 3-D Ready Sync. - Various user interface tweaks, including support for multimedia keyboards and a "Recent Files" section in the File menu. Version 1.2.0: - Flexible fullscreen mode with support for multiple screens. - Support for remote controls via LIRC. - Support for camera devices. - A loop mode. - User interface improvements. bino-2.5/README.md000066400000000000000000000012061475415313200135720ustar00rootroot00000000000000# Bino: a 3D video player Bino is a video player with a focus on 3D and Virtual Reality: - Support for 3D videos in various formats - Support for 360° and 180° videos, with and without 3D - Support for 3D displays with various modes - Support for Virtual Reality environments, including SteamVR, CAVEs, powerwalls, and other multi-display / multi-GPU / multi-host systems Bino is based on [Qt](https://www.qt.io/). The optional Virtual Reality and multi-GPU support is based on [QVR](https://marlam.de/qvr/). No other libraries are required. See [bino3d.org](https://bino3d.org/) and the [manual](https://bino3d.org/bino-manual.html). bino-2.5/doc/000077500000000000000000000000001475415313200130615ustar00rootroot00000000000000bino-2.5/doc/bino-manual.css000066400000000000000000000013051475415313200157740ustar00rootroot00000000000000/* General */ body { background: #ffffff; margin: 0; line-height: 1.5em; color: #333333; font-family: helvetica, arial, sans-serif; max-width: 50em; margin-left: 1em; margin-right: auto; padding: 0 0.5em; } h1, h2, h3, h4, h5, h6 { margin-bottom: 0; line-height: 1.2em; margin-top: 1em; } a:link, a:visited { color: #0000e0; text-decoration: none } a:hover, a:active { color: #0000ff; text-decoration: underline } /* Dark mode */ @media (prefers-color-scheme: dark) { body { background: #333333; color: #cccccc; } a:link, a:visited { color: #7777e0; } a:hover, a:active { color: #9999ff; } } bino-2.5/doc/bino-manual.md000066400000000000000000000255241475415313200156150ustar00rootroot00000000000000--- title: Bino header: Version 2.5 date: Feburary 15, 2025 section: 1 --- # Overview Bino is a video player with a focus on 3D and Virtual Reality: - Support for stereoscopic 3D videos in various formats - Support for 360° and 180° surround videos, with and without stereoscopic 3D - Support for Virtual Reality environments, including SteamVR, CAVEs, powerwalls, and other multi-display / multi-GPU / multi-host systems # Invocation `bino` [*options*] *URL*... - `-h`, `--help` Displays help on command line options. - `--help-all` Displays help including Qt specific options. - `-v`, `--version` Displays version information. - `--log-level` *level* Set log level (fatal, warning, info, debug, firehose). - `--log-file` *file* Set log file. - `--read-commands` *script* Read commands from a script file. See [Scripting]. - `--opengles` Use OpenGL ES instead of Desktop OpenGL. - `--stereo` Use OpenGL quad-buffered stereo in GUI mode. - `--vr` Start in Virtual Reality mode instead of GUI mode. See [Virtual Reality]. - `--vr-screen` *screen* Set VR screen geometry, either as the special values 'united' or 'intersected', or as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left), or as a an aspect ratio followed by the name of an OBJ file that contains the screen geometry with texture coordinates (example: '16:9,myscreen.obj'). - `--capture` Capture audio/video input from microphone and camera/screen/window. - `--list-audio-outputs` List audio outputs. - `--list-audio-inputs` List audio inputs. - `--list-video-inputs` List video inputs. - `--list-screen-inputs` List screen inputs. - `--list-window-inputs` List window inputs. - `--audio-output` *ao* Choose audio output via its index. - `--audio-input` *ai* Choose audio input via its index. Can be empty. - `--video-input` *vi* Choose video input via its index. - `--screen-input` *si* Choose screen input via its index. - `--window-input` *wi* Choose window input via its index. - `--list-tracks` List all video, audio and subtitle tracks in the media. - `--preferred-audio` *lang* Set preferred audio track language (en, de, fr, ...). - `--preferred-subtitle` *lang* Set preferred subtitle track language (en, de, fr, ...). Can be empty. - `--video-track` *track* Choose video track via its index. - `--audio-track` *track* Choose audio track via its index. - `--subtitle-track` *track* Choose subtitle track via its index. Can be empty. - `-p`, `--playlist` *file* Load playlist. - `-l`, `--loop` *mode* Set loop mode (off, one, all). - `-w`, `--wait` *mode* Set wait mode (off, on). - `-i`, `--input` *mode* Set input mode (mono, top-bottom, top-bottom-half, bottom-top, bottom-top-half, left-right, left-right-half, right-left, right-left-half, alternating-left-right, alternating-right-left). - `-o`, `--output` *mode* Set output mode (left, right, stereo, alternating, hdmi-frame-pack, left-right, left-right-half, right-left, right-left-half, top-bottom, top-bottom-half, bottom-top, bottom-top-half, even-odd-rows, even-odd-columns, checkerboard, red-cyan-dubois, red-cyan-full-color, red-cyan-half-color, red-cyan-monochrome, green-magenta-dubois, green-magenta-full-color, green-magenta-half-color, green-magenta-monochrome, amber-blue-dubois, amber-blue-full-color, amber-blue-half-color, amber-blue-monochrome, red-green-monochrome, red-blue-monochrome). - `--surround` *mode* Set surround mode (360, 180, off). - `--surround-vfov` *degrees* Set surround vertical field of view (default 50, range 5-115). - `-S`, `--swap-eyes` Swap left/right eye. - `-f`, `--fullscreen` Start in fullscreen mode. # Output modes Most output modes should be self explanatory, but there are some exceptions: - `stereo` requires OpenGL quad-buffered stereo support, typically limited to high-end graphics cards. - `alternating` tries to mimic stereo mode by displaying the left and right frames alternating, ideally at display speed. This is unreliable since Bino has no way of making sure that its output frames actually correspond to display output frames, but it might work, depending on your hardware and system setup. - `hdmi-frame-pack` is a special mode supported by some 3D TVs via HDMI 1.4a, where the left view is placed in the top part of a frame and the right view in the bottom part, and both parts are separated by a blank area that takes 1/49 of the vertical space. To use this mode, force your display output resolution into either 1280x1470 (720p 3D: 720+30+720=1470; 1470/49=30) or 1920x2205 (1080p 3D: 1080+45+1080=2205; 2205/49=45). - `even-odd-rows`, `even-odd-columns` and `checkerboard` are for (older) 3D TVs. # File Name Conventions Bino currently cannot detect the stereoscopic layout or the surround video mode from metadata because Qt does not provide that information. It therefore has to guess. Bino recognizes the following hints at the last part of the file name, just before the file name extension (.ext): - `*-tb.ext`, `*-ab.ext`: Input mode `top-bottom` - `*-tbh.ext`, `*-abq.ext`: Input mode `top-bottom-half` - `*-bt.ext`, `*-ba.ext`: Input mode `bottom-top` - `*-bth.ext`, `*-baq.ext`: Input mode `bottom-top-half` - `*-lr.ext`: Input mode `left-right` - `*-lrh.ext`, `*-lrq.ext`: Input mode `left-right-half` - `*-rl.ext`: Input mode `right-left` - `*-rlh.ext`, `*-rlq.ext`: Input mode `right-left-half` - `*-2d.ext`: Input mode `mono` Additionally, if the number `180` or `360` is part of the file name and separated by neighboring digits or letters by other characters, then the corresponding surround mode is assumed. # Scripting Bino can read commands from a script file and execute them via the option `--read-commands` *scriptfile*. This works both in GUI mode and in Virtual Reality mode. The script file can also be a named pipe so that you can have arbitrary remote control interfaces write commands into it as they come in. Empty lines and comment lines (which begin with `#`) are ignored. The following commands are supported: - `open` `[--input` *mode*`]` `[--surround` *mode*`]` `[--video-track` *vt*`]` `[--audio-track` *at*`]` `[--subtitle-track` *st*`]` *URL* Open the URL and start playing. The options have the same meaning as the corresponding command line options. - `capture` `[--audio-input` *ai*`]` `[--video-input` *vi*`]` `[--screen-input` *si*`]` `[--window-input` *wi*`]` Start capturing camera and microphone. The options have the same meaning as the corresponding command line options. - `play` Start playing. - `pause` Pause. - `toggle-pause` Switch between pause and play. - `stop` Stop playing. - `playlist-load` *playlist.m3u* Load the playlist. - `playlist-next` Switch to next playlist entry. - `playlist-prev` Switch to previous playlist entry. - `playlist-wait` *mode* Set wait mode (off, on). - `playlist-loop` *mode* Set loop mode (off, one, all). - `quit` Quit Bino. - `set-position` *p* Set the video position to *p*, where *p*=0 is the beginning and *p*=1 is the end. - `seek` *seconds* Seek the given amounts of seconds forward or, if the number of seconds is negative, backwards. - `wait` `stop`|*seconds* Wait until the video stops, or wait for the given number of seconds, before executing the next command. - `set-mute` `on`|`off` Set the volume mute status. - `toggle-mute` Switch between mute and unmute. - `set-volume` *vol* Set the volume level to *vol* (between 0 and 1). - `adjust-volume` *offset* Adjust the volume by the given amount (the final volume is clamped between 0 and 1). - `set-output-mode` *mode* Set the given output mode. See the command line option `--output` for a list of modes. - `set-surround-vfov` *degrees* Set surround vertical field of view (default 50, range 5-115). - `set-swap-eyes` `on`|`off` Set left/right eye swap. - `toggle-swap-eyes` Toggle left/right eye swap. - `set-fullscreen` `on`|`off` Set fullscreen mode. - `toggle-fullscreen` Toggle fullscreen mode. # Slideshows You can play slideshows of images (or videos) simply by making a playlist and switching on its *wait* status. This is the default whenever one or more of the files you open are images instead of videos; this works from the command line as well as from the GUI. With *wait* enabled, the next media in the playlist will only be displayed after you press the N key, or choose Playlist/Next from the menu. For automatic media switching based on predefined presentation times, use the scripting mode as in the following example: ``` set-fullscreen on playlist-load my-slideshow.m3u playlist-loop on playlist-wait on playlist-next wait 4 playlist-next wait 7 playlist-next wait 5 quit ``` # Virtual Reality Bino supports all sorts of Virtual Reality environments via [QVR](https://marlam.de/qvr): - When QVR is compiled just with Qt6, CAVEs and powerwalls and similar multi-display setups are supported, including multi-GPU and multi-host rendering. - When QVR is compiled with [VRPN](https://github.com/vrpn/vrpn), all sorts of tracking and interaction hardware for such systems are additionally supported. - When QVR is compiled with [OpenVR](https://github.com/ValveSoftware/openvr), SteamVR is additionally supported and automatically detected (e.g. HTC Vive). To start Bino in VR mode, use the option `--vr`. Bino will then display a screen in the virtual world, and the video will be displayed on that screen, unless the input is a surround video (360° or 180°), which will of course be displayed all around the viewer. The default is a 16:9 screen in a few meters distance from the viewer, but you can use the `--vr-screen` option to either define arbitrary planar screens via their bottom left, bottom right and top left corners, or to load arbitrary screen geometry from an OBJ file. The latter case is useful e.g. if you want Bino's virtual screen to coincide with a curved physical screen. The `--vr-screen` option also accepts the special values `united` and `intersected`. This will unite (or intersect) the 2D geometries of all VR windows at runtime. For example, use `--vr-screen=united --qvr-config=two-screen-stereo.qvr` for a two-screen stereo setup, where the left view goes on the first screen and the right view goes on the second screen. Bino uses QVRs default navigation, which may be based on autodetected controllers such as the HTC Vive controllers, or on tracking and interaction hardware configured via QVR for your VR system, or on the mouse and WASDQE keys if nothing else is available. Additional interaction in VR mode is currently limited to the same keyboard shortcuts that also work in GUI mode. That means you currently must specify the video to play on the command line, and have no way to pause, skip or seek with VR controllers. This will be added in a future version. bino-2.5/i18n/000077500000000000000000000000001475415313200130735ustar00rootroot00000000000000bino-2.5/i18n/bino_de.ts000066400000000000000000001522551475415313200150540ustar00rootroot00000000000000 Bino Media player error: %1 Medienwiedergabefehler: %1 CommandInterpreter Cannot open %1: %2 Cannot open %1: %2) Kann %1 nicht öffnen: %2 Cannot read command from %1 Kann kein Kommando von %1 lesen Invalid argument in %1 line %2 Ungültiges Argument in %1 Zeile %2 %1: %2 %1: %2 Invalid command %1 line %2 Invalid command %1 Ungültiges Kommando in %1 Zeile %2 Gui &File &Datei &Open file... Datei &öffnen... Open &URL... &URL öffnen... Open &Camera... &Kamera öffnen... &Quit &Beenden &Tracks &Spuren &Playlist &Wiedergabeliste &Load... &Laden... &Save... &Speichern... &Edit... &Editieren... &Next &Nächster Eintrag &Previous &Vorheriger Eintrag &3D Modes &3D Modi 360° mode 360° Modus Input 2D Eingabe 2D Input top/bottom Eingabe oben/unten Input top/bottom half height Eingabe oben/unten halbhoch Input bottom/top Eingabe unten/oben Input bottom/top half height Eingabe unten/oben halbhoch Input left/right Eingabe links/rechts Input left/right half width Eingabe links/rechts halbbreit Input right/left Eingabe rechts/links Input right/left half width Eingabe rechts/links halbbreit Input alternating left/right Eingabe abwechselnd links/rechts Input alternating right/left Eingabe abwechselnd rechts/links Output left Ausgabe links Output right Ausgabe rechts Output OpenGL Stereo Ausgabe OpenGL Stereo Output alternating Ausgabe abwechselnd Output red/cyan high quality Ausgabe rot/cyan hohe Qualität Output red/cyan full color Ausgabe rot/cyan vollfarbig Output red/cyan half color Ausgabe rot/cyan halbfarbig Output red/cyan monochrome Ausgabe rot/cyan monochrom Output green/magenta high quality Ausgabe grün/magenta hohe Qualität Output green/magenta full color Ausgabe grün/magenta vollfarbig Output green/magenta half color Ausgabe grün/magenta halbfarbig Output green/magenta monochrome Ausgabe grün/magenta monochrom Output amber/blue high quality Ausgabe bernstein/blau hohe Qualität Output amber/blue full color Ausgabe bernstein/blau vollfarbig Output amber/blue half color Ausgabe bernstein/blau halbfarbig Output amber/blue monochrome Ausgabe bernstein/blau monochrom Output red/green monochrome Ausgabe rot/grün monochrom Output red/blue monochrome Ausgabe rot/blau monochrom Output even/odd rows Ausgabe gerade/ungerade Zeilen Output even/odd columns Ausgabe gerade/ungerade Spalten Output checkerboard Ausgabe Schachbrettmuster &Open File... Datei &öffnen... &Open File(s)... Datei(en) &öffnen... Open &Capture Device... &Aufnahmegerät öffnen... Input 3D above-below Eingabe 3D übereinander Input 3D side-by-side Eingabe 3D nebeneinander Input 3D alternating Eingabe 3D abwechselnd Output 3D side-by-side Ausgabe 3D nebeneinander Output 3D above-below Ausgabe 3D übereinander Output 3D interleaved Ausgabe 3D verwoben Output 3D anaglyph Ausgabe 3D Anaglyph &Media &Medium Mute audio Ton stumm Increase audio volume Ton lauter Decrease audio volume Ton leiser Pause Pause Seek forward 1 second Vorwärts spulen 1 Sekunde Seek backwards 1 second Rückwärts spulen 1 Sekunde Seek forward 10 seconds Vorwärts spulen 10 Sekunden Seek backwards 10 seconds Rückwärts spulen 10 Sekunden Seek forward 1 minute Vorwärts spulen 1 Minute Seek backwards 1 minute Rückwärts spulen 1 Minute Seek forward 10 minutes Vorwärts spulen 10 Minuten Seek backwards 10 minutes Rückwärts spulen 10 Minuten &View &Ansicht &Fullscreen &Vollbild &Swap eyes &Augen tauschen &Help &Hilfe &About... &Über... Error Fehler Open URL Öffne URL URL: URL: Cancel Abbrechen OK OK Open Capture Device Aufnahmegerät öffnen Camera Input: Kamera Eingabe: Open Camera Öffne Kamera Video Input: Video Eingabe: Default Standard Screen Input: Bildschirm Eingabe: Window Input: Fenster Eingabe: Audio Input: Ton Eingabe: None Keine Playlists (*.m3u) Wiedergabelisten (*.m3u) About Bino Über Bino Bino version %1 Bino Version %1 Copyright (C) %1 Martin Lambers Copyright (C) %1 Martin Lambers This is free software. You may redistribute copies of it under the terms of the <a href="http://www.gnu.org/licenses/gpl.html">GNU General Public License</a>. There is NO WARRANTY, to the extent permitted by law. Dieses Programm ist freie Software. Sie dürfen Kopien davon weitergeben gemäß der <a href="http://www.gnu.org/licenses/gpl.html">GNU General Public License</a>.Es gibt KEINERLEI GARANTIE, so weit das Gesetz es erlaubt. Video track %1 Videospur %1 (%1) (%1) Audio track %1 Tonspur %1 No subtitles Keine Untertitel Subtitle track %1 Untertitelspur %1 MetaData Cannot get meta data from %1: %2 Kann Metadaten von %1 nicht herausfinden: %2 Mode Input autodetection Eingabe Automatisch Input 2D Eingabe 2D Input top/bottom Eingabe oben/unten Input top/bottom half height Eingabe oben/unten halbhoch Input bottom/top Eingabe unten/oben Input bottom/top half height Eingabe unten/oben halbhoch Input left/right Eingabe links/rechts Input left/right half width Eingabe links/rechts halbbreit Input right/left Eingabe rechts/links Input right/left half width Eingabe rechts/links halbbreit Input alternating left/right Eingabe abwechselnd links/rechts Input alternating right/left Eingabe abwechselnd rechts/links Surround autodetection Surround Automatisch Surround off Surround Aus Surround 180° Surround 180° Surround 360° Surround 360° Output 2D left Ausgabe 2D links Output 2D right Ausgabe 2D rechts Output 3D OpenGL Stereo Ausgabe 3D OpenGL Stereo Output 3D alternating Ausgabe 3D abwechselnd Output HDMI frame pack Ausgabe HDMI Frame Pack Output left/right Ausgabe links/rechts Output left/right half width Ausgabe links/rechts halbbreit Output right/left Ausgabe rechts/links Output right/left half width Ausgabe rechts/links halbbreit Output top/bottom Ausgabe oben/unten Output top/bottom half height Ausgabe oben/unten halbhoch Output bottom/top Ausgabe unten/oben Output bottom/top half height Ausgabe unten/oben halbhoch Output even/odd rows Ausgabe gerade/ungerade Zeilen Output even/odd columns Ausgabe gerade/ungerade Spalten Output checkerboard Ausgabe Schachbrettmuster Output red/cyan high quality Ausgabe rot/cyan hohe Qualität Output red/cyan full color Ausgabe rot/cyan vollfarbig Output red/cyan half color Ausgabe rot/cyan halbfarbig Output red/cyan monochrome Ausgabe rot/cyan monochrom Output green/magenta high quality Ausgabe grün/magenta hohe Qualität Output green/magenta full color Ausgabe grün/magenta vollfarbig Output green/magenta half color Ausgabe grün/magenta halbfarbig Output green/magenta monochrome Ausgabe grün/magenta monochrom Output amber/blue high quality Ausgabe bernstein/blau hohe Qualität Output amber/blue full color Ausgabe bernstein/blau vollfarbig Output amber/blue half color Ausgabe bernstein/blau halbfarbig Output amber/blue monochrome Ausgabe bernstein/blau monochrom Output red/green monochrome Ausgabe rot/grün monochrom Output red/blue monochrome Ausgabe rot/blau monochrom Loop off Wiederholung aus Loop one Wiederholung eins Loop all Wiederholung alle Wait off Warten aus Wait on Warten an PlaylistEditor Edit Playlist Editiere Wiedergabeliste URL URL Input Mode Eingabe Modus Surround Mode Surround Modus Video Track Videospur Audio Track Audiospur Subtitle Track Untertitelspur Move up Hoch Move down Runter Add... Einfügen... Add Einfügen Remove Löschen Edit... Editieren... Done Fertig default Standard none Keine PlaylistEntryEditor Edit Playlist Entry Editiere Wiedergabelisteneintrag URL: URL: Set File... Setze Datei... Set URL... Setze URL... Input Mode: Eingabe Modus: Surround Mode: Surround Modus: Video Track: Videospur: Audio Track: Audiospur: Subtitle Track: Untertitelspur: Done Fertig Open URL Öffne URL Cancel Abbrechen OK OK default Standard none Keine (%1) (%1) QCommandLineParser 3D video player -- see https://bino3d.org 3D Videoplayer -- siehe https://bino3d.org Media to play. Medium zum Abspielen. Set log level (%1). Setze Logstufe (%1). Set log file. Setze Logdatei. Use OpenGL ES instead of Desktop OpenGL. Nutze OpenGL ES statt Desktop OpenGL. Use OpenGL quad-buffered stereo in GUI mode. Nutze OpenGL Stereo in der Benutzeroberfläche. Start in VR mode instead of GUI mode. Starte im VR Modus statt mit der Benutzerobefläche. Set VR screen geometry, either as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left) or as a name of an OBJ file that contains the screen geometry with texture coordinates. Setze die VR Bildschirmgeometrie, entweder als Liste aus neun mit Kommas abgegrenzten Werten die drei 3D Koordinaten repräsentieren welche einen flachen Bildschirm definieren (unten links, unten rechts, oben links) oder als Name einer OBJ Datei die die Bildschirmgeometrie und Texturkoordinaten enthält. Capture video/audio input from camera and microphone. Capture audio/video input from camera and microphone. Nehme Bild und Ton von Kamera und Mikrofon auf. List audio outputs. Liste die Tonausgabegeräte. List audio inputs. Liste die Toneingabegeräte. List video inputs. Liste die Bildeingabegeräte. Choose audio output via its index. Wähle eine Tonausgabe mit ihrem Index. Choose audio input via its index. Can be empty. Wähle eine Toneingabe mit ihrem Index. Kann leer sein. Choose video input via its index. Wähle eine Bildeingabe mit ihrem Index. Set preferred audio track language (en, de, fr, ...). Setze bevorzugte Tonspursprache (en, de, fr, ...). Set preferred subtitle track language (en, de, fr, ...). Can be empty. Setze bevorzugte Untertitelsprache (en, de, fr, ...). Kann leer sein. List all video, audio and subtitle tracks in the media. Liste alle Bild-, Ton- und Untertitelspuren im Medium. Choose video track via its index. Wähle eine Bildspur mit ihrem Index. Choose audio track via its index. Wähle eine Tonspur mit ihrem Index. Choose subtitle track via its index. Can be empty. Wähle eine Untertitelspur mit ihrem Index. Kann leer sein. Swap left/right eye. Tausche linkes/rechtes Auge. swap-eyes Tausche Augen. Start in fullscreen mode. fullscreen Starte im Vollbildmodus. Set input mode (%1). Setze Eingabemodus (%1). Set output mode (%1). Setze Ausgabemodus (%1). Set 360° mode (%1). Setze 360° Modus (%1). Read commands from a script file. Lese Kommandos aus einer Skriptdatei. Set VR screen geometry, either as the special values 'united' or 'intersected', or as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left), or as a an aspect ratio followed by the name of an OBJ file that contains the screen geometry with texture coordinates (example: '16:9,myscreen.obj'). Setze die VR Bildschirmgeometrie, entweder als spezielle Werte 'united' oder 'intersected', oder als Liste aus neun mit Kommas abgegrenzten Werten die drei 3D Koordinaten repräsentieren welche einen flachen Bildschirm definieren (unten links, unten rechts, oben links), oder als Seitenverhätlnis gefolgt vom Namen einer OBJ Datei die die Bildschirmgeometrie und Texturkoordinaten enthält (Beispiel: '16:9,meinschirm.obj'). Capture audio/video input from microphone and camera/screen/window. Hole Audio/Video von Mikrofon und Kamera/Bildschirm/Fenster. List screen inputs. Liste die Bildschirm-Eingabegeräte. List window inputs. Liste die Fenster-Eingabegeräte. Choose screen input via its index. Wähle eine Bildschirmeingabe mit ihrem Index. Choose window input via its index. Wähle eine Fenstereingabe mit ihrem Index. Load playlist. Lade Wiedergabeliste. Set loop mode (%1). Setze Wiederholungsmodus (%1). Set wait mode (%1). Setze Wartemodus Set surround mode (%1). Setze Surroundmodus (%1). Invalid argument for option %1 Ungültiges Argument für Option %1 VR mode unavailable - recompile Bino with QVR support! VR Modus nicht verfügbar - rekompiliere Bino mit QVR Unterstützung! No audio outputs available. Keine Tonausgaben verfügbar. Audio output %1: %2 Tonausgabe %1: %2 No audio inputs available. Keine Toneingaben verfügbar. Audio input %1: %2 Toneingabe %1: %2 No video inputs available. Keine Bildeingaben verfügbar. Video input %1: %2 Bildeingabe %1: %2 No screen inputs available. Keine Bildschirmeingabegeräte vorhanden. Screen input %1: %2 Bildschirmeingabe %1: %2 No window inputs available. Keine Fenstereingabegeräte vorhanden. Window input %1: %2 Fenstereingabe %1: %2 File does not exist: %1 Datei existiert nicht: %1 %1: %2 %1: %2 Cannot capture and play URL at the same time. Kann nicht gleichzeitig aufnehmen und URL wiedergeben. video Bild audio Ton subtitle Untertitel no %1 tracks Keine %1spuren %1 track %2 %1spur %2 Cannot initialize QVR manager Kann QVR Manager nicht initialisieren Screen Loading screen from %1 Lade Bildschirm aus %1 Warning: %1 warning: %1 Warnung: %1 Error: %1 error: %1 Fehler: %1 Unknown error unknown error Unbekannter Fehler VideoFrame Pixel format %1 is not hardware accelerated! Pixelformat %1 ist nicht hardwarebeschleunigt! Widget Insufficient OpenGL capabilities. Insufficient OpenGL capabilities Unzureichende OpenGL Fähigkeiten. Error Fehler OpenGL stereo mode is not available on this system. OpenGL stereo mode is not available on this system OpenGL Stereo Modus ist nicht verfügbar auf diesem System. bino-2.5/i18n/bino_ka.ts000066400000000000000000001706631475415313200150620ustar00rootroot00000000000000 Bino Media player error: %1 მედია დამკვრელის შეცდომა CommandInterpreter Cannot open %1: %2 გახსნის შეცდომა: %1: %2 Cannot read command from %1 %1-დან ბრძანების წაკითხვის შეცდომა Invalid argument in %1 line %2 არასწორი არგუმენტი %1-ში %2-ე ხაზზე %1: %2 Invalid command %1 line %2 არასწორი ბრძანება %1 ხაზზე %2 Gui &File &ფაილი &Open file... &ფაილის გახსნა... Open &URL... &URL-ის გახსნა... Open &Camera... &კამერის გახსნა... &Quit &გასვლა &Tracks &აუდიობილიკები &3D Modes &3D რეჟიმები 360° mode 360° რეჟიმი Input 2D შეყვანა 2D Input top/bottom შეყვანა ზედა/ქვედა Input top/bottom half height შეყვანა ზედა/ქვედა ნახევარ სიმაღლეზე Input bottom/top შეყვანა ქვედა/ზედა Input bottom/top half height შეყვანა ქვედა/ზედა ნახევარ სიმაღლეზე Input left/right შეყვანა მარცხენა/მარჯვენა Input left/right half width შეყვანა მარცხენა/მარჯვენა ნახევარ სიმაღლეზე Input right/left შეყვანა მარჯვენა/მარცხენა Input right/left half width შეყვანა მარჯვენა/მარცხენა ნახევარ სიმაღლეზე Input alternating left/right შეყვანა ცვლადი მარცხენა/მარჯვენა Input alternating right/left შეყვანა ცვლადი მარჯვენა/მარცხენა Output left მარცხენა გამოტანა Output right მარჯვენა გამოტანა Output OpenGL Stereo OpenGL სტერეოს გამოტანა Output alternating ცვლადის გამოტანა Output red/cyan high quality წითელი/ცისფერი მაღალი ხარისხის გამოტანა Output red/cyan full color წითელი/ცისფერი სრული ფერების გამოტანა Output red/cyan half color წითელი/ცისფერი ნახევარი ფერების გამოტანა Output red/cyan monochrome წითელი/ცისფერი შავთეთრის გამოტანა Output green/magenta high quality მწვანე/ალისფერი მაღალი ხარისხის გამოტანა Output green/magenta full color მწვანე/ალისფერი სრული ფერის გამოტანა Output green/magenta half color მწვანე/ალისფერი ნახევარი ფერის გამოტანა Output green/magenta monochrome მწვანე/ალისფერი შავთეთრის გამოტანა Output amber/blue high quality ყვითელი/ლურჯი მაღალი ხარისხის გამოტანა Output amber/blue full color ყვითელი/ლურჯი სრული ფერის გამოტანა Output amber/blue half color ყვითელი/ლურჯი ნახევარი ფერის გამოტანა Output amber/blue monochrome ყვითელი/ლურჯი შავთეთრის გამოტანა Output red/green monochrome წითელი/მწვანე შავთეთრის გამოტანა Output red/blue monochrome წითელი/ლურჯი შავთეთრის გამოტანა Output even/odd rows ლუწი/კენტი მწკრივების გამოტანა Output even/odd columns ლუწი/კენტი სვეტების გამოტანა Output checkerboard ჭადრაკული დაფის გამოტანა &Open File(s)... Open &Capture Device... &Playlist &Load... &Save... &Edit... &Next &Previous Input 3D above-below Input 3D side-by-side Input 3D alternating Output 3D side-by-side Output 3D above-below Output 3D interleaved Output 3D anaglyph &Media &მედია Mute audio ხმის დადუმება Increase audio volume ხმის გაზრდა Decrease audio volume ხმის დაწევა Pause გაჩერება Seek forward 1 second წინ გადახვევა 1 წამით Seek backwards 1 second 1 წამით უკან გადახვევა Seek forward 10 seconds 10 წამით წინ გადახვევა Seek backwards 10 seconds 10 წამით უკან გადახვევა Seek forward 1 minute ერთი წუთით წინ გადახვევა Seek backwards 1 minute ერთი წუთით უკან გადახვევა Seek forward 10 minutes 10 წუთით წინ გადახვევა Seek backwards 10 minutes 10 წუთით უკან გადახვევა &View _ხედი &Fullscreen &მთელ ეკრანზე &Swap eyes &თვალების ადგილის გაცვლა &Help &დახმარება &About... &შესახებ... Error შეცდომა Open URL URL-ის გახსნა URL: URL: Cancel _გაუქმება OK დიახ Open Capture Device Camera Input: Open Camera კამერის გახსნა Video Input: ვიდეოს შეყვანა: Default ნაგულისხმები Screen Input: Window Input: Audio Input: აუდიოს შეტანა: None არაფერი Playlists (*.m3u) About Bino Bino-ის შესახებ Bino version %1 Bino-ის ვერსია: %1 Copyright (C) %1 Martin Lambers (C) %1 Martin Lambers, ყველა უფლება დაცულია This is free software. You may redistribute copies of it under the terms of the <a href="http://www.gnu.org/licenses/gpl.html">GNU General Public License</a>. There is NO WARRANTY, to the extent permitted by law. ეს უფასო პროგრამაა. შეგიძლიათ, გაავრცელოთ ის<a href="http://www.gnu.org/licenses/gpl.html">GNU General Public License</a> ლიცენზიით. არანაირი გარანტია, კანონით დაშვებულ ფარგლებამდე, ამ პროგრამას არ გააჩნია. Video track %1 ვიდეო ტრეკი %1 (%1) (%1) Audio track %1 აუდიო ტრეკი %1 No subtitles სუბტიტრების გარეშე Subtitle track %1 სუბტიტრების ტრეკი %1 MetaData Cannot get meta data from %1: %2 %1-დან მეტამონაცემების მიღების შეცდომა: %2 Mode Input autodetection Input 2D შეყვანა 2D Input top/bottom შეყვანა ზედა/ქვედა Input top/bottom half height შეყვანა ზედა/ქვედა ნახევარ სიმაღლეზე Input bottom/top შეყვანა ქვედა/ზედა Input bottom/top half height შეყვანა ქვედა/ზედა ნახევარ სიმაღლეზე Input left/right შეყვანა მარცხენა/მარჯვენა Input left/right half width შეყვანა მარცხენა/მარჯვენა ნახევარ სიმაღლეზე Input right/left შეყვანა მარჯვენა/მარცხენა Input right/left half width შეყვანა მარჯვენა/მარცხენა ნახევარ სიმაღლეზე Input alternating left/right შეყვანა ცვლადი მარცხენა/მარჯვენა Input alternating right/left შეყვანა ცვლადი მარჯვენა/მარცხენა Surround autodetection Surround off Surround 180° Surround 360° Output 2D left Output 2D right Output 3D OpenGL Stereo Output 3D alternating Output HDMI frame pack Output left/right Output left/right half width Output right/left Output right/left half width Output top/bottom Output top/bottom half height Output bottom/top Output bottom/top half height Output even/odd rows ლუწი/კენტი მწკრივების გამოტანა Output even/odd columns ლუწი/კენტი სვეტების გამოტანა Output checkerboard ჭადრაკული დაფის გამოტანა Output red/cyan high quality წითელი/ცისფერი მაღალი ხარისხის გამოტანა Output red/cyan full color წითელი/ცისფერი სრული ფერების გამოტანა Output red/cyan half color წითელი/ცისფერი ნახევარი ფერების გამოტანა Output red/cyan monochrome წითელი/ცისფერი შავთეთრის გამოტანა Output green/magenta high quality მწვანე/ალისფერი მაღალი ხარისხის გამოტანა Output green/magenta full color მწვანე/ალისფერი სრული ფერის გამოტანა Output green/magenta half color მწვანე/ალისფერი ნახევარი ფერის გამოტანა Output green/magenta monochrome მწვანე/ალისფერი შავთეთრის გამოტანა Output amber/blue high quality ყვითელი/ლურჯი მაღალი ხარისხის გამოტანა Output amber/blue full color ყვითელი/ლურჯი სრული ფერის გამოტანა Output amber/blue half color ყვითელი/ლურჯი ნახევარი ფერის გამოტანა Output amber/blue monochrome ყვითელი/ლურჯი შავთეთრის გამოტანა Output red/green monochrome წითელი/მწვანე შავთეთრის გამოტანა Output red/blue monochrome წითელი/ლურჯი შავთეთრის გამოტანა Loop off Loop one Loop all Wait off Wait on PlaylistEditor Edit Playlist URL Input Mode Surround Mode Video Track Audio Track Subtitle Track Move up Move down Add... Remove Edit... Done default none PlaylistEntryEditor Edit Playlist Entry URL: URL: Set File... Set URL... Input Mode: Surround Mode: Video Track: Audio Track: Subtitle Track: Done Open URL URL-ის გახსნა Cancel _გაუქმება OK დიახ default none (%1) (%1) QCommandLineParser 3D video player -- see https://bino3d.org 3D ვიდეო დამკვრელი -- იხილეთ https://bino3d.org Media to play. დასაკრავი მედია. Set log level (%1). ჟურნალის ფაილის დაყენება (%1). Set log file. ჟურნალის ფაილის დაყენება. Use OpenGL ES instead of Desktop OpenGL. Desktop OpenGL-ის მაგიერ OpenGL ES-ის გამოყენება. Use OpenGL quad-buffered stereo in GUI mode. GUI რეჟიმში ოთხმაგად-ბუფერირებული OpenGL-ის გამოყენება. Start in VR mode instead of GUI mode. GUI რეჟიმის მაგიერ VR რეჟიმში გაშვება. Set VR screen geometry, either as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left) or as a name of an OBJ file that contains the screen geometry with texture coordinates. დააყენეთ VR ეკრანის გეომეტრია ან მძიმით გამოყოფილი 9 მნიშვნელობით, რომლებიც ბრტყელი ეკრანისსამ 3D კოორდინატს აღწერენ (ქვედა მარცხენა, ქვედა მარჯვენა, ზედა მარცხენა) ან OBJ ფაილის სახელი, რომელიც ეკრანის გეომეტრიას ტექსტურების კოორდინატებით შეიცავს. Capture video/audio input from camera and microphone. ვიდეო/აუდიოს ჩაწერა კამერიდან და მიკროფონიდან. List audio outputs. აუდიო გამოტანის სია. List audio inputs. აუდიო შეყვანის სია. List video inputs. ვიდეო შეყვანის სია. Choose audio output via its index. აირჩიეთ აუდიოს გამოტანა მისი ინდექსით. Choose audio input via its index. Can be empty. აირჩიეთ აუდიოს შეყვანა მისი ინდექსით.არ შეიძლება, ცარიელი იყოს. Choose video input via its index. აირჩიეთ ვიდეოს შეყვანა მისი ინდექსით. Set preferred audio track language (en, de, fr, ...). აირჩიეთ აუდიო ტრეკის სასურველიენა (en, de, fr, …). Set preferred subtitle track language (en, de, fr, ...). Can be empty. აირჩიეთ სუბტიტრებისთვის სასურველი ენა (en, de, fr, …) შეიძლება, იყოს ცარიელი. List all video, audio and subtitle tracks in the media. მედიაში ყველა ვიდეოს, აუდიოს და სუბტიტრების ჩამონათვალი. Choose video track via its index. აირჩიეთ ვიდეო ტრეკი მისი ინდექსით. Choose audio track via its index. აირჩიეთ აუდიო ტრეკი მისი ინდექსით. Choose subtitle track via its index. Can be empty. აირჩიეთ სუბტიტრების ტრეკი მისი ინდექსით. შეიძლება, ცარიელი იყოს. Swap left/right eye. მარჯვენა/მარცხენა თვალების მიმოცვლა. swap-eyes თვალების-ადგილების-მიმოცვლა Start in fullscreen mode. მთელ ეკრანზე გაშვება. Set input mode (%1). შეყვანის რეჟიმის დაყენება (%1). Set output mode (%1). გამოტანის რეჟიმის დაყენება (%1). Set 360° mode (%1). 360° რეჟიმის დაყენება (%1). Read commands from a script file. ბრძანებების სკრიპტის ფაილიდან წაკითხვა. Set VR screen geometry, either as the special values 'united' or 'intersected', or as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left), or as a an aspect ratio followed by the name of an OBJ file that contains the screen geometry with texture coordinates (example: '16:9,myscreen.obj'). Capture audio/video input from microphone and camera/screen/window. List screen inputs. List window inputs. Choose screen input via its index. Choose window input via its index. Load playlist. Set loop mode (%1). Set wait mode (%1). Set surround mode (%1). Invalid argument for option %1 არასწორი არგუმენტი პარამეტრისთვის %1 VR mode unavailable - recompile Bino with QVR support! VR რეჟიმი ხელმიუწვდომელია. თავიდან ააგეთ Bino, QVR-ის მხარდაჭერით! No audio outputs available. აუდიოს გამოტანა ხელმიუწვდომელია. Audio output %1: %2 აუდიოს გამოტანა %1: %2 No audio inputs available. აუდიოს შეყვანა ხელმიუწვდომელია. Audio input %1: %2 აუდიოს შეყვანა %1: %2 No video inputs available. ვიდეოს შეყვანა ხელმიუწვდომელია. Video input %1: %2 ვიდეოს შეყვანა %1: %2 No screen inputs available. Screen input %1: %2 No window inputs available. Window input %1: %2 File does not exist: %1 ფაილი არ არსებობს: %1 %1: %2 Cannot capture and play URL at the same time. URL-დან ჩაწერა და დაკვრა ერთდროულად შეუძლებელია. video ვიდეო audio აუდიო subtitle ქვესათაური no %1 tracks %1 ტრეკი არ არსებობს %1 track %2 %1 ტრეკი %2 Cannot initialize QVR manager QVR მმართველის ინიციალიზაციის შეცდომა Screen Loading screen from %1 %1-დან ეკრანის ჩატვირთვა Warning: %1 გაფრთხილება: %1 Error: %1 შეცდომა: %1 Unknown error უცნობი შეცდომა VideoFrame Pixel format %1 is not hardware accelerated! პიქსელის ფორმატი %1 აპარატურულად არ ჩქარდება! Widget Insufficient OpenGL capabilities. OpenGL-ის არასაკმარისი შესაძლებლობები. Error შეცდომა OpenGL stereo mode is not available on this system. OpenGL-ის სტერეო რეჟიმი ამ სისტემაზე ხელმიუწვდომელია. bino-2.5/i18n/bino_zh.ts000066400000000000000000001464651475415313200151130ustar00rootroot00000000000000 Bino Media player error: %1 媒体播放器错误: %1 CommandInterpreter Cannot open %1: %2 Cannot open %1: %2) 未能打开 %1: %2 Cannot read command from %1 未能从 %1 中读取指令 Invalid argument in %1 line %2 参数无效 %1 中第 %2 行 %1: %2 %1: %2 Invalid command %1 line %2 Invalid command %1 指令无效: %1 中第 %2 行 Gui &File &文件 &Open file... &打开文件... Open &URL... &打开URL... Open &Camera... &打开摄像头... &Quit &退出 &Tracks &轨道 &Playlist &播放列表 &Load... &加载... &Save... &保存... &Edit... &编辑... &Next &下一个 &Previous &上一个 &3D Modes &3D模式 360° mode 360°模式 Input 2D 输入2D Input top/bottom 输入上/下 Input top/bottom half height 输入半高上/下 Input bottom/top 输入下/上 Input bottom/top half height 输入半高下/上 Input left/right 输入左/右 Input left/right half width 输入半宽左/右 Input right/left 输入右/左 Input right/left half width 输入半宽右/左 Input alternating left/right 输入左/右交替 Input alternating right/left 输入右/左交替 Output left 输出左边 Output right 输出右边 Output OpenGL Stereo 输出OpenGL立体 Output alternating 输出交替 Output red/cyan high quality 输出高品质红/青 Output red/cyan full color 输出全色红/青 Output red/cyan half color 输出半色调红/青 Output red/cyan monochrome 输出单色红/青 Output green/magenta high quality 输出高质量绿/品红 Output green/magenta full color 输出全色绿/品红 Output green/magenta half color 输出半色调绿/品红 Output green/magenta monochrome 输出全色绿/品红 Output amber/blue high quality 输出高质量琥珀/蓝 Output amber/blue full color 输出全色琥珀/蓝 Output amber/blue half color 输出半色调琥珀/蓝 Output amber/blue monochrome 输出单色琥珀/蓝 Output red/green monochrome 输出单色红/绿 Output red/blue monochrome 输出单色红/蓝 Output even/odd rows 输出偶/奇行 Output even/odd columns 输出偶/奇列 Output checkerboard 输出棋盘图案 &Open File(s)... Open &Capture Device... Input 3D above-below 输入上下3D Input 3D side-by-side 输入左右3D Input 3D alternating 输入交替3D Output 3D side-by-side 输出左右3D Output 3D above-below 输出上下3D Output 3D interleaved 输出交替3D Output 3D anaglyph 输出补色3D &Media &多媒体 Mute audio 静音 Increase audio volume 增大音量 Decrease audio volume 减低音量 Pause 暂停 Seek forward 1 second 前进1秒 Seek backwards 1 second 后退一秒 Seek forward 10 seconds 前进10秒 Seek backwards 10 seconds 后退10秒 Seek forward 1 minute 前进1分钟 Seek backwards 1 minute 后退1分钟 Seek forward 10 minutes 前进10分钟 Seek backwards 10 minutes 后退10分钟 &View &查看 &Fullscreen &全屏 &Swap eyes &交换眼睛 &Help &帮助 &About... &关于... Error 错误 Open URL 打开URL URL: URL: Cancel 取消 OK 确定 Open Capture Device Camera Input: Open Camera 打开摄像头 Video Input: 视频输入: Default 默认 Screen Input: Window Input: Audio Input: 音频输入: None Playlists (*.m3u) 播放列表 (*.m3u) About Bino 关于Bino Bino version %1 Bino版本 %1 Copyright (C) %1 Martin Lambers Copyright (C) %1 Martin Lambers This is free software. You may redistribute copies of it under the terms of the <a href="http://www.gnu.org/licenses/gpl.html">GNU General Public License</a>. There is NO WARRANTY, to the extent permitted by law. 这是免费软件。您可以根据 <a href="http://www.gnu.org/licenses/gpl.html">GNU通用公共许可证</a>在法律允许的范围内复制和使用,作者无任何保证及不承担任何责任。 Video track %1 视频轨道 %1 (%1) (%1) Audio track %1 音频轨道 %1 No subtitles 无字幕 Subtitle track %1 字幕轨道 %1 MetaData Cannot get meta data from %1: %2 无法从 %1: %2 中获取元数据 Mode Input autodetection 输入自动检测 Input 2D 输入2D Input top/bottom 输入上/下 Input top/bottom half height 输入半高上/下 Input bottom/top 输入下/上 Input bottom/top half height 输入半高下/上 Input left/right 输入左/右 Input left/right half width 输入半宽左/右 Input right/left 输入右/左 Input right/left half width 输入半宽右/左 Input alternating left/right 输入左/右交替 Input alternating right/left 输入右/左交替 Surround autodetection 环绕自动检测 Surround off 环绕关 Surround 180° 环绕180° Surround 360° 环绕360° Output 2D left 输出2D左 Output 2D right 输出2D右 Output 3D OpenGL Stereo 输出OpenGL立体3D Output HDMI frame pack 输出HDMI帧包 Output 3D alternating 输出交替3D Output left/right 输出左/右 Output left/right half width 输出半宽左/右 Output right/left 输出右/左 Output right/left half width 输出半宽右/左 Output top/bottom 输出上/下 Output top/bottom half height 输出半高上/下 Output bottom/top 输出下/上 Output bottom/top half height 输出半高下/上 Output even/odd rows 输出偶/奇行 Output even/odd columns 输出偶/奇列 Output checkerboard 输出棋盘图案 Output red/cyan high quality 输出高品质红/青 Output red/cyan full color 输出全色红/青 Output red/cyan half color 输出半色调红/青 Output red/cyan monochrome 输出高单色红/青 Output green/magenta high quality 输出高品质绿/品红 Output green/magenta full color 输出全色绿/品红 Output green/magenta half color 输出半色调绿/品红 Output green/magenta monochrome 输出单色绿/品红 Output amber/blue high quality 输出高品质琥珀/蓝 Output amber/blue full color 输出全色琥珀/蓝 Output amber/blue half color 输出半色调琥珀/蓝 Output amber/blue monochrome 输出单色琥珀/蓝 Output red/green monochrome 输出单色红/绿 Output red/blue monochrome 输出单色红/蓝 Loop off 关闭循环 Loop one 循环单个 Loop all 循环所有 Wait off Wait on PlaylistEditor Edit Playlist 编辑播放列表 URL URL Input Mode 输入模式 Surround Mode 环绕模式 Video Track 视频轨道 Audio Track 音频轨道 Subtitle Track 字幕轨道 Move up 上移 Move down 下移r Add... 添加... Add 添加 Remove 移除 Edit... 编辑... Done 完成 default 默认 none PlaylistEntryEditor Edit Playlist Entry 编辑播放列表入口 URL: URL: Set File... 设置文件... Set URL... 设置URL... Input Mode: 输出模式: Surround Mode: 环绕模式: Video Track: 视频轨道: Audio Track: 音频轨道: Subtitle Track: 字幕轨道: Done 完成 Open URL 打开URL Cancel 取消 OK OK default 默认 none (%1) (%1) QCommandLineParser 3D video player -- see https://bino3d.org 3D视频播放器 -- 前往https://bino3d.org Media to play. 播放的媒体 Set log level (%1). 设置日志等级 (%1)。 Set log file. 设置日志文件。 Use OpenGL ES instead of Desktop OpenGL. 使用OpenGL ES而不是桌面OpenGL。 Use OpenGL quad-buffered stereo in GUI mode. 在GUI模式下使用OpenGL四重缓冲立体。 Start in VR mode instead of GUI mode. 以VR模式而不是GUI模式启动。 Set VR screen geometry, either as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left) or as a name of an OBJ file that contains the screen geometry with texture coordinates. 设置VR屏幕几何形状,可以是用逗号分隔的九个值,表示定义平面屏幕(左下角、右下角、左上角)的三个3D坐标,也可以是一个OBJ文件的名称,该文件包含了屏幕几何形状和纹理坐标。 Capture video/audio input from camera and microphone. Capture audio/video input from camera and microphone. 从照相机和麦克风捕获音视频输入。 List audio outputs. 列出音频输出。 List audio inputs. 列出音频输入。 List video inputs. 列出视频输入。 Choose audio output via its index. 通过索引选择音频输出。 Choose audio input via its index. Can be empty. 通过索引选择音频输出,可以为空。 Choose video input via its index. 通过索引选择视频输入。 Set preferred audio track language (en, de, fr, ...). 设置偏好的音频轨道语言 (英语, 德语, 法语等)。 Set preferred subtitle track language (en, de, fr, ...). Can be empty. 设置偏好的字幕轨道语言 (英语, 德语, 法语等),可以为空。 List all video, audio and subtitle tracks in the media. 列出媒体中的所有视频、音频和字幕轨道。 Choose video track via its index. 通过索引选择视频轨道。 Choose audio track via its index. 通过索引选择音频轨道。 Choose subtitle track via its index. Can be empty. 通过索引选择字幕轨道,可以为空。 Swap left/right eye. 切换左右眼。 swap-eyes 切换眼睛 Start in fullscreen mode. fullscreen 进入全屏 Set input mode (%1). 设置输入模式 (%1)。 Set output mode (%1). 设置输出模式 (%1)。 Set 360° mode (%1). 设置360°模式 (%1)。 Read commands from a script file. 从脚本文件中读取指令。 Set VR screen geometry, either as the special values 'united' or 'intersected', or as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left), or as a an aspect ratio followed by the name of an OBJ file that contains the screen geometry with texture coordinates (example: '16:9,myscreen.obj'). Capture audio/video input from microphone and camera/screen/window. List screen inputs. List window inputs. Choose screen input via its index. Choose window input via its index. Load playlist. 加载播放列表。 Set loop mode (%1). 设置循环模式 (%1)。 Set wait mode (%1). Set surround mode (%1). 设置环绕模式 (%1)。 Invalid argument for option %1 %1 选项中的参数无效 VR mode unavailable - recompile Bino with QVR support! VR模式不可用-使用QVR支持重新编译Bino! No audio outputs available. 无有效音频输出。 Audio output %1: %2 音频输出 %1: %2 No audio inputs available. 无有效音频输入。 Audio input %1: %2 音频输出 %1: %2 No video inputs available. 无有效视频出入。 Video input %1: %2 视频输入 %1: %2 No screen inputs available. Screen input %1: %2 No window inputs available. Window input %1: %2 File does not exist: %1 文件不存在: %1 %1: %2 %1: %2 Cannot capture and play URL at the same time. 无法同时获取和播放URL。 video 视频 audio 音频 subtitle 字幕 no %1 tracks 没有 %1 轨道 %1 track %2 %1 轨道 %2 Cannot initialize QVR manager 无法初始化QVR管理器 Screen Loading screen from %1 从 %1 中读取屏幕 Warning: %1 warning: %1 警告: %1 Error: %1 error: %1 错误: %1 Unknown error unknown error 位置错误 VideoFrame Pixel format %1 is not hardware accelerated! 像素格式 %1 没有硬件加速 Widget Insufficient OpenGL capabilities. Insufficient OpenGL capabilities OpenGL功能不足 Error 错误 OpenGL stereo mode is not available on this system. OpenGL stereo mode is not available on this system OpenGL立体模式在此系统上不可用 bino-2.5/res/000077500000000000000000000000001475415313200131055ustar00rootroot00000000000000bino-2.5/res/bino-logo-big.svg000066400000000000000000001355121475415313200162610ustar00rootroot00000000000000 image/svg+xml free 3d video player bino-2.5/res/bino-logo-small-128.png000066400000000000000000000147401475415313200171240ustar00rootroot00000000000000PNG  IHDR>a pHYsLLn2tEXtSoftwarewww.inkscape.org<mIDATxwx]Օܢ.ْmpl `86%!vh $`2 @13LB&|LM') evq$q\]EWV^k(_ -8b "a7DުShy #aX/F.3Bo -W! G "G VpBKaw),lPj`V(1@$p+=StB] |4ZA@8,VG%@ٮJ8oRoYĂbj K(+P$hD8Y$gq a$T1_z j^ ׻ %Z? aE)&\_RҫU_'uൣ`ZVAư!,`A*ӝ*=k'wWWyƠcXU>\0>;lB~4 ś+g.7Ut ̴?V:zNaI0k "JbU7:3׎Tm`,ώpt/qK*S-Âb}ր'&yU6D\3@5g̔SZCB  |%ɵ*^BI9AP>']9),9t-yѳR V T;sݍm\;V1 og$5İ N$>\V4^i]t':}nDiy(V+2h59.n5'"m*;_a<˯ Rb\`(?+o u v҉gufwCÃQr2hەR_54]! جj/T1d " E>k ,aښ>@/a:@D888*"~z=]GD&Kb4~F6=&~dC,~\IjE"FD>|`yh 4.=59)/i@D^h\@D+b. RL^2i"rB,ެJ\ 8Ӱ?kE s΃J -9>2ܼˎ61܃ֽ4-q?4 *{'bS `$T:>+E/|wF"R""?Q'}fLM-o|ҋ@DV}G򪾙np |1_;DJD~ 2 iCR LEdT> }"h~ @8 O}%]JvFBI|JKqikO@(hK^Rq?z,~ MDX{j-QǦf{7azФS9j*OPM.eގO_;+>1||_$˰s.`:pFXbXޞ<`fSSP:} uWK~㫎lw/t*<.9:SwFd>J텏9M@P̋uOxliX:1c,^̟߃0f @ԟÄoEs=10PV(ʳWzr=<JQ.NNx*+!F+l5VX~>d']<w|/t/D7!ٹ<;h"Zb;*x^»^oL97wf8Ux0vd7#Nթ7a =gWNN|4~o +ɾ|ʒ8وϭr yqZSP7˓lqMu=Ҝ> *NkNż :I R$=X=V=b\.8'UgS`h3N_Y{6*lEx*Dz/bg_$h~$J Wd|0.ߛFD+OCdp$X̨%brw#g 0#{0 |H!wi71e^5hkJdx)_U8N)ePV &JŜk5'Z9OMP_.T+wkP3i0? 1r= f=#>_'F~ q4A? P_({- *9CZ0[{B}3@_R|Lɯj@{(D7W7"{HKY 88g>.GFNJǖjwn.Ç@蟊9) AUnܞ\s6Pr2ܮ'DK+kxboNɓ|qbܬ`p2MV*=ꁍ㼽%W{q8 rOZtK)ym޾-%a؁wh~r`쀒nk^Mc8ssG7EhL fͣqa@O϶| g?@:NÐq.%O<= 3]r>Ш(ΏUצG~}%ӺEۺ'pV:&9ݥ@)7(gw^OF 'tm{zM˳W_iK3a:S8-SٶX:ng]=TKȞuZV/;Xd =|puF=&(Ҭ^3Q $C%} vw۳@V?TI??[l2KET"bEny]k ߍК#Oe] :!TC]o:#!Ȯn]?}KIM6ˠ< ]vvKAR7zϖ}[M>ءMULlN_&0ִՃ,kXͦy[{g!c#\{׏'߾a` C,)xx$S($Zr=PXۼ:k&3ݛK }n`.8h*K*ReKcWJNUHb,qm60}lJ)gȉf2DBWtݼY':`_MI"% )Ro6Cě 0OhSl}XŹj)̵ Lo'nM'ߜ)ifi"S/;f,[Y9RfG(֦FZGxnhst.VfxB;Ix4KDzNUOEZU7)Rj#-$Wm^jg03XWڲC_Xs̚*W[z8NL#> ?t6@੨yt^m{"m6e.IVxmn^AMk %K^T9`itDnNa@Tmh .V6~O8cKAUaޛ;]i8EeU37X5*4%巺MPCL,z,149E3֨ug}݊w6^h`\ 鄋n 2Tt58_;LG@nٶդFOvSE p"6vYa&,~"JL)`\mylN''9AB dH(WfoXhKU>@EfǠ.~tilX.2WnӴ'2$G9 JK˔7X9mYM:ц#u9 FTY&NDH(۞06J+ jtm~:004mY-;_61.s3qf4N:s< `ǍCי2 * wuN)]@` MYt۰ pR7Aof ~}KV+PRo˘?9ohw $>n|MaLF8~@XqhNӉOBe_%Rr "і#qx3u8 +2`>j '"FCɦ@qbYz&M8 Г#'x6Dەںv^w.l[`dh*`N(É 8i DްѸx$GW8k~iDqCƥ) U5E[ղ%ax&mpCw4)k==) }vvǾX׋7(6S=o,>/_ T$"8Zx U4S).ntNUlW#àZ&I,.Y:^IB+Rp-kp΀j>o jށ> wy1>ٯ~`\iqHu0(RMyIP4:S,"4H(rݼ`|#}Jm]o\[|}L'Σ]/pE h+a6q>6ĂI&e<z}/x$_#9h IXcO7Iy .A~1"˿C;q"8*}g/lκR0mnDQ]PDJkyR;K#/ۧd0T`0JM _8'|  f.Ѕ4#/:XTӟDr =Ț8_wUJ=&2KʰK2$A-C  Mݏ͟r$c-mD,$h۾?os$Oȫ [`Ny5:v7G>Xb]BJٻv_RC'{m[gp+7*u} 1ߺmglIɥA% ¬%2]m| ZlTˇF}vz\NJٗ+uz ^aT6N{B_2Uه*zS.n|WjEA"R L 4%c[\ӺEiڰ;5Ftn(hߵ=ֲ_w4PP,W 8 V(Tjxiz{oV#'OQ/7 N^\Կ-RjflNdhTWꦥJyɂXaGVi4Ku~CȎ+վ{ M̪/>Ef?1(A:D_~D$@qlxц^TaioVc<񗛀 T*]&sJt$"DcP8$EzT(NȜ z~aྞg!c@wsE}2Wc_4wO b`F1|**-¯s}Lkӫ 3 ӣw__}(# {x_TH4sC.Zb7`vԌWL(㪈Dd[zTGDdT)Y#"{ ;! tbIPZSp]]E/5 <=MSE@(ZpZs,hJ\ Q@EZN<@ aj@/dH%ۏ76IRm*4d =6FS2o \Gi ~ 33hmYZ% nJ~tlXӅ%[lB)MLT.L~wMeZx4b;G!5va6Ɵ0 $e:P4C0nL -6hKyu~by5j_Xr?BպUvJǔ2cvcug<0Оh>;ktTIENDB`bino-2.5/res/bino-logo-small-22.png000066400000000000000000000020031475415313200170220ustar00rootroot00000000000000PNG  IHDRĴl; pHYs)~tEXtSoftwarewww.inkscape.org<IDAT8[h\esnxa$֤TMXxyWڂjAlD!FZP "*BE"T\U$69e%7aQU6rl7|+ Pu? h9vs %"l3O[֋ a'$߆)@3X^IA[HJ%LP>Q'Q% S~tFs*̄@uF.~b Kc S3fU (v,M:ERd38Qq|p}V1? _^em>4тrybGDaUȦ3]~UT=3$x(RoIcqpZډ…?#胎&8Ԋ.jO!^)W`~yvdi'*:~'+y'/[IҠŕ y772i5d֊fX1Uv3c{zPDV肪vծ-Z=?~'oHFz*IIENDB`bino-2.5/res/bino-logo-small-256.png000066400000000000000000000327611475415313200171310ustar00rootroot00000000000000PNG  IHDR\rf pHYs 5ltEXtSoftwarewww.inkscape.org< IDATxyUz}˾o$aMXd5""&"~"T^ *^de k,u23}~L23鞩:zLlSRe!!! $d @H0&aXE"/DM' CJDN8''G\ яw"RSpR^/mJ5i\B`9łdhFkZ+bHMManWQ[WBzCȥ?@+dA`o/+p"zC DD ¤-rDD7<0D)ҀW*XNxqBHgDM,k*Caxz#{=("h8&&IB.B oQ76syhPP{3.YAXz @R$N94j *]rL8"%#|je߄PKuZV~ٌ?AAw53.YN#}lYۀ"W9⪮M%8^Vd[rvVex6B⹎Z~L A7AA+V#ȴ/C$UJ<*rH @Dp|8`eq"W)UC/7˱8NNv!G;'UxNS~"4pj`!k`<&"H(9"","pf5KݢҋP)"y3yf'"_+gO@jG@) =EнQ<-"Ȭ<2 {e N{3X+"6GDcIX C17E; "3[ c}._|.xa˰5أᚋxDD*"LsU#"cETMц NDd_Z`UNyCD^9Wc_ FDDDn^3]n*;E6a&_DEfo* q6 iVb+}pu2Dt0)I-—9gEDӕ۰G33SFfS*e{i[LEdɐ9;wi !vl"l CNDd/PLA 6]2 6`M)}7onN^@4db5CBDdI8_'z 9Ga@e"2$H+h&pesx2}L> zn~0!J DEdJ @bK.쇧wgh؛<%"Hʸ@D^:4j` 0M_p @A"q"VM*A`n~WDN6]?7"R;x|؆G:~+ggf5uNf]2~8f}>I k5Oh:}pRjj;FKҿ\,=JܪU[(FŸHkd~gz$>: w nW}6(y%ӎpxJѺ5񕛾+7'A`7- |Ey{J,Ӏ^Ưt&2*ț|$7zʖ'zΓ =&1ߊx*o`8Y.|[oD+'3ۙ: όdc^/XczV'of%6"%.1spދ~w[r7s<}).<%32̧^"RJȵXr_m|'cwEY{}[-{+{}HQe)6vߪQ&q<ƈ\/2kDీɽo6((̿bfdy?aѷP><\bu5ZFO̓=ug` 9 F~B.Aî|pxA.hob /d?q]yA{}oSy 5c$BnvU6&`o]؏g"\c!P4]_;NWy-~n`%Lj7IRR<:g. p̭qM_/Nu}~b#D+ G*F08{PЊW2;Uߧ_ug;l+ Z`\?dST#e=TymDGfF}NBj v@L(4Gߦ<#8,U}]5YqmFeg,fڼI_}0 :xs@|Շ^;r|\rWF="%9' ^ґ]0.]!M_{"ޭ?9'DJl 9+).a73Ywu& ر^sϣxC콋' *uf^>$od(c1XL:'R66R}ԥ/=Xs&huMyXqj)+1TɀP4?UP?/fap&% 9ˉr[$R:?u 0[_>xd5{cfؾr*Ďq-yzSa Q70 S| Q"53G M~&(NXp R-b%P4p Vg=pM:˕K@4r0?PyȷLMD|9J7ۃuc`}!ъI89Wƥ *g}𑁝ObLxoG&U0ٯ  ^70ͰOž#"3 x⪉\Ĝ-P ©\f`@ cCGQ2pdow|@_y:Lo ϊ?}D3/+o_Q-YL`zD R1ҙǛPJFHż|YS`Lߗn@X܌ᕀ&n9#V, %3-^[W> l7|{leGh;7L[B%+JP<Jf~%$pSy>df}ֳT(p2k@Ol3}TmHֿRi@a9FԽKk?xET}վ}Y K nǒ3 .pp'Z 2|v/RL.VNڧ#iR c׎Uyt|Tnh̗[@ƙ) shFng{PBXEjHgP$?e0d+uFG=[^D| "%@ Бg>/ {P'h0!9B<ϻcߨq$>yl @$gT2A#瓬{@dNb)_Ҟ "Ƀh~;@҃<{u?6b{zg:EsߣH' jsQ]mmHw x['[<C}Ym w"QBwW4#b-K/Z}Tdr&_n6x4P"+WhHxW*Ѩ{k>O_l\c|0SbCʐfv`L,,b#fzѧjWmwAۊޒ; 4/bsԯQ،w mW*GCpDh_7wTz%'A%ռ^O㲠$˚tptA;J6i9䅆J_njx`ÆD멀JڛhxQ 'eCQ^ @M\ԵW2GF [)F" GP)M탈`W^0ǿ <*^ JN]wnq@u O^0v$ӵcRG8A<|*>x3фIjߚy`; v|p+RFE4S5<(n\6~0P$=g. K<0(^79餗k϶](ZxeHC='d%n^*!@bo=| Y? s>ړT ոT:?NU%F\fTs:^8x,VPS`jKuao#FX:7ZMm~ky#}.@r[>6;JN?Mw0X?tzuI`1(uկjnxo ؀t:A#]i?]5EbBL5f"CE׶/Qjݧ. &i+?u͏|Z?}oCuH6:7qョ5'j77@F*>t6sY/bnN2@t6F\}g[R{\G:/#{8 e!g}´:k9ƚ=@C-~l7pJ[[M~;~.`9z<#ys9u5}|X֤wP0ռA{Hs>q/twP>Qv"w?>U> ]ޭm~?;bR߫. bo;tӗgLzH' o%_Jo{T_ɹ<ܓW::ur3׮@䳗Nc¬h8[_mqH_Rm~c7Q"ubYwؔ/5Xwt gg|:#x0ǪyRuo?1}^ȃ+Ad~UWvDݗ[TR:\W)pUrtdc};ǠJ}=^Wpnk^ I/wvwS>^P]_0-B=j5wRf5~iP0թo[6#~ R簷rk;Il(׍UPP)O;| :&-%.~.&BiJ8R>W~WTɗE@z:1n7!P/j%Ju~ f*G"Y[?MfIP0?1߅YMڽٜ1 @.~({ow*u43@j:d|?ߠOf%ٲگ}.#!ѿgim_uxtX>;> E dN&O4@PpR]ZhTOcPmUr>H8v!_y&W*~|0~˥ w- u20UlǨ|UM6N>8=<`"YOH[nu-{Xٞ#}Vϡ-Y_`Ƨߜx s`RkB)S[-q"Cm&tgȩ,YĨ J}hxmWH?̿7cwTR815;N;P^xVmϹ#7*v셓~<NtG)L)Rm[/v[r:qG2W'AAĉ?*~Tr-h?i$Wj{9,knSvRJvkNj8>5>20lqYL>u"1:(W`,5znDˁZQN)4~" .qSNcKN`B⁍ F*'!r=}Ժ 7}+Z ѵr<^s ,RN"R]@Ϗ˼Y̓(DjvG4ɦMeΊJMAi s0>18E՗C$ۉÅ0܈Bd^u>lxha,`~ ㆿ=W^+⨏5=xdθE@b]:(zH6)\&c+T'^T][$n&>r.r5y^%V-7kFuŸoO\ h ymXKs7Z2(hK`"ݺdM5Фí=S/8I5Gh"YvGO?Ggyhz]G971m$VhC@ lTW!נD\Ԃ`qԁꝰ< Mu sXw ڑ2?:ؖh~_/o*(ޅwuŬg#*H/lg~`nԀFcyގ'FK< OվJ--sF[:9w*inDdZ:'Au{V@x}HiP02 7NfGC J5tc3* ryMCR"Z6P59v*V_Z`[#>\ 'Axlީm} Q'oT_͊D{l;ѭ9lѺ8\r%_R ks;#1O@;,ձM8솨c%W\ֻtIX6Ft3 . xpp:I}u3F~pKڴ>Q"|M7]%rRO]xDcszX%:|?6h-}ڶ|K?=-n-ܭ gK;!r(@D%#cSҊ[9_@tw*.VI* @ o;bJۿ,(KuD|,蕀dԊwʲr[)GijjĈ`RM+aǀ=#Z@i#_/J#2( =~%RDtZ#Ub]?nRX9?wJxj~TB@7hH3c%N=WhtڻAkx6S;7SE÷gEDJh} FĦ/Iu40KF`R :|F-Edo -AE5>Q~t!NMվZvsa`|C;6žMϊn@ī< ElƉAj'O zalfyq2 hFk^E*ht;\|*4I~6:`H(3;(hʼnv' zaqX Ʀ-**ѐn߸;Z @R:߀gt#1o@85j_AN?^[S[|QX堜l^7CM`DtJFbE!c R=WiTۚo;<Ru&G4[ A*&wNGk~]P02"cԶ~3 p Y?W0EI/'6,N7]BJݽju_0ZS{3ㆈTM'ߑ~(E|/"E¯.(x1g@9P{+~;P̓?w/6SsBC㴌XM$`_)pP)?#dU=ȴEHKjk;[0h%`Hݱ2Szy`ӁugAܠc;!:RR[_Gt;Ru BJrmJ~tz8 <EKL:]vom~/h$AzZ6M_r&֘}*w~Ө z7]'>5f0;:v\`XuYf&_>ȮgkQMkQ[txR:)ʀ#ݲ-QJbyR0pR]Y<L/{SLP~ jv  6]g$V'h7]%kz%xt=W" SmO{+`zhkMP[7'd ᖂ*|N2]T N'`O^;}B']mOm\))@upj4#w P#ݲc[ADAz`OVa jU麕Ǩn5]\)XR+_Rw 5R-/]Y2]|(hR?]GBm}ģ^oRpR^.H&'_:6Z8 M BW [iu!,UCg+$0HL qy!sW Rk^4ٛxJe˛t]dH +5X CE 1J+j_:%O 9R'Bڴ2z~S ! IB߀3V dJo~Z}sf~MgV.Tjf ׷Z9؅}Ū7QLLԷEmRi-GO\k`X @(0s=,v`SNR4A(*xUZE@)h^A7|ȯ+8ͅJYw l#0kRn/]Olgsd_DUim :!~|QSz|ii_ ۶gOQjB@=3.g˲h]!vU?Qб&,(U^oy p[L8]<2oLYtz" +oU]-IOi߼UկVgw5^PdXȅw"U50J䑀D;5T6~+sy%)r5coVţO%VQ*:#ݮ~ރyBpJ,ȞEpq TD!}r=IJ–.x+ y@6'6E*^e)[EJ-@ZJvh!ٶF%;_ISRJ.7]N6]L Ƅ@H @)B @DKOXq6 Pĕ:suɤk&%Rs6={;u ف  ]/HwdJ"y' S\zҘO>ka!  O]ӳ֌#D8Gl]1~=uImZ(^k z/Ju2{=yfp!P9I0,J41mS,g.Q$ 5{>5KR>IIRLQ>˞ !z=BG%0{L>:2_ŽP޾k/!$=W-tמ@]W>qڡJGۀCl}ƪW5VͲQͫڏZ}6/4E anߍZ]16$‚wײo 1J(C{+f4i;x.÷iYO?SB#a­c5sRӾDXpV_'瞻+PMX61߷(=mu// XF(Ð[Fwzy-8keR|Wxk4]0\n3l|nmn 212IENDB`bino-2.5/res/bino-logo-small-32.png000066400000000000000000000030271475415313200170320ustar00rootroot00000000000000PNG  IHDR szz pHYsNtEXtSoftwarewww.inkscape.org<IDATX[l\u9gc`H"Isr'VA@EP/H (&[IJ[@ APPbǍ=c{.xϙl ?ֿg21R}sATuyPC@bu@߾M,fE hd"-w~^tFwD[5'7 * 70:x\)>Nq^;н9p2x!HȨZE6y@\*_)+ "KE/pRDlyaD"r+"2‚KCL;2GayHDfvDû7Av!(ZBfl K-~ϦOl=qc TV؟( *B10&9Pҽq75/=U{wY+'8~@ .bVuY?޹]O1u"̳:Y8\d/]3>]_;1=\1>OR8X ̬A\WH} %e;M$z%۟64s{i&ߊ,ڢ:IW'Ed/wb?[%Y{Eg _zs|2ݱ.>+n^zK/}/ ^^|-ox!ӾIENDB`bino-2.5/res/bino-logo-small-48.png000066400000000000000000000044611475415313200170440ustar00rootroot00000000000000PNG  IHDR00W pHYs\\< 7tEXtSoftwarewww.inkscape.org<IDATh͙{T?s݅V (R-PԪ4>Zb>jM)5i m|jm?(iJ ay]sfuOݙ{̒٤g9;|sΈ֚j"2jСuH͡Fj`C~zZ䱑uQ uCh`*W ⫌ci o@ q%l[`@_ jᯝXV-Z Lz#hxe2Ay17={&/ּb |'D +e? 7stB-`.^$e!""bdtbix?Dk!oI;H|8-"MD5#bqOAD&6%5yy\D "sf.] 'gZ`IzXJ25V<+S~`N6oXSiP,\r5 /"/Hi ψ|l *o9x%B-F΀b3 Hi}Xы}u-+?<J]xM@-ሑhVw컩\Cұ ?75o_s:]]yn[Z%eo 2 NPx7Bu6~N꥛.|xU$ K҅ b-`6|Y;ֺdM xFd pf-%) BVDI5˫m\n.i I?IW 2@y LRQ$c,B@Pa20P1<53udgZ-(,䚆DQ6 u4ތ@]`݆rvͷue0&pw"t'wb u .m(1ud-|I׈u]J"JŊP'91bsĀm< Nx&9bI*8.lYOz?N(IrxJǙI o*к0*S!>wZ׀2+P^0[ӿ:s#ˮ 63I5aMM YOeT2û1 g,R~R99>Vνki<ڷYTSuZ}ZǮŘ @D\ymH4Qk35~qʄZhv?N82~^[:P^('aDXsI )qY֋8jbU]zo|![Ιe!w+8?}6ܻcEQ%LHvqBkq`\77Q&iq@ WLA*)ܞ@iq&PN}8@[h )־YQ5-;ECyoMFhԤ#3qȌ.O3qi7(*7W5s̒5^{@oa.O0FLcYX[QǞQ&>m`cئɥ HuV%s:FFLa\8[Š`E7 `LMwDA5}@}vOج; {Κ.OuZ!uHul*.:psBf3=؞/e̖7mW5bfbZq4;zk1wERD>zaՏߋhYפKVM|a'GbZ z$Wo_L=??y%4IENDB`bino-2.5/res/bino-logo-small-512.png000066400000000000000000000744271475415313200171310ustar00rootroot00000000000000PNG  IHDRx pHYs22tEXtSoftwarewww.inkscape.org< IDATxwdUB癞9!#"i0HaP S]QꮬsFQP@Qd33;+ ܮ^{^Wu=s(("f[EQEQG EQE j((JQ@QEQ"(APEQ(D5%4$rm9Eh" E"RW?O:nc6ٖMQPDzmN7 )XD.-(ޡE"2-W/7ѝ0yM)/P@Q,#"u~ׁB.#@vxl&RhXDh܆$NA~Eª`M (VQX@DhI|ܠbipjQ0AR?4iM  +|8o> a JږGQ€zDOs ܚG89sp?yƶ,T(U" f FgxN&lܺ a"ߵ-(JH9b:bu?ʮ}^c&l '=l=eQ EiDUdKǏT3p.M0)kWM (E)M/n]Tr~] ̇4L2""4?< t8n F)ʸ( MXju++g klˢ(~Dcַ~)t'x_?6`ý}p%u\vmˢ(~D E)љY[s5*u dxS-5efR\߷ofRLK61_an~QߢLgzx5ޓRze,tr!Hܶ8kP2m3L[ZKUL&;g,"V(v@A,\Dzum(B EV۸uv jS;>%/noo LN[³g+7uGQ2frHzz7NFkPHeGe1HDPhpȁlFìXseZl(F Eq=3){ l h/NʪBP$,}4;ж8 PTbѷchP`4-<=D ^7LqQ4j(J j`, Mz`+# x4f?,zM߭2VLxu3KJ=A-Ȓ?Jѯ)Eqd)2i~^D(JE(" \[Y@h狷A a9qx]m((dIsvf*B1똯=cwLQP@QJ`k]g~)\MA'v6mo~(JPO9&o:PL~!(|-X6-w/JS9{.2v(&ZmA9AB^gC2eSݼEQed]?N=?FHs"ܴLʄqũ#끸xYD>'",hL[H|x8hqrX7𬈼[D4 2.j(%";dlbODd/)@D,,R)UD-OP 3SmS{U"}tFDdg관RNDdo.k@eq*Eλ>("W"TD~Jv*}/x:\<""GFj(H|88"ہD Yd[ %H|<_rfL-b5" x\D>/"FdkTT|lp߅@eDGE(vQ TVJ3Yyma3"IQܲQ@ɦ8y??(BDso#q/*@p.QW%",-%<&"Hܶ0}D}d͹ieM"̶0w2Ӷ,> *p;a{.tA@my|dq 9"XDB6{^my|ƛɮ0AmxPg\[RQ%_z@D"I_rږnj|]0KD>Ϟz:Dd 5'co.RnWC%BDdp ې$輕l5Bΰ-K@HN6fH(Σ@ȭ_8ȶ<d"_1De LD4V"DD` }B6?R\Ig[o_ॷ# m 8GDrJnjFr%#%cIޯ3&@hmaP qdӕ겝Q dlk@mRrg`myĈՃ_Jm$EDVF5Fnd)1GV(e& j+"W]<öCDAx9cvY5F,?%"o-R>ADs }4#SQY  S#sߋ"A>A  "M }<ҏ Rꝶ:\`b5,""r }nv,N$QHkPMڀD:Yh[(%rjc$"fD5\$QQ]"` ;M>(bM# ` vߒzl"ac v7j84uc{ {+s7y1'i[g' << $wQL1EFc"bu-X-3n$JiL6R۞eh3pi?gcL ‡UrȾwKb\&LCQ#/_Bb48?E'?#h\x觚w=i;nMe "jTHw;r,~z؟lo?GQ~N+Rנd3S<7}d2⌃дEG8+&w׶TAC}M] (. < |jWkT7Z۫TLVpkϰ8>VH]N9 +U;]A$~3ʟ+"$Fn2Cqܷ7϶-QMH'w7T@05qܷʶ80٭Kc.[a[@ ~"2{>aGfjA "'Һ+<~x,9FZ=% ^k P7sONMb ^* }h,-P DwE= 9Hp֕r]ǃ( swgŴ=Wѩo"}amQl> ATD(]0+sκ5_B_7c֖{>.+>oF5r\ݭWɱ'p q˲TBJWjRԠ@M`~ 2k֞Ͷ~A "?->Ov_YvE $RX}+3O G~ 7}Xv9} 7E_ wm 2 1yJ)TZP]gǼG.o-W#zmIli"-mep|ML5J_ jP4w'(33u1笿h]lM k]Nḇ-Mº,b&?U';%p!۪4cq/Dc v٠@) 3r%̌Onj"t?k$1mq]bv--";!*Ǒ SԤnR#`[0̛R)KmƱm9lI"e:_ˁ@ (ܛgNѴ;>z~xF$ epӂ` deY}I) +XP`n2ۈOo} C5ό5.| ߈9G"ږ#L>}ҏ!z&q%(-ʌ$o;4.>bSh"Uh]w;xY_\;S~v"4-]y\d[/.|{WN.g@(a-^i@X]LHmyE-DC盢Pa ~(ѡ<@I42eUJe4-;՟Ӷ^ ~5-Bk5@T5 Hעxucj'VSi۪rbS$ٱmP?YrHV')/7TCJ~Azi\|d8Ccn;m8~ӬV؃p2pSPy/grןI@I63WQ?h!:g}`۲M=xm9*܊TR'mES 0?ҰలJqGh?Ͻ i \ 9^r:eP$#yWp]+H۶nj" mˡd pP&Xy"_  dNs4,\h[7 { eٙ9'X ЎJ?7Z 8f>BF/q:N) :g@hzr c[e,?$p5W@X)y)g>[Nӏ6M+N| IDAThXtm|im9"Vme\{PD9N)rlmH˞MDKm53lˠL9?W?Ռ+9WgdP}c+(^S?D. EP+5S\߀?8d^ Fc ltڏ//Z&+iKG6J`[</`u纩 erFyJ)@ o(I#;1 g nn #+u&.dɗ#/I,Xm43R&K5&%ZT6R/WOY=V7︂xӌDC-ӄXږAeρRkWc (a#_1яWW)PeJGcCEݬCm$1~ n8xL>0;%l8FxFEvk' I69v]ַpqg0 (`»>Y8m)Ѡ@oVQ7mGm[8 4dA{$G g_JO@OHOP28E(<iY slˡR*r_ܞP?Zɮ{ r8A( mO Zbyyun{a !5-'~ F#+@"5p( pmw|윕Ua6^Θ ڏ_SMU%$[Ͷ N `Yei䕀WC )uRA]aS=ouJX$gZ("#]Am9oYC`(RrWn޴xXۡ_  @>l[;b@\0Fko!RX(aE~mj%gѺ\+Z ?WZ(87"Um"^*-CXXTd#?WҏtکWhDV=R@r9r sEöe@ORdһ*3d:ҏ$?(0Na_pZP% dZ:gmtÙeP|_Nx02Q_AmkBq+>'qmj!@-b.ؼi4.=5q`kumj!\ݶ }.t Z<^nxlZ=ql]ض Xh-bF f S XWo[ebS]X?e -}rm9%V+#?5LcVDâ#\N ɦ-Fv`%O5aŏKFѥ):֐ݪ$p0-/F=J1 7B`%@Ěa[j 0ږAoBcb*7fM2xnX`*FCUf _p<`/b$f:ۖ6ˊ@iBdVۖC' sPS ^'INJi$YGږiteP(L1Q|WKxl%TMqѶe@&2(& |N=D PIP7I4y'H -C5r`3jl #g~l ;H "5͘i[j= ٖA ]Xy@TAMKR?goDSO|ʂf2TC [E:FWy)ʤ|hF=%o/g늽l`ʛ?xJ5˴C.Xf[J 8Jh*UcA-{E%][$R(@Ӂ+(D1oAd -{}h/[-CPCF J6>W I.=}<XmsNՏg 9Ie~Li'M yZ`m`RҏT@ɟ/ز9H2܊O RSݶ Jp9l|TG oAy/b7w8Zlˠ^=&N;exєPk(XHlPj( *ax_^4.?ɂ\J5˔?i@9IB('FvYzD}ml$,4u-B%xv-JR0t^<@]n_F-(drDa-DI}d+e1 +h?rIwg5i2H l$eam6z`{$٭M˵da@$їliwY=L0.RfxK_ 2bZ#9s_HҵĚfL$3{ м4,9UXqm*!P@ps4 ;\*JVI,I3m Ý/}טn.vH /axffG뚯R7_Mdc\(A3ۖ!(  \ ;n/cd8ؾL7M@F1D_t$/sZȤw֋HnПS;M#s C>80ɦie@)e} g>:)dMnOImaG5`2cOJnwW'xHd};l1׻v=a"o T.@ i[e%;2ߣ2 L_W{M@)dJf,Ld}l5R/4c$d.6ƞ0/?G"@e P>X,pwzK#{˔Od[&/JtUzͻ+J6?–߽;/ ([M<2 [}WE6)Ml^ ƒ7mH,i_2p lt?M S<*1~49@+ۮ^Gf"ɖ@@ ۤ@I>}dOED 8HIkZB(>. \SSUxТqYo&a[ 1a7} 3܋#uN0>/HL!J_W 9BT4-V$*Z2_6LP2l>ͩtNJvϓo%Fs+!0C@F6(iz!0o _-DxF> { p$zmxc ˎ.G2鑞m3#{rzw2(ϒIZ/R$ LJr~qp9,'Wl8Dcl1 t=]۞Ί5vf LlK' Rj` @' B{;xFjYa Co<}߯$ݥ{o2tyN&.-e(h l G%J?@7׍vyg 郡LnmzdІw~ >Eʷ: MG" Vɘ&EG*1w7lsWJEKj}O]G-]T';p*(bsH,0i@^-D*s XggpK7{ 2€MWAm?q>Rh&@@vtFר61%a,7d'sbm&Wǝs"(oɔĈ`mP.1c(@]vFc jC7i}`>LpZʆ| .3IJM ҶETYאMgP v(%ݷw&>OrOJ5uRA]aN?[j` (  lQS|/ޞMx(~d չ 8Ÿ0I/9  P`l,Wg1y{6NvRB\Ro<ڵ?V}L_KPg뭿t q1%2\m[q0?RJ2%==MG.NR+㕏'S?s{`dL,0z50tFZ_V_yNxZJ`0 w+q_3觔e!dJ8Hٶl;ȥ!*¯d˻j"ž+rWD᧦ڝjMzWoR2 Vc"ӶE/_aPgN}A3*>gxNj?e4iM~/7L`18m Gn :~NϚي;LfWn.-$&(p'0Qi2 cx;^շ`k Z=/[l@|Z~~} d SO&kw~<߈3 _KQcxNWAȨ "K\%k)~}NQ xO?vW{ǻ" .,𬤀gy)ZހA)Et돝TS#C-[A;^^ZLʯ+ *pit1jiCQje[{jleK7RaxSc8̘L&0*0@2b_+UzbVlsFD Զz:cUEԨ{ Hm~S=ZԔ]bsJ諣&O7m0OsEͫĦeȐJu#7ں3I_[sf2Ce(xvmp_sT"w-,b]u;,(0RZ0"I`?{H1t|:om+ >A>Tzf9 E%P/Ų (}8 yaɩe"6Ptl}A#RP/c/<$* 12<<(99=)8+"χE ';l[)tϫ%.;dR>Ͷ(@oD-(vDŽ0fˣpm*J1篟~FMlg (Rܺ(1dp@GBE^ѹysks  J02}?x13p={v@/Wd ͯ&Aj ~qx;&l3,\t0?61dDJuP9J^QtaGz( ؖ lՌ+97~*njMB~JI4,20wMU&w;$[ˤ7b1`rFpx6 Χ7r>cqɛ1׈³wmȆ@H Evh3'8ݮIYJG?=A  5Aj<IX ֐x cijip쀓1 tEOT7Rvk$S`k$L/PԠy^6E;V=.9"lkyx} BQ&'v\FWO$L>*={Qyq{y}9y`&p.4P@ 6ٖm2$elr ح&@*GI+3*&quf epncJg\X 0»S.tv]sjx'MT&X) l~?.f?0I `@IG)J->Kjw2Wl_ ?j"KsPZԃhGNdmP A3^-0UN{], Qy2]ήs忖['+3 ]ex P^@umfeGŘ+'WB4qՀdGٓ{|J_%뫬ǓwM zɶ (`I 9VPuuje/F P9`Hm77{%G4 z|>z%RĨ0C_ŝIh[J6ce-BRLPjyR2W7PGj[p]{hj|#`bOAy)0@%ɫC]gmKQ 2@?"*l=d`uկE)MG%/nω;IYn`nU8hR XAaW\?ѹʊ:@3*cҿČ<+KJ}3FE)3i `[7#ҧsf-ZF3$p׫=ןN3m*Ž,lP/yJ8֘_.A0*c?~/2H.*E$w0ݬ;>; |9ߍ9?&GlP)9mQz~U_JtzW}I,{p#"(0VGrm*%pZcz\SpxyDfWp U3. ãT1 x9=n5ye+2/g] IDATcAt84_) p-MA=i}m`IwÌټ^/%8;_s;vz#uA$a%ӻqж H`l-[D@ / jc;ګ`0u^'{֋od:j/Hl \ږ-xk:E84Z㑕0d}ų$~hQHU^Rzdk@ܱɶH?Ļy(Uϯ7p"|C1f!(Y.ӧ wُf9I>z~yFZzǓ7w<^g^U2/@i o[- 6Z:a9-=/s mR})O[ @fq2TC pmܢɅ6]Rnԫ#U_8G5JґApۖiOt3եv8Ea]S`]]92C=~UH/R%뤂O_z,c^m1!Ƙ7϶nGMp|KqLϛ+yzwRzdk+ utase娆`֐q(U@` /AnӷeG{fpO_Q1zr>qJHwc ? 鮀N(lG(_]|n TR&͐WӇq/KyJ݋Ndnk@ .'hp]?{3ɥ8^n ^Kl/CX^û<)bL/z#uwIXn39J` aޕ┝® Ư;x wf2w7G9> Iu~_lm"`e~91SvڠhaVƫaU_` `yy|+ ȤW׿lP-5o--Lsvu  aLOϼ.FJXI;G/Ϩ=TNRVz4}zlږ f^+l Im׽o؞U{6vuϾ? G_SNƚfZntK=Ƙ ml jIecRA r@Ȃcm+kȝkr[_{7v聇l|T,)lǽ180pW}c(; v^WqtHc.R5Iuvؾ bWꀶ  {*8hOk+G0C򢟜 qX-uRAw}I _" 3e}pm`A=a}mÉx<>^n cC2z//{T|Q[^Rz ycSF2[n[Z Wٖ Vpx8 < =.M= jDF(([Ynt-%u0fæ^\AaOT_eMF0ߣy&F"qz؃㭋w矣fޯA np,}n>jf{]"G^/I,`le"FANck'y?PR-Ta ?ӳ!{-X­OpIDn9 2C}(vȤz^rYrb5JU Q?R34vt m1]ep=8 qb~\& ݇bI=vX}^-Reex)r -Sl/rZ]E"[ )̐DDң켧yQ6y/n?5/+Ѿ+a nζ NB/diKN5;qp_"e's'VN*@J]Aׇ"Bd`Lrg?*}|` vpdҽWj_+ѿ'WA .DؾN8Bx u߶ NS< v 7 ]u"7>WGe坲 kKq;Jw./=A-ӔvCO")x}oT{V})ƚO}~}9nbnQ?"Be,_ eoUh_ `|',Zt0VۊLzuږgMŘV3{C [ISb8I u;l$+*lqad8;pڎ!2 Xx Df}%r8I 3v5epV:цEmLI9@Cw<&Ɨs ߥ7J؇FR^:~4d[9P(l$҄LǝnSK*s<ã@:]K>ʫm#6l}b8M s|\! 6 j07 m0]2 ^ysN7_T+w?x-%ګtT^">a;QBbބj -4|]6p  #)PWgCD.ᳶS"mB  ۖi]o^~Io aی/Bl_*RJu"pL-h{C?Q؎  JnbC)'Ɏa짣.AAb۟m1 Ƥ X9Yef]a ߬}ϵ8S>G 3OkbXh ^%e_{_fc;Y=jBilٖIځ v(E>Rsl:G8/)1G;Vp|QvޓؔĚg&2/=h[ؖiͽ]a;F f pX۩4zj+1Wn;(01 Fwm5>e T܇*|}"{zWwSq׉vd킌H?Rf„ܖ{p/£epU@+Q.ȗ7pDS)NGsu_ N4(T{+rD{YN'^4_h -āݐFzMkmC5Oԯ8k%9ջ%︃Dɺl_Rέ3Xe&uepPM-! i?Q_?O$I &[=8ǿ)H\G4(01@lkGK&Ef!Ą'c}(9VBM v m>M ƓC% 4  Xw!Pd7Ao"L=5pzkP_m$m. v\>ϭ! p(VCBCfLybLv^x{XlD{l67 ΅sDaQ.W. ;o:;5qVgs$IC!-B )IHB[Ph~/\(t.) %$$dOǎx_$Y<cYlf#K̙."M |ተ#vI]nЗ0tWd+]2 zLnCH,+,nu+bygOgB̳Kȧ?#by]DXi[`&x/ʎ#L_@94&Kغ%@nIJh(%g=[~."24K.ෲcb3a' tCBYw|1 ۃ `X_I' ߀PC2Xh)p1:w,a'"q&حd~ Y&GN ko&uԻ.hM?Ȏ#,Q!`a%=tLuoBi ~Ѭ # ƺl̵HvEv=uձעaχ/nˋ.QrGÖ_3 >B~c])c"/nS6!"隿_G0 (i1gܚHdDIkOy (*DX=5W->zAvb`0t ɎCODϲ2 ,Y/&Pb:? zJae&@lGXXGFS'D 1!'ЩS}Ih%b.[CzK'u+M W"ճkvVG=,2;å&N؉﮻ H;2z{oZFŜ}P~-ǛTe'N \G!;dTp/f A`܈>&l!xB\(s])ig!z苶u|s>[l(EӠy֙Ka,,Y12$a'"f<#LaU7!|%UĖ0)0m6Ie PJt hƪAl0S83B|5Hߤ{R{~xJH؆ њ,-'W9";]" [25iILR7Nnf6Ry8dl@P%lџNAe2[$gz/'~[1E)*5E0aDd_n,]x/;,iw7r5Zf@v&:Fz8~/ :G/"Ѷ/߹n_ıh.;o1A26MV3X {*"̞/_C蔵Oꝫ-̤@`6ƽ3dxX*wc[.B'R3> 7h!,d$j M ZQEv Yxe`B@̜^G&"{e& z?O$ط K aOIGOQYG}"꼍OJPM~.ߛd!Z5A$].XBuYY [L'-?t{VOO#;W8-ވ e0`kգc0.P+{XJVι̖/߻d5PF;W[R7ڤ@u"(Y`6CNwr}x>|ض߹n_ı'_ X@sCc0.<@}7p\vp0_;0uLh;WeL Tr:fDy_W\D8 Jv ܈ [x߼DPNݷ{ROi+й\Ѩ0<2Ȑ&8@ɯNɊ/9<BBC P!D$H??FD,×Gړl2mP:kP 3A姲0DD{ HkJrNzCF|!Hf tɋtODz^uuߣD``]pг;N撃P!e#tfNB͂*yR @x5DI_͇ݲ0".zpQN q$B'l+3;%wL{;W5)P4`V]~KvF@xH _7^#'l~_1H^nR}' [WF\͇Je`T\D чcHׇ=6bc7Dh'#IE+*-?G+*urS^ָޭQqЋj1$|_A&l=ˁ `[:DtI]n':mK4ݴwКRv F@/ z,{k" Hw|C ѬȖ7hoI螾e##ׇ×mEz>fhG=8 >0鉁:dX ^H0P 2IZjR mj4s'2I t@9xdǑH%.Cj%XcY6| `+YC!2 н@g'^e4 PvF@_]'\`N}'ll$H=/ZQmm_4 jI͇Dem0:.b eǑHkD1a0Q "vt~;hw[EKR m@>$'/;T`ONO,^GeX#ٰI&?fAd [' 5Ynڲ]yO\Ĉ &B'ù ZbSf~=$USM TlO_%kyӹIDAT$b@%rn@v,.?oʔ3 lg! @ 2Zmٻmm 8uM.(tt0J3;H=/ZQm\y;IWɎ#pZ/Ɏ#Y°Dzv.;lCBm-[mnhQG-:&7ܴmmJv 87pX c$l= aMdgρR0 }NIsn.o#88q9dǑL!4`x"{fU@[RMIP'^)g)i>t[X\ˎ#Q>(`,DmȬGa7E@MQCpUu$|+dǑR"iq$7,>6$V*e'gEP/).;WmC>V cp/$D!ى g!*Uֲv؂С")1C*RXW>E7!pt30@7ێ>{Zs]X>'T%Av./5Ui/nz $;jtVAF0'W`Qq "k~o{zߖ ۠ =[Hf 5lA)ǟɎC|XvI?" o|7E:2 ч>)P 2 `޷VN+뀀Xv%0>gџ k&Fn8=zROԤ@%o<쳾[?shh8t8̂?Q:)%j ZvF NTXc!,&^SvY?=w}>JQQuҵPIuT[7Gˎ,x/AC1 >5q ;o Y6>E< 0&ZAc'~7lJ7jߋQ+9uR:yT:bdUӝbmb{5##t~2)#З1yPf Z@5;__<O ,|8*F;1lx֖~G)|t.P &A4J,ְEda673&=[1'pv#F(eIP h&*PPk(І~ ѹ+@ڷ䌆;""-MKIs(,t|$/xaq/'VN*@(@hBaŧ8GmyB@¯5"H/P3eGr귞v-\$П8:+;z»QFϋ UB\.q֌1}WsO.~ۀ`,:٧"ƭ|%  v7E/c9ۼ]-;YFDl$QFCdX-nMtvP̎G~Ȏ1ƌN䕶I.Dkc[SLvV8w?] GPM]Kl_v,V#Ic;@1 |G;ޗpd=;tMc)kʎjx 92& `9HMeb9< =D_Wu-cFA;oݲWv(V$ xJv1& ~KvVDc%]syWxd8`,m |!Q&;+"j ,cƘhSe L;Ȏ1f=n5¸HacDrK9+k|-z) Ƚ|Dv,1 h-𣢄^_(;60|-(%_/a5pr.o<`"%ف0L@_~{iᙜ̓ YEtng>d'fX ~Pv(L_\ \ca0v";?`RmzZÁO<0qck{Ae%}UAٱ0RNon\\S40ƌԴobڼ9br\XDt 1hAJ[CMO|=YO AH G!$9U0rsrx7_(ep`1+*`g%;d4Kh x^rh9Uʎ%`Ag\< edBv d*@ ma4\A4/ٱ09x0i< B(DI z wʣI [GTљۀxdH>+ ɣf'5(]?Q rpw<{ڸBv8L..`Uaٱ$[/N'={䰘Nx[k|6$į!:]J:rVtca;mѱ&~ Dk ݭpɕD+ɌXmYy),U 4VrAo{WF-Y,`ݬ"f441Pf]EXb\/X5_Np@AQ?ʷi gQGˉ=#Y #qcKG;Xs5e:43h'2Uuh. lCӏQq[tf١0֫DeWw5)J|?ϻ7VZE{nj|1? ,&+~MC}[v<P|μGdɣX'"UɨI~gv_>!X\n"CBj l %ΪUGD55ɸ虥Z޴g;>?%N w{L c &>O^,Y,ܯ.L5Uʎ1#Ӱ󉪜aէeG4yo}Ýcä`,'7۔{(wܧaCvt0X/_ې^x) 4H hG^>Aٱ0ft/XP{it+2^ _Qqᷫe`Ǒ#J3Av4XU)gq]*73H66츹83;.7}e;OG%;axӞݙr_YxK5 x=3s\:c#e/;\yեˎT# %nћUGsF?Ҕt ͇]c:&D1sUOWHNB#fymխsk_ca 0@t2e.0㹼k2lA7mC/]v, 0{H+(gNRa`XAϭyy^X3XL'M|/ʔS$#vCݼCX3+X]r}۩C_r4lܽߛۚɟnڐ3jvCxwPvHF7-EEGxKK6ܛL46fD:775Ab^7f&fS0L% &STx+4r_CƓ?[aEgE|n8辢xFyµ;D$R4[.R&(e)F.zI"R)"o@-("Edzm9 C$@#phU"2g dckdWȚFQeN8?\*"W z62Ԏ^F4tG8pP{;~@wW:}zPZ ""4w}捳r`'iVD^tlJw,Bv`p̥loMn@@3R'\" +?\o\R``RH|^~48HƉJ(>OF<`ikoo.X*bT"u}Oρ+lDq8hzל!r^ɐȯ}}ç1KZ(?LANJ1C 40_H7|gSVS|F,x!ܖʗ@B.4s(ɗ[{D[FU&+`90-ͭCLtߐ|9uZ^[4Vܳc3DK+YC_ѤB[`EiPUx8Us9HVVYE)`@ҵf,̄v\pZ玛̤fEa\?FҰ:G\@+MomǙlR{޿9Ŋ@7@ɂ+]ގolyJͪ))/4yehz0^$wB)ӡZ[;$uI.83l*x@tDD╢?Ji 3ؑ2! m>"J}KŸT޸uEJQ H=$㳾*N b뛑fnЌɞ`v4 R)OjktgɏڽvC ;v7S S!1b[k= {ZP:lA#Z'0%tʑ͖EI==k/'Ժ5e:`‰İ`O˃n!6#A'T,?'^^mSiUk`R͇uOki@k՝kWt~݇JDd곔XBC@Kvlj=_ynm=7.;3ЌQA/y(HPK9  w'/bEnG㞸c~s AP3rXʉ߷N[C 8$ |K ;Jwi7{Y0y>`JtkYm @#)b9y6Onm M_f'W)SBcNJmrGz.r# }]k)#ܴ9_ nc1.xa7港4haEڋV8h iz'Gl,g#2ύD3Ft @01#;\-'x7JX~U{g*4zA"w 3ˉ.%H2(;ӠI`7`݊tqHnyd ,T%;RJ?l)wp!p+#aWo͘jIÌZ8ch0^ XTm \;WP9mz' GZS}'+U[SZstftR~^5i06-aLlJFN*8 [{w:BM;tw;ѫ?Wn!| m.`qOVM>m PGhvOYzcCJuU+JA^!5+yvc=Z8GЪi o,j)4OAWo]zCJ+y՛J;ig"c@O2qt,_GwR8U*xgiيC 6@9ohWT)=ɸggG:Pr(HS7A3_G}Uϔuɋ%Rm ^xO.'8?YZu'S1o=IENDB`bino-2.5/res/bino-logo-small.svg000066400000000000000000001142611475415313200166260ustar00rootroot00000000000000 image/svg+xml bino-2.5/res/org.bino3d.bino.desktop000066400000000000000000000006701475415313200173750ustar00rootroot00000000000000[Desktop Entry] Name=Bino GenericName=3D Video Player GenericName[de]=3D Videospieler GenericName[fr]=Lecteur vidéo 3D GenericName[pl]=Odtwarzacz 3D GenericName[ru]=3D видео плеер Icon=org.bino3d.bino Categories=AudioVideo;Video;Player; Type=Application Exec=bino %u StartupNotify=true Terminal=false MimeType=video/mp4;video/mpeg;video/ogg;video/quicktime;video/webm;video/x-matroska;video/x-mng;video/x-ms-wmv;video/x-msvideo; bino-2.5/res/org.bino3d.bino.metainfo.xml000066400000000000000000000044421475415313200203260ustar00rootroot00000000000000 org.bino3d.bino Bino 3D Video Player Martin Lambers FSFAP GPL-3.0-or-later

A video player with a focus on 3D and Virtual Reality. Bino can play stereoscopic 3D and 360°/180° videos, and display them on 3D displays and in Virtual Reality environments including SteamVR and CAVEs.

org.bino3d.bino.desktop The main user interface https://bino3d.org/bino-screenshot-v21.png https://bino3d.org

Improved meta data detection, for automatic selection of stereoscopic 3D and surround modes.

Added support for HDR video input with tone mapping, and for zooming in surround videos.

Added support for slideshow mode and improved compatibility with ARM platforms. Fixed various bugs.

Added support for capturing screens and windows and for HDMI frame packing output. Fixed various bugs.

Added support for 180° video, playlists, and more output formats. Fixed color conversion bugs.

Rewritten based on Qt, with new support for 360° video and more powerful Virtual Reality support including autodetection of SteamVR.

bino-2.5/src/000077500000000000000000000000001475415313200131035ustar00rootroot00000000000000bino-2.5/src/appicon.ico000066400000000000000000003122701475415313200152350ustar00rootroot00000000000000 hV  @@ (Bf (W 5_(  1*)M:F, _V TPbbbbRـd d LTd GGjg ` j-[&[ \ UwZd4Z i][&H:i< Y.$MI lgMI9R$lF &nnnGo*<nNPpo_%[* pRs`^hnsO6u`>nuuF 1 x!b3\Y3 d z$ae^e#H G#@????( @ ,-I"7Y3 ^Y?<0___F.U!```SZ`]PUaaaaaaaaVFbbbbbbbbbU`d d d d b Va d d d Lp0ele e e e @q0s1X e e a C.JRdwf f f ^ s0i,`(b)_ f f Kmj7I\o gggU k-b)X%O!;gg` 3}$/ATg)iiiO"d*Z&Q"GEeiiA&9Lb/"jjjS ^'.R#I?6Q jjO h 1D kkkZL A8.< kk\hR)=e lllhM Bd:0'l0lllt: {QC!5"lmmmmJn<"2) X'mmm< QwRooooW+! b, ooo@ ws[ppppnDZ# ~: pppB'qqqkqf9 bqqqCq rrrOjrkDVrrrrAq};v tttL~9 tttttttt> : uuuK ZuuuuuukU vvvJgvvvvvR/ w!w!w!I  ^w!w!w!mA/J[x"x"x"I]- RQ>Jwz$z$z$Hx%~{%{%{%N$}& z&|'|'O& 45c }(P&  o7L& `  /b $N O? ? (@ @&&2A23F#]S=CX@0]]\G)؎9U ,i ^^^^W >7x7*^^^^^^J#@PYK=z\_______9XcUNKPYNj<{]```````9XcO\```VOk```````R ```````TJMaaaaaaaaaaaaaaaa[IM]bbbbbbbbbbbbbbbbb^G]Hbbbbbbbbbbbbbbbbbb^GHccccccccccccccccccc[Fc c c c c c c c c c c c c c c c c c c c Pz^d d d d d d d d d Z FHY d d d d d d d c Ccf`jse e e e e e e e Z ~5u1p/l-?b e e e e e e X$fS\foye e e e e e e d =v2q0l.h,c*|5c e e e e e e IjEOXbkt~f f f f f f f Z w2r0m.i,d*_(Z&9f f f f f f Y {t8AKT^gpz f f f f f f f L s1n/j-e+`)['W%R#L f f f f f f E w:4=GPZclvg g g g g g g Co/k-f+a)\'X%S#N!X&c g g g g g T s5'09CLV_hr ~hhhhhhh; l-g+b)]'Y%T#O!JFH hhhhhh@ 89",5?HQ[dn.vyhhhhhhh}7=~h+c*^(Z&U$P"K GBY'hhhhhhF (1;DMW_k99pbiiiiiii|5Pif*7_(Z&V$Q"L GC>9X iiiiiW q+o$-7@IS[/|qiiiiiii8Ev\(W%R#M!HD?:5? iiiiih:,7  )3<EOU$jjjjjjj>5Y&S#N!IE@;62['jjjjjjA %/8AK kkkkkkkH#N JFA<73.0ikkkkkL !*4=E4kkkkkkkPJ 0GB=84/*%]kkkkkX  &09lllllll]C>950+&"Qlllllj7|F",6mmmmmmmj@?:51,'# I mmmmmm9 G| (1XmmmmmmmmK;W62-(#  D mmmmmm@ |$-"nnnnnnnnY:3.)$   B nnnnnnE  #nnnnnnnnlL%/*%!  FnnnnnnJ oooooooooR+&"  NooooooN*pppppppppkM +'#    _ppppppQ ppppppppppY#    pppppppSeqqqqqqqqqqqQ    o3 qqqqqqqSzqqqqqqq[qqqq@    fqqqqqqqP0? jrrrrrrrp2 qrrrrYO# 7\rrrrrrrrNNsssssssR%TsssssssssssssssssKLKsssssssN$M#rssssssssssssssssCM!HtttttttK" Dtttttttttttttttt@L FtttttttI! etttttttttttttto@ = CuuuuuuuG  E ruuuuuuuuuuuuuXX  AvvvvvvvC  Y* vvvvvvvvvvvvvGyt >v v v v v v v @ x9v v v v v v v v v v v k@ x ;w w w w w w w > l4w w w w w w w w w w JL 8x!x!x!x!x!x!x!: M% ox!x!x!x!x!x!x!]@y 7x"x"x"x"x"x"x"7 Ft!x"x"x"x"Z= qqyT 4y"y"y"y"y"y"y"4X* ?|<e1~ 1y#y#y#y#y#y#y#1B 0z$z$z$z$z$z$z$0D X{%{%{%{%{%{%{%W+' 2@jo{%{%{%{%{%{%{%f20u?4 jo|&|&|&|&|&|&|&i20u jot$|'|'|'|'|'|'i20ujo#P}(}(}(}(}(i42ujoS L& p#~(~(~(i40ulo   H})~)i40u lo C   ; j#i4.u I*    D"3-  @- 7!?????```  0?????( LL5C6KG- [D+рUU4Y \\S ;Eh -]]]\K!y6Q3t ]]]]]V><! (]]]]]]]M:B#0^^^^^^^^ZC1&(%^^^^^^^^^^R??=. ^^^^^^^^^^^\G(64 _____________T>J>6______________AGSl6______________AGDQkTSROTU`)6``````````````AGM(U^`````^TھNb]6``````````````AHL]``````````XLa"0``````````````D5K^`````````````TެL%i ``````````````R]```````````````\Mwaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`QaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaS7aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaVJ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbW@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbUM bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbQcccccccccccccccccccccccccccccccccccccccLWcccccccccccccccccccccccccccccccccccccccaDec c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c \Nd d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d Pid d d d d d d d d d d d d d d d d d d d ] TU_d d d d d d d d d d d d d d d d d c Cnq kotyd d d d d d d d d d d d d d d d d d ] ?y3w2t1s0=Ud d d d d d d d d d d d d d d d ZM _dhmrw{d d d d d d d d d d d d d d d d d \ 8z3w2u1r0p/n.k-=` d d d d d d d d d d d d d d d Kv`X]bfkpty~e e e e e e e e e e e e e e e e d <z3x2u1s0p/n.l-i,g+r0X e e e e e e e e e e e e e e _ D"SRV[`dinrw|e e e e e e e e e e e e e e e e Q z4x3v2s1q0o/l.j-g,e+c*e+S e e e e e e e e e e e e e e Mj$LKPTY^bglpuze e e e e e e e e e e e e e e d :y3v2t1q0o/m.j-h,f+c*a)^(a*U e e e e e e e e e e e e e b @(JDINRW\`ejnsx}f f f f f f f f f f f f f f f V y3w2t1r0p/m.k-h,f+d*a)_(]'Z&b*^ f f f f f f f f f f f f f KuW=BGLPUZ^chlqv{f f f f f f f f f f f f f f f Gw2u1r0p/n.k-i,g+d*b)_(]'[&X%V$r0e f f f f f f f f f f f f a > 7;@EINSX\afjotx}f f f f f f f f f f f f f f f 7u1s0q/n.l-i,g+e*b)`(^'[&Y%V$T#R"Cf f f f f f f f f f f f f I 559>CGLQVZ_dhmrv{g g g g g g g g g g g g g g a v2s1q0o/l.j-g,e+c*`)^(\'Y&W%U$R#P"R#] g g g g g g g g g g g g \ Uzq`.37<AEJOTX]bfkpty~g g g g g g g g g g g g g g Y t1q0o/m.j-h,f+c*a)^(\'Z&W%U$S#P"N!L ~7g g g g g g g g g g g g g B w(,15:?CHMRV[`dinrw| g g g g g g g g g g g g g g Q r0p/m.k-h,f+d*a)_(]'Z&X%U$S#Q"N!L JHX g g g g g g g g g g g g R x$d%*/38=AFKOTY^bglpuz ggggggggggggggIp/n.k-i,g+d*b)_(]'[&X%V$T#Q"O!L JHE8gggggggggggge;+%#(-16;?DIMRW\`ejnsx$hhhhhhhhhhhhhhFn.l-i,g+e*b)`(^'[&Y%V$T#R"O!M KHFCFa hhhhhhhhhhhhG }!&+/49=BGKPUZ^chlqwt1hhhhhhhhhhhhhhAEh-}f+c*a)^(\'Z&W%U$S#P"N!L IGDB@=P"ghhhhhhhhhhhh: 9"&+059>CGLQVZ_dhU<viiiiiiiiiiiiii9Hd*a)_(]'Z&X%U$S#Q"N!L JGEC@>;9U iiiiiiiiiiiiE k $).37<AEJOSX]bh 5}iiiiiiiiiiiiii=@^&_(]'[&X%V$T#Q"O!L JHECA><:7=iiiiiiiiiiiiU t*"',15:?CHMQV[`h.iiiiiiiiiiiiiiB7_'a[&Y%V$T#R"O!M KHFCA?<:85O!iiiiiiiiiiiig5   %*/38=AFKOTY@)jjjjjjjjjjjjjjG2UY%W%U$R#P"M!K IFDB?=:8631\ jjjjjjjjjjjjA  #(-16;?DIMRXT#jjjjjjjjjjjjjjH*V$MU$S#P"N!L IGDB@=;9641/G jjjjjjjjjjjjM Q !&*/49=BGKP jjjjjjjjjjjjjjQR$Q"N!L JGEC@>;97420-u3jjjjjjjjjjjj]|'" $(-27;@EINfkkkkkkkkkkkkkkYQ#_O!L JHECA><:7520.+Ikkkkkkkkkkkkk5 0 "&+059>CGU kkkkkkkkkkkkkkbU M KHFCA?<:8531.,)(dkkkkkkkkkkkkB   $).37<AFkkkkkkkkkkkkkki+J!IFDB?=:8631/,*(%VkkkkkkkkkkkkI  "',15:?B:MkkkkkkkkkkkkkkkL QG2GDB@=;9641/-*(&#I kkkkkkkkkkkkTq  %*.38<lllllllllllllllQEC@>;97420-+(&$!= lllllllllllleU{4U #(,16<lllllllllllllllXDA><:7520.+)'$" o1lllllllllllll}: 9u? !&*/4:Klllllllllllllllb@D?<:8531.,)'%"  W&lllllllllllll? z6 $(-23 7mmmmmmmmmmmmmmmmI ?++=:8631/,*(%#   M"mmmmmmmmmmmmmE - "&+1mmmmmmmmmmmmmmmmS<9641/-*(&#!   AmmmmmmmmmmmmmM$  $).mmmmmmmmmmmmmmmm_97420-+(&$!    7mmmmmmmmmmmmmT "'+^#nnnnnnnnnnnnnnnnmJ -8W520.+)'$"     /nnnnnnnnnnnnn]&  %(-nnnnnnnnnnnnnnnnnR7%31.,)'%"     7nnnnnnnnnnnnnf 0 #+ nnnnnnnnnnnnnnnnne@++1/,*(%#      @nnnnnnnnnnnnnnx-|'9 !zooooooooooooooooooN/-*(&#!     H oooooooooooooo}9 -~GI oooooooooooooooooo_-+(&$!     V'oooooooooooooo; Nak \oooooooooooooooooooD b+)'$"      x6 oooooooooooooo= hr ooooooooooooooooooo\*'%"       Foooooooooooooo@ x{ ppppppppppppppppppppJ(%#         XppppppppppppppA wpppppppppppppppppppph; ''#!        mppppppppppppppCO pppppppppppppppppppppV$x!       W'pppppppppppppppC qqqqqqqqqqqqqqqqqqqqqqM#}       JqqqqqqqqqqqqqqqC}! qqqqqqqqqqqqqqqqqqqqqqpF       %oqqqqqqqqqqqqqqqBu e qqqqqqqqqqqqqq]qqqqqqqqou5        Gqqqqqqqqqqqqqqqq@{i?z yrrrrrrrrrrrrrr> mrrrrrrrrr@     C qrrrrrrrrrrrrrrrr@oN1, mrrrrrrrrrrrrrr}8 Lrrrrrrrrrr[?  5jrrrrrrrrrrrrrrrrr?U/x"FS mrrrrrrrrrrrrrr|7 P$rrrrrrrrrrrqNS% (;nrrrrrrrrrrrrrrrrrr;4 1 mrrrrrrrrrrrrrr{7 ,Rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry6 P/ lssssssssssssssy8 *K"rssssssssssssssssssssssssssssssssssj(- kssssssssssssssx7 (&Sssssssssssssssssssssssssssssssssss`@&+ jssssssssssssssw7 &$>osssssssssssssssssssssssssssssssssV $) ittttttttttttttv6 $" AtttttttttttttttttttttttttttttttttM!"' ittttttttttttttu5 "  $gttttttttttttttttttttttttttttttttC0J % httttttttttttttt5    L$sttttttttttttttttttttttttttttttt=6# huuuuuuuuuuuuuur6      Cuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuh! guuuuuuuuuuuuuuq5      _uuuuuuuuuuuuuuuuuuuuuuuuuuuuuT#  fuuuuuuuuuuuuuup4    *luuuuuuuuuuuuuuuuuuuuuuuuuuuuE D evvvvvvvvvvvvvvo4     G!tvvvvvvvvvvvvvvvvvvvvvvvvvvo9i evvvvvvvvvvvvvvm4      n4 vvvvvvvvvvvvvvvvvvvvvvvvvvTOz dvvvvvvvvvvvvvvm3    <vvvvvvvvvvvvvvvvvvvvvvvvvBU cv v v v v v v v v v v v v v m3   @v v v v v v v v v v v v v v v v v v v v v v v ZQ~ cw w w w w w w w w w w w w w k2   ?w w w w w w w w w w w w w w w w w w w w w uBUe bw w w w w w w w w w w w w w j1   `. uw w w w w w w w w w w w w w w w w w w Q@ bw!w!w!w!w!w!w!w!w!w!w!w!w!w!i1   B kw!w!w!w!w!w!w!w!w!w!w!w!w!w!w!w!w!e@ 0 ax!x!x!x!x!x!x!x!x!x!x!x!x!x!h2   Sx!x!x!x!x!x!x!x!x!x!x!x!x!x!x!l}<7IL `x"x"x"x"x"x"x"x"x"x"x"x"x"x"f1 L% gx"x"x"x"x"x"x"x"x"x"x"x"jBMg _x"x"x"x"x"x"x"x"x"x"x"x"x"x"e0 k3ex"x"x"x"x"x"x"x"x"Y<"hn? _y"y"y"y"y"y"y"y"y"y"y"y"y"y"d0 @ D`w!y"x"eM_- |^y#y#y#y#y#y#y#y#y#y#y#y#y#y#c0 =]y#y#y#y#y#y#y#y#y#y#y#y#y#y#a/ G ]y#y#y#y#y#y#y#y#y#y#y#y#y#y#a/ /\z$z$z$z$z$z$z$z$z$z$z$z$z$z$`.8y_z$z$z$z$z$z$z$z$z$z$z$z$z$z$c/~#}z$z$z$z$z$z$z$z$z$z$z$z$z$z$=,o+h3~{%{%{%{%{%{%{%{%{%{%{%{%{%{%>,.mY~{%{%{%{%{%{%{%{%{%{%{%{%{%{%>,U@E~{&{&{&{&{&{&{&{&{&{&{&{&{&{&>+BD 1D?|&|&|&|&|&|&|&|&|&|&|&|&|&|&>+?D2 |&|&|&|&|&|&|&|&|&|&|&|&|&|&>+|'|'|'|'|'|'|'|'|'|'|'|'|'|'>+^}'}'}'}'}'}'}'}'}'}'}'}'}'@+ e]- s$}'}'}'}'}'}'}'}'}'}'}'@+  P}(}(}(}(}(}(}(}(}(}(@+R   K% n#}(}(}(}(}(}(}(}(@)      C})~)~)~)~)~)~)@) A    : i"~)~)~)~)~)@)      w;{(~)~)~)@)  2     %_**@) x       f3x(@) W        T)+U      ,b   ; ?  %L   3- 8 &kz ?????????>|~~~~|>|>x?????PNG  IHDR\rf4IDATxy\E}˾o$aMXd5"bBXe ?^*^/xaMl3}S?L23T9S43sN[]z:U" `hb@@@90 `""V E*"! 0C C"lQ8݆RsL+@? D0$d^J5k`9Âx o*o`#"U6<ҹ/㪕l x[J6~ػ6t9#!"2x ?+Mpf7]x !:DrZ`vX5R"";` R0 Q!LF9RhR0] <s` +M' = Oz ;<՝;} 3VO+&T\) $%%/`oUvF(/<#'I~wU [ o(i[$'| Iu؅ڳK$m0v%=#X@[gH*x ;2)1}߫dIZ6s#yE_@YLe~n$R|D >dv"ƿZp{E_*dee)i$! GtQ|-Ȃx+W9GO}>~/~'} `^=~t"|4'ӭS[ UWoP*` w0vk#CFmL!731?/L<j =4Y%x 081DPq?U|so yFDPo N+%O c6]0D9܉h#^r%!L0:Cd& B"0 a!fӁVxK).gvM sdܺ)Jp;mu1R*ayC } "G'.z`W!TMO_Pݛe8NGBR'"OϩE%`)p)4N*-"򴈜-"fX"r<l~ , P <,nЉL/v!x"U)8K7s#ȥ{(s"2Cʐ^kJ r ?֋ȿ1ꤳ..{z;"rC q@ǏMo!ׯq?xC!)"q{5?~OIL3Ӧю?Pu 8xYDnMi6r "7r:7p^/  ;Dx-SpCg]g(2 "E"r+:ptR'"H`H w]<#"#"C栃!!=*\M' ^wGY"|5p>BDN5 jJj:?','T3c<$"xәA+"7wiz]1}pc`v2@DƊyʝUi+Lә5F^o ?þ۝ MfP<׉0$wUigB"2(HkpAޗD3 y+"?q;>{8k k:3wй绸syl"&1ϋ"Rn:3W "G.]p%0 Cj/Kd(Lu i vt,XUDtKxRD*./2ѾwJEt*<e@Lz$V&X2&MᔹDFGd,=Kĉׯ'ݱmkvD)tBKҿp,9U8wQ@tҵ#̍5jG3ߊj-{(φp %{BP4OQ Z>Ѿw4~~ފ̷p!vYӌ;GK`uoH9Gki@tRƯfT|e_U|ƗǷq{H1ߊv&,U(q"g7N' ztP0T>'į|HšM"= xUXI՜ۻ!2)uc?r֚UK=/\y3.)(4  ؿ L:mFәtr5Z]4ǎ?jo*W = =2~<L2@2M0%Bsa^‰s&:3X%so p#^1wG/]?WhQ߬tŗ { 9Q8*;<3rp/H"(X4m>c}ptNnp1ESI&c؃0Sk@׳ԒV@0u+Uv-x)(A_w >bĂ 7Tfݗ'|,~LJ=D^|nB[: C\)Q<*#F"f`羅Kq7 Htq2 Z .PEA^O%.0VL ʥp{h%rKssj1}wIGO~~v Y{h* ֛m>Ms/0;)uA}16ߥ m".d9wgȁ0] .3ކMSQydMce ݯq8.CE==v}ߦo*IbS8r;u=RpHazWI]TV }**?w唂 GY:1M A pJg&&?Hu_Ph6V6IX G߬ pyzu.zlٸ&T#a3q=y3ϟ.Qs5%XG"UPϮ#2#Qԭ;(hE>Hy(EFTYQZ` 2hSɸ FˀlJ/xe1 tz ]1?tԿ |<|O8z8t<;DFτJ'} X@z bLؐd0/:t_ DFSVyMӼ~`Õi`Я좑Te%_c<1g5@2v`긟`rex W΄̸f\ KӀ01 P|]tT=~_:" 34\1Kx{xyp~߹9zUc %=?=j:Pc+?#q'Sv@^90Fyy|!`P >,g3F3a<'&}6(|]~IVp7U{6ߓke@LcOE-c_"ңO(:E0) ;Y۫>ӯG˄ ]:ٗΘ/"mTDτqW&#$;x9*0')U{P.9pO 1!ou1M_ *Lg(( ЍXPz{(3vb<%R0,y8n KqO_h:C*&esdM'C50@ٜp+ͼY#V8 %Ӏ'n[^]4n/0 9xf;{vh4-Ѷ1:K]ThSh)Dn8}[M}4x92}LoM}q D)P{ހ^Gs4s X@ő'TN\[*d'1;+N$yO}Q:K7gRI=mTXxjzO;&l_C'`BaO=Ƙ6X<ue p]=[!yzNjo *([}Ꞻx'n4S[},>e:I xDU`=U= '4h9wڿQ󗓈~ $_jk"IFţ=y1 'yГMr/.sp/q_a>`pݴ6}swO'AKߣ٫p  AuPąVm큻G`A6N`eӻ9O_oXg3SbyCʠ~oq78%i7Zg`>UGjKZWLADWol|W1^ԟK yCml|Q R }Mf,'kvz(l]ȅ3=קVwOMqeΛ@(r{'7#=PqZ߽hEB48 k _ <"o҆/gH{zC GP c,:n0 j;Ӹs`{T3AIUuopOq=e;O_1Vv$۵cRxrS)ߞ&LBRKx'QK$Y!V?`ꇌ_d5'JQK%DOJm\W:?8cիH5à`t?̙rN<' R%l+Pq}v|Y{ނL`0{Ok fP ѰDF#6PN+0BnzTH?';j(Omm,/'jO &9{:cG$)+Ul!@Ǧoo}+#,ʹWkJL5GrX?p{}I׸#b:_[T['\)Y;jVӑWCOj = U tMe6(:v|tRK7W]xό57/x5 9vt |Q߃]@ObVe:/dtq$o&IMcCX6tد˄}rtY*j[܏WM$^mr}@|8 Rڧ+JcHMRôb5+>i~Sxt$^ԾWՋǖ,Dck:7ܐr,kBuP0ѴYMĉm=ج@GzMʻӍC4=vS@1aȌAA'ڠ.N<;&^ l@' ov%Wrv|_d_WElܤx+'^ܷ?겸6>aFJ8__Uw{8'֚XS5JdFn]W\l3NGԓ6|6t1Kt{g|i6Mސ}!/_"(:nmdBBʣ ہb7'A<ʹdVV$4/LI;g.Vj:Xmô~Zm[j>ADLm\o/ޫ{'#u9:skn͛5ʲLuUPP)o;<fg6A& wc0@T+R69 *SFOВչ)&9Ycbb[%1ʟRkW\Mi՟6ޜVXC ]3h<)d2 o_봏Q-MQR=ڠOXZthsw}>n~W9:V,LAxjtmg3s!렠;Ls?=BN#spZ@tsRz 4G/K{H>?YP0ABZmt4ս=kIcWy8*0"4ߕU2 3sҘ3 JA߶q6:2]<m~Ka/Y`^YԺjh2:0DӖz']ّ<5K;(H**f#e{kx*7HE=r V鲑pZ3Q9rb\zH!Y=DOWz*Q}Zx,I;:j8`t V_LK{1O5((ţ)Z^޿;w?% qc͟XKw64PAgsJFi6e{*s6܅^<\85]Zh50D:̢wKSw W3U=*^>S8Zzh] ՀzhnĚ` VѮQJɢ}.BXE#ȩ_ӢqTjzբ:5 H`tLZp0"{O6TAd,Xz'6 @˝`q6SƂqP>풧2dAA|69-կkt\Zaх@`I"59׎Rm|(KDn#7nϽ(ik|sXK4śz {*7LxZ!F~5ě>?.)Ò}5~.#-/(6dmޜ@Z(ݼ76*Z_5Xd샂׮C5jw >1ݳWL ") i@kNkx0DN=ϮO+p=iTˁZbA3\NcƖ5%VAǥ 2(hO@"zlԲYEB])wmͣyܟ+N)N5ߎ-LHYdܐ(*7yj/=6r[[Gkں0 Ws "є{EHi]:`G}3Z+t8x~&l\o@G,V,ֵ@'^6$3AGu3k;7V2tD@BcbD^hÝQ O9F]!2%YVHgd hpS cPIĐ;t׿vAa/\7$‚\dӐ TMD+so ,GFtɦ5JP0t'ܣ8ᆁufybk8 ؉mD}-Qj*Tdz`/~i]y5höwzY_;Z,^%#{q}xZlرb{?Ca ual9Vq[w @w[ 2kc0H9T*U4˜$Mq1K6E$m5Wb+rе+z"]k '*ц W:(8B#Ce!娿#2g/ ]c#Qrc?l6UFǜw:b`>9m^’DT@7PHYj>ˠ`hHL=:Nûx#7׀nűU$(hO]Q7P¨,Q-xEdzF|#+xЇP^^s5o`>L{lu]c;Y1`3D*rf"{Oު=eRPi(7W?v"6I'Tk 4|RJ|r|B^x<8߹:߬^0?tC5o&?P[UVtQ`M?L}o}䅦 y|M{u~'LLm6]+l܆?w?T[ q=m;$\cF*HD$\r8͛Z%k-%y#w+=Et77˗3_ |ih錤cռ}z6YIģRF]Jm?tz8m:yӲ-*O6t+`R B%j޾T=zYwPux¼ӗ) {7>{C>3%]©ka4ΰ dӼ9־#/CpTEz:ZS-)y+Sjp]t Q }ٍϜ/YɆo*| -TD:Ȗ+:=wen@vA0?>DU 6؛pXlH  Blnmh8 DƏjț:*VA#Բ vK4 ֽkt7 *wD8oA%7aQ -<*ަYvNӋ5\3z}NoǴ0vqq3%TpR 1@~N6l{KNjA+(rXm̯ɉ8jsbOz:A-P:BE7G>Ϧ5^M?}@o)ۛaŁm;9Y+4N bM>~JӵA0(̾W3ltznG(L:E*3+V={x ;)u;pn#tA֢7GWr:Zˣ{Pk0DJqLKh0p-n%יo͟׼3=Ȃ_2i\ԦJxni3rL tĪ3mkA{uE];V{SכItqRY 7 >` qRg 2/PN.vR"4_Q>Gi[@:Tu|>ۀjdžkW> /ƚYl[v:VD͛Й꩓?4Xt_@/')UӸ봯;P겓j2]fSŞ=j/?RoS+ni'tF }pRT:FgM pw#;[L sԯKu4smժn:8co4mg2dyJ;*r[ E# 6k>M)m&}+ԃ2Fߡ GI0[i} %J-Qd< ajB<8ta{OHaY܏j,]"_Qv.E R"io&޺Nc'į#Gk:F pRMc''Lc00#0EP3Q+b:ޡ*jG\sMd|䚂hmb/o`:/!T^a{=GӦeqΝLͱi9I0k'3>, m1S:qv^:q0/<MV !Y@кkU}]ؔZSk|'O]H}oq+*Hn,|5ͬygruѺ[GdB5k?[<"QͲMkڎ[МϜ5'h54ofd{MR%Hͼ~<;[^ҋh, 硲飶f4|4R=;oW6&J15k }jZ%fy= mPly?_zos0x|Qсǎf.;_xF C;Gtvi&5o ;o#%m[G|מ}40Tny mxi016eIENDB`bino-2.5/src/appicon.rc000066400000000000000000000000351475415313200150600ustar00rootroot00000000000000IDI_ICON1 ICON "appicon.ico" bino-2.5/src/bino.cpp000066400000000000000000001472401475415313200145460ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include #include #include #include #include "bino.hpp" #include "log.hpp" #include "tools.hpp" #include "digestiblemedia.hpp" #include "metadata.hpp" static Bino* binoSingleton = nullptr; Bino::Bino(ScreenType screenType, const Screen& screen, bool swapEyes) : _wantExit(false), _videoSink(nullptr), _audioOutput(nullptr), _player(nullptr), _audioInput(nullptr), _videoInput(nullptr), _screenInput(nullptr), _windowInput(nullptr), _captureSession(nullptr), _lastFrameInputMode(Input_Unknown), _lastFrameSurroundMode(Surround_Unknown), _screenType(screenType), _screen(screen), _frameIsNew(false), _frameWasSerialized(true), _swapEyes(swapEyes) { Q_ASSERT(!binoSingleton); binoSingleton = this; } Bino::~Bino() { delete _videoSink; delete _audioOutput; delete _player; delete _audioInput; delete _videoInput; delete _screenInput; delete _windowInput; delete _captureSession; binoSingleton = nullptr; } Bino* Bino::instance() { return binoSingleton; } void Bino::initializeOutput(const QAudioDevice& audioOutputDevice) { _videoSink = new VideoSink(&_frame, &_extFrame, &_frameIsNew); connect(_videoSink, &VideoSink::newVideoFrame, [=]() { emit newVideoFrame(); }); connect(_videoSink, &VideoSink::newVideoFrame, [=]() { _frameWasSerialized = false; }); _audioOutput = new QAudioOutput; _audioOutput->setDevice(audioOutputDevice); } void Bino::startPlaylistMode() { if (playlistMode()) { return; } if (captureMode()) { stopCaptureMode(); } connect(Playlist::instance(), SIGNAL(mediaChanged(PlaylistEntry)), this, SLOT(mediaChanged(PlaylistEntry))); _player = new QMediaPlayer; _player->setVideoOutput(_videoSink); _player->setAudioOutput(_audioOutput); _player->connect(_player, &QMediaPlayer::errorOccurred, [=](QMediaPlayer::Error /* error */, const QString& errorString) { LOG_WARNING("%s", qPrintable(tr("Media player error: %1").arg(errorString))); }); _player->connect(_player, &QMediaPlayer::playbackStateChanged, [=](QMediaPlayer::PlaybackState state) { LOG_DEBUG("Playback state changed to %s", state == QMediaPlayer::StoppedState ? "stopped" : state == QMediaPlayer::PlayingState ? "playing" : state == QMediaPlayer::PausedState ? "paused" : "unknown"); if (state == QMediaPlayer::StoppedState) Playlist::instance()->mediaEnded(); }); emit stateChanged(); } void Bino::stopPlaylistMode() { if (_player) { delete _player; _player = nullptr; emit stateChanged(); } } void Bino::startCaptureMode(bool withAudioInput, const QAudioDevice& audioInputDevice) { if (playlistMode()) stopPlaylistMode(); if (captureMode()) stopCaptureMode(); _captureSession = new QMediaCaptureSession; _captureSession->setAudioOutput(_audioOutput); _captureSession->setVideoSink(_videoSink); if (withAudioInput) { _audioInput = new QAudioInput; _audioInput->setDevice(audioInputDevice); _captureSession->setAudioInput(_audioInput); } } void Bino::startCaptureModeCamera( bool withAudioInput, const QAudioDevice& audioInputDevice, const QCameraDevice& videoInputDevice) { startCaptureMode(withAudioInput, audioInputDevice); _videoInput = new QCamera; _videoInput->setCameraDevice(videoInputDevice); _captureSession->setCamera(_videoInput); _videoInput->setActive(true); emit stateChanged(); } void Bino::startCaptureModeScreen( bool withAudioInput, const QAudioDevice& audioInputDevice, QScreen* screenInputDevice) { startCaptureMode(withAudioInput, audioInputDevice); _screenInput = new QScreenCapture; _screenInput->setScreen(screenInputDevice); _captureSession->setScreenCapture(_screenInput); _screenInput->setActive(true); emit stateChanged(); } void Bino::startCaptureModeWindow( bool withAudioInput, const QAudioDevice& audioInputDevice, const QCapturableWindow& windowInputDevice) { startCaptureMode(withAudioInput, audioInputDevice); _windowInput = new QWindowCapture; _windowInput->setWindow(windowInputDevice); _captureSession->setWindowCapture(_windowInput); _windowInput->setActive(true); emit stateChanged(); } void Bino::stopCaptureMode() { if (_captureSession) { delete _captureSession; _captureSession = nullptr; delete _videoInput; _videoInput = nullptr; delete _screenInput; _screenInput = nullptr; delete _windowInput; _windowInput = nullptr; if (_audioInput) { delete _audioInput; _audioInput = nullptr; } emit stateChanged(); } } bool Bino::playlistMode() const { return _player; } bool Bino::captureMode() const { return _captureSession; } void Bino::mediaChanged(PlaylistEntry entry) { if (!playlistMode()) return; if (entry.noMedia()) { _player->stop(); } else { // Get meta data MetaData metaData; metaData.detectCached(entry.url); // Special handling of files that cannot be digested by QtMultimedia directly QUrl digestibleUrl = digestibleMediaUrl(entry.url); // Set new source _player->setSource(digestibleUrl); if (entry.videoTrack >= 0) { _player->setActiveVideoTrack(entry.videoTrack); } if (entry.audioTrack >= 0) { _player->setActiveAudioTrack(entry.audioTrack); } else if (Playlist::instance()->preferredAudio() != QLocale::AnyLanguage) { int audioTrack = -1; for (int i = 0; i < int(metaData.audioTracks.length()); i++) { QLocale audioLanguage = metaData.audioTracks[i].value(QMediaMetaData::Language).toLocale(); if (audioLanguage == Playlist::instance()->preferredAudio()) { audioTrack = i; break; } } if (audioTrack >= 0) { _player->setActiveAudioTrack(entry.audioTrack); } } if (entry.subtitleTrack >= 0) { _player->setActiveSubtitleTrack(entry.subtitleTrack); } else if (entry.subtitleTrack == PlaylistEntry::NoTrack) { // do nothing } else if (metaData.subtitleTracks.size() > 0 && Playlist::instance()->wantSubtitle()) { int subtitleTrack = 0; for (int i = 0; i < int(metaData.subtitleTracks.length()); i++) { QLocale subtitleLanguage = metaData.subtitleTracks[i].value(QMediaMetaData::Language).toLocale(); if (subtitleLanguage == Playlist::instance()->preferredSubtitle()) { subtitleTrack = i; break; } } _player->setActiveSubtitleTrack(subtitleTrack); } _player->play(); _videoSink->newPlaylistEntry(entry, metaData); } emit stateChanged(); } void Bino::quit() { stop(); _wantExit = true; emit wantQuit(); } void Bino::seek(qint64 milliseconds) { if (!playlistMode()) return; _player->setPosition(_player->position() + milliseconds); } void Bino::setPosition(float pos) { if (!playlistMode()) return; _player->setPosition(pos * _player->duration()); } void Bino::togglePause() { if (!playlistMode()) return; if (_player->playbackState() == QMediaPlayer::PlayingState) { _player->pause(); emit stateChanged(); } else if (_player->playbackState() == QMediaPlayer::PausedState) { _player->play(); emit stateChanged(); } } void Bino::pause() { if (!playlistMode()) return; if (_player->playbackState() == QMediaPlayer::PlayingState) { _player->pause(); emit stateChanged(); } } void Bino::play() { if (!playlistMode()) return; if (_player->playbackState() != QMediaPlayer::PlayingState) { _player->play(); emit stateChanged(); } } void Bino::setMute(bool m) { bool isMuted = _player->audioOutput()->isMuted(); if (m != isMuted) { _audioOutput->setMuted(m); emit stateChanged(); } } void Bino::toggleMute() { _audioOutput->setMuted(!_player->audioOutput()->isMuted()); emit stateChanged(); } void Bino::setVolume(float vol) { _audioOutput->setVolume(vol); } void Bino::changeVolume(float offset) { _audioOutput->setVolume(_player->audioOutput()->volume() + offset); } void Bino::stop() { if (!playlistMode()) return; if (_player->playbackState() == QMediaPlayer::StoppedState) { _player->stop(); emit stateChanged(); } } void Bino::setSwapEyes(bool s) { if (_swapEyes != s) { _swapEyes = s; emit stateChanged(); } } void Bino::toggleSwapEyes() { _swapEyes = !_swapEyes; emit stateChanged(); } void Bino::setVideoTrack(int i) { LOG_DEBUG("changing video track to %d", i); if (playlistMode()) { _player->setActiveVideoTrack(i); emit stateChanged(); } } void Bino::setAudioTrack(int i) { LOG_DEBUG("changing audio track to %d", i); if (playlistMode()) { _player->setActiveAudioTrack(i); emit stateChanged(); } } void Bino::setSubtitleTrack(int i) { LOG_DEBUG("changing subtitle track to %d", i); if (playlistMode()) { _player->setActiveSubtitleTrack(i); emit stateChanged(); } } void Bino::setInputMode(InputMode mode) { _videoSink->inputMode = mode; _frame.inputMode = mode; _frame.reUpdate(); _frameIsNew = true; LOG_DEBUG("setting input mode to %s", inputModeToString(mode)); } void Bino::setSurroundMode(SurroundMode mode) { _videoSink->surroundMode = mode; _frame.surroundMode = mode; _frame.reUpdate(); _frameIsNew = true; LOG_DEBUG("setting surround mode to %s", surroundModeToString(mode)); } bool Bino::swapEyes() const { return _swapEyes; } bool Bino::muted() const { return _audioOutput->isMuted(); } bool Bino::paused() const { return (playlistMode() && _player->playbackState() == QMediaPlayer::PausedState); } bool Bino::playing() const { return (playlistMode() && _player->playbackState() == QMediaPlayer::PlayingState); } bool Bino::stopped() const { return (playlistMode() && _player->playbackState() == QMediaPlayer::StoppedState); } QUrl Bino::url() const { QUrl url; if (playing() || paused()) url = _player->source(); return url; } int Bino::videoTrack() const { int t = -1; if (captureMode()) t = 0; else if (playlistMode()) t = _player->activeVideoTrack(); return t; } int Bino::audioTrack() const { int t = -1; if (captureMode()) t = 0; else if (playlistMode()) t = _player->activeAudioTrack(); return t; } int Bino::subtitleTrack() const { int t = -1; if (captureMode()) t = 0; else if (playlistMode()) t = _player->activeSubtitleTrack(); return t; } InputMode Bino::inputMode() const { return _videoSink->inputMode; } InputMode Bino::assumeInputMode() const { return _frame.inputMode; } bool Bino::assumeStereoInputMode() const { return (assumeInputMode() != Input_Mono); } SurroundMode Bino::surroundMode() const { return _videoSink->surroundMode; } SurroundMode Bino::assumeSurroundMode() const { return _frame.surroundMode; } void Bino::serializeStaticData(QDataStream& ds) const { ds << _screenType << _screen; } void Bino::deserializeStaticData(QDataStream& ds) { ds >> _screenType >> _screen; } void Bino::serializeDynamicData(QDataStream& ds) { ds << _frameWasSerialized; if (!_frameWasSerialized) { ds << _frame; if (_frame.inputMode == Input_Alternating_LR || _frame.inputMode == Input_Alternating_RL) { ds << _extFrame; } _frameWasSerialized = true; } ds << _swapEyes; } void Bino::deserializeDynamicData(QDataStream& ds) { bool noNewFrame; ds >> noNewFrame; if (!noNewFrame) { ds >> _frame; if (_frame.inputMode == Input_Alternating_LR || _frame.inputMode == Input_Alternating_RL) { ds >> _extFrame; } _frameIsNew = true; } ds >> _swapEyes; } bool Bino::wantExit() const { return _wantExit; } bool Bino::initProcess() { bool haveAnisotropicFiltering = checkTextureAnisotropicFilterAvailability(); LOG_DEBUG("Using OpenGL in the %s variant", IsOpenGLES ? "ES" : "Desktop"); // Qt-based OpenGL initialization initializeOpenGLFunctions(); // FBO and PBO glGenFramebuffers(1, &_viewFbo); glGenFramebuffers(1, &_frameFbo); glGenTextures(1, &_depthTex); glBindTexture(GL_TEXTURE_2D, _depthTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, 1, 1, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr); glBindFramebuffer(GL_FRAMEBUFFER, _viewFbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, _depthTex, 0); CHECK_GL(); // Quad geometry const float quadPositions[] = { -1.0f, +1.0f, 0.0f, +1.0f, +1.0f, 0.0f, +1.0f, -1.0f, 0.0f, -1.0f, -1.0f, 0.0f }; const float quadTexCoords[] = { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f }; static const unsigned short quadIndices[] = { 0, 3, 1, 1, 3, 2 }; glGenVertexArrays(1, &_quadVao); glBindVertexArray(_quadVao); GLuint quadPositionBuf; glGenBuffers(1, &quadPositionBuf); glBindBuffer(GL_ARRAY_BUFFER, quadPositionBuf); glBufferData(GL_ARRAY_BUFFER, sizeof(quadPositions), quadPositions, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); GLuint quadTexCoordBuf; glGenBuffers(1, &quadTexCoordBuf); glBindBuffer(GL_ARRAY_BUFFER, quadTexCoordBuf); glBufferData(GL_ARRAY_BUFFER, sizeof(quadTexCoords), quadTexCoords, GL_STATIC_DRAW); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); GLuint quadIndexBuf; glGenBuffers(1, &quadIndexBuf); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, quadIndexBuf); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(quadIndices), quadIndices, GL_STATIC_DRAW); CHECK_GL(); // Cube geometry const float cubePositions[] = { -10.0f, -10.0f, +10.0f, +10.0f, -10.0f, +10.0f, -10.0f, +10.0f, +10.0f, +10.0f, +10.0f, +10.0f, +10.0f, -10.0f, -10.0f, -10.0f, -10.0f, -10.0f, +10.0f, +10.0f, -10.0f, -10.0f, +10.0f, -10.0f, -10.0f, -10.0f, -10.0f, -10.0f, -10.0f, +10.0f, -10.0f, +10.0f, -10.0f, -10.0f, +10.0f, +10.0f, +10.0f, -10.0f, +10.0f, +10.0f, -10.0f, -10.0f, +10.0f, +10.0f, +10.0f, +10.0f, +10.0f, -10.0f, -10.0f, +10.0f, -10.0f, -10.0f, +10.0f, +10.0f, +10.0f, +10.0f, -10.0f, +10.0f, +10.0f, +10.0f, +10.0f, -10.0f, -10.0f, +10.0f, -10.0f, +10.0f, -10.0f, -10.0f, -10.0f, -10.0f, -10.0f, +10.0f }; const float cubeTexCoords[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }; static const unsigned short cubeIndices[] = { 0, 1, 2, 1, 3, 2, 4, 5, 6, 5, 7, 6, 8, 9, 10, 9, 11, 10, 12, 13, 14, 13, 15, 14, 16, 17, 18, 17, 19, 18, 20, 21, 22, 21, 23, 22 }; glGenVertexArrays(1, &_cubeVao); glBindVertexArray(_cubeVao); GLuint cubePositionBuf; glGenBuffers(1, &cubePositionBuf); glBindBuffer(GL_ARRAY_BUFFER, cubePositionBuf); glBufferData(GL_ARRAY_BUFFER, sizeof(cubePositions), cubePositions, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); GLuint cubeTexCoordBuf; glGenBuffers(1, &cubeTexCoordBuf); glBindBuffer(GL_ARRAY_BUFFER, cubeTexCoordBuf); glBufferData(GL_ARRAY_BUFFER, sizeof(cubeTexCoords), cubeTexCoords, GL_STATIC_DRAW); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); GLuint cubeIndexBuf; glGenBuffers(1, &cubeIndexBuf); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeIndexBuf); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cubeIndices), cubeIndices, GL_STATIC_DRAW); CHECK_GL(); // Plane textures glGenTextures(3, _planeTexs); for (int p = 0; p < 3; p++) { glBindTexture(GL_TEXTURE_2D, _planeTexs[p]); unsigned int black = 0; glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &black); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); if (p == 0) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } } CHECK_GL(); // Frame textures glGenTextures(1, &_frameTex); glBindTexture(GL_TEXTURE_2D, _frameTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); if (haveAnisotropicFiltering) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 4.0f); glGenTextures(1, &_extFrameTex); glBindTexture(GL_TEXTURE_2D, _extFrameTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); if (haveAnisotropicFiltering) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 4.0f); CHECK_GL(); // Subtitle texture glGenTextures(1, &_subtitleTex); glBindTexture(GL_TEXTURE_2D, _subtitleTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); if (haveAnisotropicFiltering) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 4.0f); CHECK_GL(); // Screen geometry glGenVertexArrays(1, &_screenVao); glBindVertexArray(_screenVao); glGenBuffers(1, &_positionBuf); glBindBuffer(GL_ARRAY_BUFFER, _positionBuf); glBufferData(GL_ARRAY_BUFFER, _screen.positions.size() * sizeof(float), _screen.positions.constData(), GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); glGenBuffers(1, &_texcoordBuf); glBindBuffer(GL_ARRAY_BUFFER, _texcoordBuf); glBufferData(GL_ARRAY_BUFFER, _screen.texcoords.size() * sizeof(float), _screen.texcoords.constData(), GL_STATIC_DRAW); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); glGenBuffers(1, &_indexBuf); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuf); glBufferData(GL_ELEMENT_ARRAY_BUFFER, _screen.indices.length() * sizeof(unsigned int), _screen.indices.constData(), GL_STATIC_DRAW); CHECK_GL(); return true; } void Bino::rebuildColorPrgIfNecessary(int planeFormat, bool colorRangeSmall, int colorSpace, int colorTransfer) { if (_colorPrg.isLinked() && _colorPrgPlaneFormat == planeFormat && _colorPrgColorRangeSmall == colorRangeSmall && _colorPrgColorSpace == colorSpace && _colorPrgColorTransfer == colorTransfer) { return; } LOG_DEBUG("rebuilding color conversion program for plane format %d, value range %s, color space %s, color transfer %s", planeFormat, colorRangeSmall ? "small" : "full", colorSpace == VideoFrame::CS_BT601 ? "bt601" : colorSpace == VideoFrame::CS_BT709 ? "bt709" : colorSpace == VideoFrame::CS_AdobeRgb ? "rgb" : "bt2020", colorTransfer == VideoFrame::CT_NOOP ? "none" : colorTransfer == VideoFrame::CT_ST2084 ? "st2084" : "std_b67"); QString colorVS = readFile(":src/shader-color.vert.glsl"); QString colorFS = readFile(":src/shader-color.frag.glsl"); colorFS.replace("$PLANE_FORMAT", QString::number(planeFormat)); colorFS.replace("$COLOR_RANGE_SMALL", colorRangeSmall ? "true" : "false"); colorFS.replace("$COLOR_SPACE", QString::number(colorSpace)); colorFS.replace("$COLOR_TRANSFER", QString::number(colorTransfer)); if (IsOpenGLES) { colorVS.prepend("#version 300 es\n"); colorFS.prepend("#version 300 es\n" "precision mediump float;\n"); } else { colorVS.prepend("#version 330\n"); colorFS.prepend("#version 330\n"); } _colorPrg.removeAllShaders(); _colorPrg.addShaderFromSourceCode(QOpenGLShader::Vertex, colorVS); _colorPrg.addShaderFromSourceCode(QOpenGLShader::Fragment, colorFS); _colorPrg.link(); _colorPrgPlaneFormat = planeFormat; _colorPrgColorRangeSmall = colorRangeSmall; _colorPrgColorSpace = colorSpace; _colorPrgColorTransfer = colorTransfer; } void Bino::rebuildViewPrgIfNecessary(SurroundMode surroundMode, bool nonLinearOutput) { if (_viewPrg.isLinked() && _viewPrgSurroundMode == surroundMode && _viewPrgNonlinearOutput == nonLinearOutput) return; LOG_DEBUG("rebuilding view program for surround mode %s, non linear output %s", surroundModeToString(surroundMode), nonLinearOutput ? "true" : "false"); QString viewVS = readFile(":src/shader-view.vert.glsl"); QString viewFS = readFile(":src/shader-view.frag.glsl"); viewFS.replace("$SURROUND_DEGREES", surroundMode == Surround_360 ? "360" : surroundMode == Surround_180 ? "180" : "0"); viewFS.replace("$NONLINEAR_OUTPUT", nonLinearOutput ? "true" : "false"); if (IsOpenGLES) { viewVS.prepend("#version 300 es\n"); viewFS.prepend("#version 300 es\n" "precision mediump float;\n"); } else { viewVS.prepend("#version 330\n"); viewFS.prepend("#version 330\n"); } _viewPrg.removeAllShaders(); _viewPrg.addShaderFromSourceCode(QOpenGLShader::Vertex, viewVS); _viewPrg.addShaderFromSourceCode(QOpenGLShader::Fragment, viewFS); _viewPrg.link(); _viewPrgSurroundMode = surroundMode; _viewPrgNonlinearOutput = nonLinearOutput; } bool Bino::drawSubtitleToImage(int w, int h, const QString& string) { if (_subtitleImg.width() == w && _subtitleImg.height() == h && _subtitleImgString == string) return false; if (_subtitleImg.width() != w || _subtitleImg.height() != h) _subtitleImg = QImage(w, h, QImage::Format_ARGB32_Premultiplied); _subtitleImgString = string; QColor bgColor = Qt::black; bgColor.setAlpha(0); _subtitleImg.fill(bgColor); if (string.isEmpty()) return true; // this tries to reproduce what qvideotexturehelper.cpp does since it is entirely // unclear and undocumented how subtitles are expected to be handled QFont font; float fontSize = h * 0.045f; font.setPointSize(fontSize); QTextLayout layout; layout.setText(string); layout.setFont(font); QTextOption option; option.setUseDesignMetrics(true); option.setAlignment(Qt::AlignCenter); layout.setTextOption(option); QFontMetrics metrics(font); float lineWidth = w * 0.9f; float margin = w * 0.05f; float height = 0.0f; float textWidth = 0.0f; layout.beginLayout(); for (;;) { QTextLine line = layout.createLine(); if (!line.isValid()) break; line.setLineWidth(lineWidth); height += metrics.leading(); line.setPosition(QPointF(margin, height)); height += line.height(); textWidth = qMax(textWidth, line.naturalTextWidth()); } layout.endLayout(); int bottomMargin = h / 20; float y = h - bottomMargin - height; layout.setPosition(QPointF(0.0f, y)); textWidth += fontSize / 4.0f; //QRectF bounds = QRectF((w - textWidth) * 0.5f, y, textWidth, height); QPainter painter(&_subtitleImg); QTextLayout::FormatRange range; range.start = 0; range.length = layout.text().size(); range.format.setForeground(Qt::white); layout.draw(&painter, {}, { range }); return true; } static int alignmentFromBytesPerLine(const void* data, int bpl) { int alignment = 1; if (uint64_t(data) % 8 == 0 && bpl % 8 == 0) alignment = 8; else if (uint64_t(data) % 4 == 0 && bpl % 4 == 0) alignment = 4; else if (uint64_t(data) % 2 == 0 && bpl % 2 == 0) alignment = 2; LOG_FIREHOSE("convertFrameToTexture: alignment is %d (from data %p, bpl %d)", alignment, data, bpl); return alignment; } void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) { // 1. Get the frame data into plane textures int w = frame.width; int h = frame.height; int planeFormat; // see shader-color.frag.glsl int planeCount; // reset swizzling for plane0; might be changed below depending in the format glBindTexture(GL_TEXTURE_2D, _planeTexs[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); if (frame.storage == VideoFrame::Storage_Image) { LOG_FIREHOSE("convertFrameToTexture: format is image"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(frame.image.constBits(), frame.image.bytesPerLine())); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.image.bytesPerLine() / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, frame.image.constBits()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); planeFormat = 1; planeCount = 1; } else { std::array planeData; if (frame.storage == VideoFrame::Storage_Mapped) { planeData = { frame.mappedBits[0], frame.mappedBits[1], frame.mappedBits[2] }; } else { planeData = { frame.bits[0].data(), frame.bits[1].data(), frame.bits[2].data() }; } if (frame.pixelFormat == QVideoFrameFormat::Format_ARGB8888 || frame.pixelFormat == QVideoFrameFormat::Format_ARGB8888_Premultiplied || frame.pixelFormat == QVideoFrameFormat::Format_XRGB8888) { LOG_FIREHOSE("convertFrameToTexture: format argb8888"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_RED); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_GREEN); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_BLUE); planeFormat = 1; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_BGRA8888 || frame.pixelFormat == QVideoFrameFormat::Format_BGRA8888_Premultiplied || frame.pixelFormat == QVideoFrameFormat::Format_BGRX8888) { LOG_FIREHOSE("convertFrameToTexture: format bgra8888"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); planeFormat = 1; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_ABGR8888 || frame.pixelFormat == QVideoFrameFormat::Format_XBGR8888) { LOG_FIREHOSE("convertFrameToTexture: format abgr8888"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_GREEN); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_RED); planeFormat = 1; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_RGBA8888 || frame.pixelFormat == QVideoFrameFormat::Format_RGBX8888) { LOG_FIREHOSE("convertFrameToTexture: format rgba8888"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); planeFormat = 1; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_YUV420P) { LOG_FIREHOSE("convertFrameToTexture: format yuv420p"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[1]); glBindTexture(GL_TEXTURE_2D, _planeTexs[2]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[2], frame.qframe.bytesPerLine(2))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(2)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[2]); planeFormat = 2; planeCount = 3; } else if (frame.pixelFormat == QVideoFrameFormat::Format_YUV422P) { LOG_FIREHOSE("convertFrameToTexture: format yuv422p"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[1]); glBindTexture(GL_TEXTURE_2D, _planeTexs[2]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[2], frame.qframe.bytesPerLine(2))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(2)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[2]); planeFormat = 2; planeCount = 3; } else if (frame.pixelFormat == QVideoFrameFormat::Format_YV12) { LOG_FIREHOSE("convertFrameToTexture: format yv12"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[1]); glBindTexture(GL_TEXTURE_2D, _planeTexs[2]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[2], frame.qframe.bytesPerLine(2))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(2)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[2]); planeFormat = 3; planeCount = 3; } else if (frame.pixelFormat == QVideoFrameFormat::Format_NV12) { LOG_FIREHOSE("convertFrameToTexture: format nv12"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1) / 2); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_BYTE, planeData[1]); planeFormat = 4; planeCount = 2; } else if (frame.pixelFormat == QVideoFrameFormat::Format_P010 || frame.pixelFormat == QVideoFrameFormat::Format_P016) { LOG_FIREHOSE("convertFrameToTexture: format p010/p016"); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 2); glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, w, h, 0, GL_RED, GL_UNSIGNED_SHORT, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_SHORT, planeData[1]); planeFormat = 4; planeCount = 2; } else if (frame.pixelFormat == QVideoFrameFormat::Format_Y8) { glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); LOG_FIREHOSE("convertFrameToTexture: format y8"); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); planeFormat = 5; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_Y16) { glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 2); LOG_FIREHOSE("convertFrameToTexture: format y16"); glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, w, h, 0, GL_RED, GL_UNSIGNED_SHORT, planeData[0]); planeFormat = 5; planeCount = 1; } else { LOG_FATAL("Unhandled pixel format"); std::exit(1); } } glPixelStorei(GL_UNPACK_ALIGNMENT, 4); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); // 2. Convert plane textures into linear RGB in the frame texture glBindTexture(GL_TEXTURE_2D, frameTex); if (IsOpenGLES) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, w, h, 0, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, nullptr); else glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16, w, h, 0, GL_BGRA, GL_UNSIGNED_SHORT, nullptr); glBindFramebuffer(GL_FRAMEBUFFER, _frameFbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, frameTex, 0); glViewport(0, 0, w, h); glDisable(GL_DEPTH_TEST); rebuildColorPrgIfNecessary(planeFormat, frame.colorRangeSmall, frame.colorSpace, frame.colorTransfer); glUseProgram(_colorPrg.programId()); _colorPrg.setUniformValue("masteringWhite", frame.masteringWhite); for (int p = 0; p < planeCount; p++) { _colorPrg.setUniformValue(qPrintable(QString("plane") + QString::number(p)), p); glActiveTexture(GL_TEXTURE0 + p); glBindTexture(GL_TEXTURE_2D, _planeTexs[p]); } glBindVertexArray(_quadVao); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); glBindTexture(GL_TEXTURE_2D, frameTex); glGenerateMipmap(GL_TEXTURE_2D); } void Bino::preRenderProcess(int screenWidth, int screenHeight, int* viewCountPtr, int* viewWidthPtr, int* viewHeightPtr, float* frameDisplayAspectRatioPtr, bool* surroundPtr) { Q_ASSERT(_frame.inputMode != Input_Unknown); int viewCount = 2; int viewWidth = _frame.width; int viewHeight = _frame.height; float frameDisplayAspectRatio = _frame.aspectRatio; switch (_frame.inputMode) { case Input_Unknown: // cannot happen, update() sets a known mode case Input_Mono: viewCount = 1; break; case Input_Top_Bottom: case Input_Bottom_Top: frameDisplayAspectRatio *= 2.0f; [[fallthrough]]; case Input_Top_Bottom_Half: case Input_Bottom_Top_Half: viewHeight /= 2; break; case Input_Left_Right: case Input_Right_Left: frameDisplayAspectRatio /= 2.0f; [[fallthrough]]; case Input_Left_Right_Half: case Input_Right_Left_Half: viewWidth /= 2; break; case Input_Alternating_LR: case Input_Alternating_RL: break; } switch (_frame.surroundMode) { case Surround_Unknown: // cannot happen, update() sets a known mode case Surround_Off: break; case Surround_180: frameDisplayAspectRatio *= 2.0f; break; case Surround_360: break; } if (subtitleTrack() >= 0 && (screenWidth > viewWidth || screenHeight > viewHeight)) { if (screenWidth / viewWidth > screenHeight / viewHeight) { viewWidth = screenWidth; viewHeight = viewWidth / frameDisplayAspectRatio; } else { viewHeight = screenHeight; viewWidth = viewHeight * frameDisplayAspectRatio; } } if (viewCountPtr) *viewCountPtr = viewCount; if (viewWidthPtr) *viewWidthPtr = viewWidth; if (viewHeightPtr) *viewHeightPtr = viewHeight; if (frameDisplayAspectRatioPtr) *frameDisplayAspectRatioPtr = frameDisplayAspectRatio; if (surroundPtr) *surroundPtr = (_frame.surroundMode != Surround_Off); /* We need to get new frame data into a texture that is suitable for * rendering the screen: _frameTex. */ if (_frameIsNew) { // Convert _frame into _frameTex and, if needed, _extFrame into _extFrameTex. convertFrameToTexture(_frame, _frameTex); if (_frame.inputMode == Input_Alternating_LR || _frame.inputMode == Input_Alternating_RL) { // the user might have switched to this mode without the extFrame // being available, in that case fall back to the standard frame if (_extFrame.width != _frame.width || _extFrame.height != _frame.height) convertFrameToTexture(_frame, _extFrameTex); else convertFrameToTexture(_extFrame, _extFrameTex); } // Render the subtitle into the subtitle texture if (drawSubtitleToImage(viewWidth, viewHeight, _frame.subtitle)) { glBindTexture(GL_TEXTURE_2D, _subtitleTex); glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, _subtitleImg.width(), _subtitleImg.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, _subtitleImg.bits()); glGenerateMipmap(GL_TEXTURE_2D); } // Done. _frameIsNew = false; } if (_frame.inputMode != _lastFrameInputMode || _frame.surroundMode != _lastFrameSurroundMode) { emit stateChanged(); } _lastFrameInputMode = _frame.inputMode; _lastFrameSurroundMode = _frame.surroundMode; } void Bino::render( const QVector3D& unitedScreenBottomLeft, const QVector3D& unitedScreenBottomRight, const QVector3D& unitedScreenTopLeft, const QVector3D& intersectedScreenBottomLeft, const QVector3D& intersectedScreenBottomRight, const QVector3D& intersectedScreenTopLeft, const QMatrix4x4& projectionMatrix, const QMatrix4x4& orientationMatrix, const QMatrix4x4& viewMatrix, int view, // 0 = left, 1 = right int texWidth, int texHeight, unsigned int texture) { // Update screen switch (_screenType) { case ScreenUnited: _screen = Screen(unitedScreenBottomLeft, unitedScreenBottomRight, unitedScreenTopLeft); break; case ScreenIntersected: _screen = Screen(intersectedScreenBottomLeft, intersectedScreenBottomRight, intersectedScreenTopLeft); break; case ScreenGeometry: // do nothing break; } // Set up framebuffer object to render into glBindTexture(GL_TEXTURE_2D, _depthTex); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, texWidth, texHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr); glEnable(GL_DEPTH_TEST); glBindFramebuffer(GL_FRAMEBUFFER, _viewFbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); // Set up view glViewport(0, 0, texWidth, texHeight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Set up input mode unsigned int frameTex = _frameTex; float frameAspectRatio = _frame.aspectRatio; float viewOffsetX = 0.0f; float viewFactorX = 1.0f; float viewOffsetY = 0.0f; float viewFactorY = 1.0f; if (_swapEyes) view = (view == 0 ? 1 : 0); switch (_frame.inputMode) { case Input_Unknown: // cannot happen, update() sets a known mode case Input_Mono: // nothing to do break; case Input_Top_Bottom: viewFactorY = 0.5f; viewOffsetY = (view == 1 ? 0.5f : 0.0f); frameAspectRatio *= 2.0f; break; case Input_Top_Bottom_Half: viewFactorY = 0.5f; viewOffsetY = (view == 1 ? 0.5f : 0.0f); break; case Input_Bottom_Top: viewFactorY = 0.5f; viewOffsetY = (view != 1 ? 0.5f : 0.0f); frameAspectRatio *= 2.0f; break; case Input_Bottom_Top_Half: viewFactorY = 0.5f; viewOffsetY = (view != 1 ? 0.5f : 0.0f); break; case Input_Left_Right: viewFactorX = 0.5f; viewOffsetX = (view == 1 ? 0.5f : 0.0f); frameAspectRatio /= 2.0f; break; case Input_Left_Right_Half: viewFactorX = 0.5f; viewOffsetX = (view == 1 ? 0.5f : 0.0f); break; case Input_Right_Left: viewFactorX = 0.5f; viewOffsetX = (view != 1 ? 0.5f : 0.0f); frameAspectRatio /= 2.0f; break; case Input_Right_Left_Half: viewFactorX = 0.5f; viewOffsetX = (view != 1 ? 0.5f : 0.0f); break; case Input_Alternating_LR: if (view == 1) frameTex = _extFrameTex; break; case Input_Alternating_RL: if (view == 0) frameTex = _extFrameTex; break; } LOG_FIREHOSE("Rendering view %d from %s frame texture fx=%g ox=%g fy=%g oy=%g", view, frameTex == _frameTex ? "standard" : "extended", viewFactorX, viewOffsetX, viewFactorY, viewOffsetY); // Determine if we are producing the final rendering result here (which is the // case for VR mode) or if we are just rendering to intermediate textures (which // is the case for GUI mode). In GUI mode, the screen aspect ratio is unknown. bool finalRenderingStep = (_screen.aspectRatio > 0.0f); // Set up correct aspect ratio on screen float relWidth = 1.0f; float relHeight = 1.0f; if (finalRenderingStep) { if (_screen.aspectRatio < frameAspectRatio) relHeight = _screen.aspectRatio / frameAspectRatio; else relWidth = frameAspectRatio / _screen.aspectRatio; } // Set up shader program rebuildViewPrgIfNecessary(_frame.surroundMode, finalRenderingStep); glUseProgram(_viewPrg.programId()); QMatrix4x4 projectionModelViewMatrix = projectionMatrix; if (_frame.surroundMode == Surround_Off) projectionModelViewMatrix = projectionModelViewMatrix * viewMatrix; _viewPrg.setUniformValue("projectionModelViewMatrix", projectionModelViewMatrix); _viewPrg.setUniformValue("orientationMatrix", orientationMatrix); _viewPrg.setUniformValue("frameTex", 0); _viewPrg.setUniformValue("subtitleTex", 1); _viewPrg.setUniformValue("view_offset_x", viewOffsetX); _viewPrg.setUniformValue("view_factor_x", viewFactorX); _viewPrg.setUniformValue("view_offset_y", viewOffsetY); _viewPrg.setUniformValue("view_factor_y", viewFactorY); _viewPrg.setUniformValue("relative_width", relWidth); _viewPrg.setUniformValue("relative_height", relHeight); // Render scene glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, _subtitleTex); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, frameTex); if (_frame.surroundMode != Surround_Off) { // Set up filtering to work correctly at the horizontal wraparound: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); if (_frame.surroundMode == Surround_360) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Render glBindVertexArray(_cubeVao); glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, 0); // Reset filtering parameters to their defaults glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); } else { glBindVertexArray(_screenVao); if (_screenType == ScreenUnited || _screenType == ScreenIntersected) { // the geometry might have changed; re-upload it glBindBuffer(GL_ARRAY_BUFFER, _positionBuf); glBufferData(GL_ARRAY_BUFFER, _screen.positions.size() * sizeof(float), _screen.positions.constData(), GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glBindBuffer(GL_ARRAY_BUFFER, _texcoordBuf); glBufferData(GL_ARRAY_BUFFER, _screen.texcoords.size() * sizeof(float), _screen.texcoords.constData(), GL_STATIC_DRAW); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuf); glBufferData(GL_ELEMENT_ARRAY_BUFFER, _screen.indices.length() * sizeof(unsigned int), _screen.indices.constData(), GL_STATIC_DRAW); } glDrawElements(GL_TRIANGLES, _screen.indices.size(), GL_UNSIGNED_INT, 0); } } void Bino::keyPressEvent(QKeyEvent* event) { if (event->matches(QKeySequence::Quit) || event->matches(QKeySequence::Cancel) || event->key() == Qt::Key_Escape || event->key() == Qt::Key_MediaStop) { quit(); } else if (event->key() == Qt::Key_MediaTogglePlayPause || event->key() == Qt::Key_Space) { togglePause(); } else if (event->key() == Qt::Key_MediaPause) { pause(); } else if (event->key() == Qt::Key_MediaPlay) { play(); } else if (event->key() == Qt::Key_VolumeMute || event->key() == Qt::Key_M) { toggleMute(); } else if (event->key() == Qt::Key_VolumeDown) { changeVolume(-0.05f); } else if (event->key() == Qt::Key_VolumeUp) { changeVolume(+0.05f); } else if (event->key() == Qt::Key_Period) { seek(+1000); } else if (event->key() == Qt::Key_Comma) { seek(-1000); } else if (event->key() == Qt::Key_Right) { seek(+10000); } else if (event->key() == Qt::Key_Left) { seek(-10000); } else if (event->key() == Qt::Key_Right) { seek(+10000); } else if (event->key() == Qt::Key_Down) { seek(-60000); } else if (event->key() == Qt::Key_Up) { seek(+60000); } else if (event->key() == Qt::Key_PageDown) { seek(-600000); } else if (event->key() == Qt::Key_PageUp) { seek(+600000); } else if (event->key() == Qt::Key_N) { Playlist::instance()->next(); } else if (event->key() == Qt::Key_P) { Playlist::instance()->prev(); } else if (event->matches(QKeySequence::FullScreen) || event->key() == Qt::Key_F || event->key() == Qt::Key_F11) { emit toggleFullscreen(); } else if (event->key() == Qt::Key_E || event->key() == Qt::Key_F7) { toggleSwapEyes(); } else { LOG_DEBUG("Unhandled key event: key=%d text='%s'", event->key(), qPrintable(event->text())); event->ignore(); } } bino-2.5/src/bino.hpp000066400000000000000000000157411475415313200145530ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "screen.hpp" #include "videosink.hpp" #include "playlist.hpp" class Bino : public QObject, QOpenGLExtraFunctions { Q_OBJECT public: enum ScreenType { ScreenUnited, // global 2D united screen given by QVR ScreenIntersected, // global 2D intersected screen given by QVR ScreenGeometry // explicit geometry stored in _screen }; private: /* Data not directly relevant for rendering */ bool _wantExit; VideoSink* _videoSink; QAudioOutput* _audioOutput; // for playing a play list: QMediaPlayer* _player; // for capturing audio/video: QAudioInput* _audioInput; QCamera* _videoInput; QScreenCapture* _screenInput; QWindowCapture* _windowInput; QMediaCaptureSession* _captureSession; // for rendering subtitles: QImage _subtitleImg; QString _subtitleImgString; // for updating the GUI if necessary InputMode _lastFrameInputMode; SurroundMode _lastFrameSurroundMode; /* Static data for rendering, initialized on the main process */ ScreenType _screenType; Screen _screen; /* Static data for rendering, initialized in initProcess() */ unsigned int _depthTex; unsigned int _frameFbo; unsigned int _viewFbo; unsigned int _quadVao; unsigned int _cubeVao; unsigned int _planeTexs[3]; unsigned int _frameTex; unsigned int _extFrameTex; unsigned int _subtitleTex; unsigned int _screenVao, _positionBuf, _texcoordBuf, _indexBuf; QOpenGLShaderProgram _colorPrg; int _colorPrgPlaneFormat; bool _colorPrgColorRangeSmall; int _colorPrgColorSpace; int _colorPrgColorTransfer; QOpenGLShaderProgram _viewPrg; SurroundMode _viewPrgSurroundMode; bool _viewPrgNonlinearOutput; /* Dynamic data for rendering */ VideoFrame _frame; VideoFrame _extFrame; // for alternating stereo bool _frameIsNew; bool _frameWasSerialized; bool _swapEyes; void startCaptureMode(bool withAudioInput, const QAudioDevice& audioInputDevice); void rebuildColorPrgIfNecessary(int planeFormat, bool colorRangeSmall, int colorSpace, int colorTransfer); void rebuildViewPrgIfNecessary(SurroundMode surroundMode, bool nonLinearOutput); bool drawSubtitleToImage(int w, int h, const QString& string); void convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex); public: Bino(ScreenType screenType, const Screen& screen, bool swapEyes); virtual ~Bino(); static Bino* instance(); /* Initialization functions, to be called by main() before * starting either GUI or VR mode */ void initializeOutput(const QAudioDevice& audioOutputDevice); void startPlaylistMode(); void stopPlaylistMode(); void startCaptureModeCamera( bool withAudioInput, const QAudioDevice& audioInputDevice, const QCameraDevice& videoInputDevice); void startCaptureModeScreen( bool withAudioInput, const QAudioDevice& audioInputDevice, QScreen* screenInputDevice); void startCaptureModeWindow( bool withAudioInput, const QAudioDevice& audioInputDevice, const QCapturableWindow& windowInputDevice); void stopCaptureMode(); bool playlistMode() const; bool captureMode() const; /* Interaction functions, can be called while in GUI or VR mode */ void quit(); void seek(qint64 milliseconds); void setPosition(float pos); void togglePause(); void pause(); void play(); void stop(); void setMute(bool m); void toggleMute(); void setVolume(float vol); void changeVolume(float offset); void setSwapEyes(bool s); void toggleSwapEyes(); void setVideoTrack(int i); void setAudioTrack(int i); void setSubtitleTrack(int i); void setInputMode(InputMode mode); void setSurroundMode(SurroundMode mode); /* Functions necessary for GUI mode */ bool swapEyes() const; bool muted() const; bool paused() const; bool playing() const; bool stopped() const; QUrl url() const; int videoTrack() const; int audioTrack() const; int subtitleTrack() const; InputMode inputMode() const; // this might be unknown InputMode assumeInputMode() const; // this is never unknown bool assumeStereoInputMode() const; // is the assumed mode stereo? SurroundMode surroundMode() const; // this might be unknown SurroundMode assumeSurroundMode() const; // this is never unknown /* Functions necessary for VR mode */ void serializeStaticData(QDataStream& ds) const; void deserializeStaticData(QDataStream& ds); void serializeDynamicData(QDataStream& ds); void deserializeDynamicData(QDataStream& ds); bool wantExit() const; /* Functions shared by GUI and VR mode */ bool initProcess(); void preRenderProcess( int screenWidth = 0, int screenHeight = 0, int* viewCount = nullptr, int* viewWidth = nullptr, int* viewHeight = nullptr, float* frameDisplayAspectRatio = nullptr, bool* surround = nullptr); void render( const QVector3D& unitedScreenBottomLeft, const QVector3D& unitedScreenBottomRight, const QVector3D& unitedScreenTopLeft, const QVector3D& intersectedScreenBottomLeft, const QVector3D& intersectedScreenBottomRight, const QVector3D& intersectedScreenTopLeft, const QMatrix4x4& projectionMatrix, const QMatrix4x4& orientationMatrix, const QMatrix4x4& viewMatrix, int view, // 0 = left, 1 = right int texWidth, int texHeight, unsigned int texture); void keyPressEvent(QKeyEvent* event); public slots: void mediaChanged(PlaylistEntry entry); signals: void newVideoFrame(); void toggleFullscreen(); void stateChanged(); void wantQuit(); }; bino-2.5/src/commandinterpreter.cpp000066400000000000000000000404771475415313200175250ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include #include #include #include #include "commandinterpreter.hpp" #include "modes.hpp" #include "bino.hpp" #include "gui.hpp" #include "log.hpp" CommandInterpreter::CommandInterpreter() : _file(), _lineBuf(), _lineIndex(-1), _waitForStop(false) { } bool CommandInterpreter::init(const QString& fileName) { _file.setFileName(fileName); if (!_file.open(QIODeviceBase::ReadOnly | QIODeviceBase::Text)) { LOG_FATAL("%s", qPrintable(tr("Cannot open %1: %2").arg(fileName).arg(_file.errorString()))); return false; } _lineBuf.resize(2048); _lineIndex = 0; _waitForStop = false; connect(&_timer, SIGNAL(timeout()), this, SLOT(processNextCommand())); _waitTimer.setSingleShot(true); return true; } void CommandInterpreter::start() { _timer.start(20); // every 20 msecs = 50 times per second } static int getOnOff(const QString& s) { if (s == "on") return 1; else if (s == "off") return 0; else return -1; } void CommandInterpreter::processNextCommand() { if (_waitTimer.isActive()) return; if (_waitForStop && !Bino::instance()->stopped()) return; _waitForStop = false; _file.startTransaction(); qint64 lineLen = _file.readLine(_lineBuf.data(), _lineBuf.size()); if (lineLen < 0 && _file.atEnd()) { // eof _timer.stop(); return; } if (lineLen < 0) { // input error _timer.stop(); LOG_FATAL("%s", qPrintable(tr("Cannot read command from %1").arg(_file.fileName()))); return; } if (lineLen == _lineBuf.size() - 1 && _lineBuf[lineLen - 1] != '\n') { // overflow of the line buffer _timer.stop(); LOG_FATAL("%s", qPrintable(tr("Cannot read command from %1").arg(_file.fileName()))); return; } if (lineLen > 0 && _lineBuf[lineLen - 1] != '\n') { // an incomplete line was read; roll back and try again // on the next call to this function _file.rollbackTransaction(); return; } _file.commitTransaction(); _lineIndex++; if (lineLen == 0) { return; } QString cmd = QString(_lineBuf.data()).simplified(); LOG_DEBUG("Command line %d: %s", _lineIndex, qPrintable(cmd)); // empty lines and comments if (cmd.length() == 0 || cmd[0] == '#') return; if (cmd == "quit") { Bino::instance()->quit(); } else if (cmd.startsWith("wait ")) { if (cmd.mid(5) == "stop") { _waitForStop = true; } else { bool ok; float val = cmd.mid(5).toFloat(&ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { _waitTimer.start(val * 1000); } } } else if (cmd.startsWith("open ")) { QCommandLineParser parser; parser.addPositionalArgument("URL", "x"); parser.addOption({ "input", "", "x" }); parser.addOption({ "surround", "", "x" }); parser.addOption({ "video-track", "", "x" }); parser.addOption({ "audio-track", "", "x" }); parser.addOption({ "subtitle-track", "", "x" }); if (!parser.parse(cmd.split(' '))) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else if (parser.positionalArguments().length() == 0 || parser.positionalArguments().length() > 1) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { QUrl url = parser.positionalArguments()[0]; InputMode inputMode = Input_Unknown; SurroundMode surroundMode = Surround_Unknown; int videoTrack = PlaylistEntry::DefaultTrack; int audioTrack = PlaylistEntry::DefaultTrack; int subtitleTrack = PlaylistEntry::NoTrack; bool ok = true; if (parser.isSet("input")) { inputMode = inputModeFromString(parser.value("input"), &ok); if (!ok) LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--input"))); } if (parser.isSet("surround")) { surroundMode = surroundModeFromString(parser.value("surround"), &ok); if (!ok) LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } if (parser.isSet("video-track")) { int t = parser.value("video-track").toInt(&ok); if (ok && t >= 0) { videoTrack = t; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } if (parser.isSet("audio-track")) { int t = parser.value("audio-track").toInt(&ok); if (ok && t >= 0) { audioTrack = t; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } if (parser.isSet("subtitle-track") && parser.value("subtitle-track").length() > 0) { int t = parser.value("subtitle-track").toInt(&ok); if (ok && t >= 0) { subtitleTrack = t; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } if (ok) { Bino::instance()->startPlaylistMode(); Playlist::instance()->clear(); Playlist::instance()->append(PlaylistEntry(url, inputMode, surroundMode, videoTrack, audioTrack, subtitleTrack)); Playlist::instance()->start(); } } } else if (cmd.startsWith("capture") && (cmd[7] == '\0' || cmd[7] == ' ')) { QCommandLineParser parser; parser.addOption({ "audio-input", "", "x" }); parser.addOption({ "video-input", "", "x" }); parser.addOption({ "screen-input", "", "x" }); parser.addOption({ "window-input", "", "x" }); if (!parser.parse(cmd.split(' '))) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { bool ok; QList audioInputDevices; QList videoInputDevices; QList screenInputDevices; QList windowInputDevices; int audioInputDeviceIndex = -1; int videoInputDeviceIndex = -1; int screenInputDeviceIndex = -1; int windowInputDeviceIndex = -1; if (parser.isSet("audio-input")) { if (parser.value("audio-input").length() == 0) { audioInputDeviceIndex = -2; // this means no audio input } else { audioInputDevices = QMediaDevices::audioInputs(); int ai = parser.value("audio-input").toInt(&ok); if (ok && ai >= 0 && ai < audioInputDevices.size()) { audioInputDeviceIndex = ai; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } } if (parser.isSet("video-input")) { videoInputDevices = QMediaDevices::videoInputs(); int vi = parser.value("video-input").toInt(&ok); if (ok && vi >= 0 && vi < videoInputDevices.size()) { videoInputDeviceIndex = vi; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } if (parser.isSet("screen-input")) { screenInputDevices = QGuiApplication::screens(); int vi = parser.value("screen-input").toInt(&ok); if (ok && vi >= 0 && vi < screenInputDevices.size()) { screenInputDeviceIndex = vi; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } if (parser.isSet("window-input")) { windowInputDevices = QWindowCapture::capturableWindows(); int vi = parser.value("window-input").toInt(&ok); if (ok && vi >= 0 && vi < windowInputDevices.size()) { windowInputDeviceIndex = vi; } else { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); ok = false; } } if (ok) { if (screenInputDeviceIndex < 0 && windowInputDeviceIndex < 0) { Bino::instance()->startCaptureModeCamera(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), videoInputDeviceIndex >= 0 ? videoInputDevices[videoInputDeviceIndex] : QMediaDevices::defaultVideoInput()); } else if (screenInputDeviceIndex >= 0) { Bino::instance()->startCaptureModeScreen(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), screenInputDevices[screenInputDeviceIndex]); } else { Bino::instance()->startCaptureModeWindow(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), windowInputDevices[windowInputDeviceIndex]); } } } } else if (cmd.startsWith("set-output-mode ")) { bool ok; OutputMode outputMode = outputModeFromString(cmd.mid(16), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Gui* gui = Gui::instance(); if (gui) gui->setOutputMode(outputMode); } } else if (cmd.startsWith("set-surround-vfov ")) { bool ok; float surroundVerticalFOV = cmd.mid(18).toFloat(&ok); if (!ok || surroundVerticalFOV < 5.0f || surroundVerticalFOV > 115.0f) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Gui* gui = Gui::instance(); if (gui) gui->setSurroundVerticalFieldOfView(surroundVerticalFOV); } } else if (cmd == "play") { Bino::instance()->play(); } else if (cmd == "stop") { Bino::instance()->stop(); } else if (cmd == "pause") { Bino::instance()->pause(); } else if (cmd == "toggle-pause") { Bino::instance()->togglePause(); } else if (cmd.startsWith("set-position ")) { bool ok; float val = cmd.mid(13).toFloat(&ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Bino::instance()->setPosition(val); } } else if (cmd.startsWith("playlist-load ")) { QString name = cmd.mid(14); QString errStr; bool load(const QString& fileName, QString& errStr); if (!(Playlist::instance()->load(name, errStr))) { LOG_FATAL("%s", qPrintable(tr("%1: %2").arg(name).arg(errStr))); } } else if (cmd == "playlist-next") { Playlist::instance()->next(); } else if (cmd == "playlist-prev") { Playlist::instance()->prev(); } else if (cmd.startsWith("playlist-wait ")) { bool ok; WaitMode waitMode = waitModeFromString(cmd.mid(14), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Playlist::instance()->setWaitMode(waitMode); } } else if (cmd.startsWith("playlist-loop ")) { bool ok; LoopMode loopMode = loopModeFromString(cmd.mid(14), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Playlist::instance()->setLoopMode(loopMode); } } else if (cmd.startsWith("seek ")) { bool ok; float val = cmd.mid(5).toFloat(&ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Bino::instance()->seek(val); } } else if (cmd.startsWith("set-swap-eyes ")) { int onoff = getOnOff(cmd.mid(14)); if (onoff < 0) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Bino::instance()->setSwapEyes(onoff); } } else if (cmd == "toggle-swap-eyes") { Bino::instance()->toggleSwapEyes(); } else if (cmd.startsWith("set-fullscreen ")) { int onoff = getOnOff(cmd.mid(15)); if (onoff < 0) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Gui* gui = Gui::instance(); if (gui) gui->setFullscreen(onoff); } } else if (cmd == "toggle-fullscreen") { Gui* gui = Gui::instance(); if (gui) gui->viewToggleFullscreen(); } else if (cmd.startsWith("set-mute ")) { int onoff = getOnOff(cmd.mid(9)); if (onoff < 0) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Bino::instance()->setMute(onoff); } } else if (cmd == "toggle-mute") { Bino::instance()->toggleMute(); } else if (cmd.startsWith("set-volume ")) { bool ok; float val = cmd.mid(11).toFloat(&ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Bino::instance()->setVolume(val); } } else if (cmd.startsWith("adjust-volume ")) { bool ok; float val = cmd.mid(14).toFloat(&ok); if (!ok) { LOG_FATAL("%s", qPrintable(tr("Invalid argument in %1 line %2").arg(_file.fileName()).arg(_lineIndex))); } else { Bino::instance()->changeVolume(val); } } else { LOG_FATAL("%s", qPrintable(tr("Invalid command %1 line %2").arg(cmd).arg(_lineIndex))); } } bino-2.5/src/commandinterpreter.hpp000066400000000000000000000022221475415313200175140ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ #pragma once #include #include #include class CommandInterpreter : public QObject { Q_OBJECT private: QTimer _timer; QTimer _waitTimer; QFile _file; QList _lineBuf; int _lineIndex; bool _waitForStop; public: CommandInterpreter(); bool init(const QString& fileName); void start(); public slots: void processNextCommand(); }; bino-2.5/src/digestiblemedia.cpp000066400000000000000000000134621475415313200167300ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2024 * Martin Lambers * * 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 . */ #include #include #include #include #include #include #include "urlloader.hpp" #include "tools.hpp" #include "log.hpp" #include "digestiblemedia.hpp" /* We convert JPEGs to temporary PPMs here, for the following reasons: * - QtMultimedia tries to decode JPEGs with hardware acceleration, which fails * when the image dimensions (or other properties) are not within the * constraints of the hardware decoder, which is optimized for video. * This happens often with both the GStreamer and FFmpeg backends. * Unfortunately there does not seem to be a fallback to software decoding. * - MPO files can contain multiple JPEG images. In the only relevant use case, * they contain a left and a right JPEG with a lot of junk in between (probably * called metadata by some standard). These files typically cannot be read * reliably by either QtMultimedia backend. So we read both JPEGs manually * from the MPO and stack them on top of each other (top-bottom format). * - The destination image format is PPM because it is fast to write (no * compression) and the media backend will not be tempted to try hardware * accelerated decoding on PPMs. */ QUrl digestibleMediaUrl(const QUrl& url) { static QMap> cache; // check if we need conversion QString extension = getExtension(url); bool needsConversion = (extension == "jpg" || extension == "jpeg" || extension == "jps" || extension == "mpo"); if (!needsConversion) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1 needs no conversion").arg(url.toString()))); return url; } // check if we have it cached QSharedPointer tempFile = nullptr; auto it = cache.find(url); if (it != cache.end()) { tempFile = it.value(); LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1 is in cache: %2").arg(url.toString()).arg(tempFile->fileName()))); } // build temporary file if it is not in cache yet if (!tempFile) { UrlLoader loader(url); const QByteArray& data = loader.load(); if (data.size() == 0) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot download").arg(url.toString()))); return url; } QImage img; if (!img.loadFromData(data, "JPG")) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot load JPEG").arg(url.toString()))); return url; } if (url.fileName().endsWith("mpo", Qt::CaseInsensitive)) { unsigned char jpegMarker[4] = { 0xff, 0xd8, 0xff, 0xe1 }; QByteArrayView jpegMarkerView(jpegMarker, 4); qsizetype nextJpeg = data.indexOf(jpegMarkerView, 4); if (nextJpeg <= 0) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: no second jpeg marker found").arg(url.toString()))); } else { QImage imgRight; if (!imgRight.loadFromData(QByteArrayView(data.data() + nextJpeg, data.size() - nextJpeg), "JPG")) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot load second jpeg").arg(url.toString()))); } else { if (img.format() != imgRight.format() || img.size() != imgRight.size()) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: second jpeg is incompatible").arg(url.toString()))); } else { QImage combinedImg(img.width(), 2 * img.height(), img.format()); for (int i = 0; i < img.height(); i++) { std::memcpy(combinedImg.scanLine(i), img.constScanLine(i), img.bytesPerLine()); } for (int i = 0; i < img.height(); i++) { std::memcpy(combinedImg.scanLine(img.height() + i), imgRight.constScanLine(i), imgRight.bytesPerLine()); } img = combinedImg; } } } } QString cacheDirName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir cacheDir(cacheDirName); if (!cacheDir.mkpath(cacheDirName)) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot create cache directory %2").arg(url.toString()).arg(cacheDirName))); return url; } QString tmpl = cacheDirName + '/' + QString("bino-XXXXXX") + QString(".ppm"); tempFile.reset(new QTemporaryFile(tmpl)); if (!img.save(tempFile.get(), "PPM")) { LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot save to %2").arg(url.toString()).arg(tempFile->fileName()))); return url; } LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1 is saved in %2").arg(url.toString()).arg(tempFile->fileName()))); cache.insert(url, tempFile); } return QUrl::fromLocalFile(tempFile->fileName()); } bino-2.5/src/digestiblemedia.hpp000066400000000000000000000015121475415313200167260ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2024 * Martin Lambers * * 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 . */ #pragma once #include QUrl digestibleMediaUrl(const QUrl& url); bino-2.5/src/gui.cpp000066400000000000000000001351611475415313200144020ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gui.hpp" #include "playlist.hpp" #include "playlisteditor.hpp" #include "metadata.hpp" #include "version.hpp" #include "log.hpp" QMenu* Gui::addBinoMenu(const QString& title) { QMenu* menu = menuBar()->addMenu(title); _contextMenu->addMenu(menu); return menu; } void Gui::addBinoAction(QAction* action, QMenu* menu) { menu->addAction(action); _widget->addAction(action); } static Gui* GuiSingleton = nullptr; Gui::Gui(OutputMode outputMode, float surroundVerticalFOV, bool fullscreen) : QMainWindow(), _widget(new Widget(outputMode, surroundVerticalFOV, this)), _contextMenu(new QMenu(this)) { setWindowTitle("Bino"); QPixmap icon; if (!icon.load(":res/bino-logo-small.svg")) { LOG_DEBUG("Gui: falling back to raster icon"); icon.load(":res/bino-logo-small-512.png"); } setWindowIcon(QIcon(icon)); QMenu* fileMenu = addBinoMenu(tr("&File")); QAction* fileOpenAction = new QAction(tr("&Open File(s)..."), this); fileOpenAction->setShortcuts({ QKeySequence::Open }); connect(fileOpenAction, SIGNAL(triggered()), this, SLOT(fileOpen())); addBinoAction(fileOpenAction, fileMenu); QAction* fileOpenURLAction = new QAction(tr("Open &URL..."), this); connect(fileOpenURLAction, SIGNAL(triggered()), this, SLOT(fileOpenURL())); addBinoAction(fileOpenURLAction, fileMenu); QAction* fileOpenCameraAction = new QAction(tr("Open &Capture Device..."), this); connect(fileOpenCameraAction, SIGNAL(triggered()), this, SLOT(fileOpenCamera())); addBinoAction(fileOpenCameraAction, fileMenu); fileMenu->addSeparator(); QAction* fileQuitAction = new QAction(tr("&Quit"), this); fileQuitAction->setShortcuts({ QKeySequence::Quit }); connect(fileQuitAction, SIGNAL(triggered()), this, SLOT(fileQuit())); addBinoAction(fileQuitAction, fileMenu); _trackMenu = addBinoMenu(tr("&Tracks")); _trackVideoActionGroup = new QActionGroup(this); _trackAudioActionGroup = new QActionGroup(this); _trackSubtitleActionGroup = new QActionGroup(this); QMenu* playlistMenu = addBinoMenu(tr("&Playlist")); QAction* playlistLoadAction = new QAction(tr("&Load...")); connect(playlistLoadAction, SIGNAL(triggered()), this, SLOT(playlistLoad())); addBinoAction(playlistLoadAction, playlistMenu); QAction* playlistSaveAction = new QAction(tr("&Save...")); connect(playlistSaveAction, SIGNAL(triggered()), this, SLOT(playlistSave())); addBinoAction(playlistSaveAction, playlistMenu); QAction* playlistEditAction = new QAction(tr("&Edit...")); connect(playlistEditAction, SIGNAL(triggered()), this, SLOT(playlistEdit())); addBinoAction(playlistEditAction, playlistMenu); playlistMenu->addSeparator(); QAction* playlistNextAction = new QAction(tr("&Next")); playlistNextAction->setShortcuts({ Qt::Key_N }); connect(playlistNextAction, SIGNAL(triggered()), this, SLOT(playlistNext())); addBinoAction(playlistNextAction, playlistMenu); QAction* playlistPreviousAction = new QAction(tr("&Previous")); playlistPreviousAction->setShortcuts({ Qt::Key_P }); connect(playlistPreviousAction, SIGNAL(triggered()), this, SLOT(playlistPrevious())); addBinoAction(playlistPreviousAction, playlistMenu); playlistMenu->addSeparator(); _playlistLoopActionGroup = new QActionGroup(this); QAction* playlistLoopOff = new QAction(loopModeToStringUI(Loop_Off), this); playlistLoopOff->setCheckable(true); _playlistLoopActionGroup->addAction(playlistLoopOff)->setData(int(Loop_Off)); connect(playlistLoopOff, SIGNAL(triggered()), this, SLOT(playlistLoop())); addBinoAction(playlistLoopOff, playlistMenu); QAction* playlistLoopOne = new QAction(loopModeToStringUI(Loop_One), this); playlistLoopOne->setCheckable(true); _playlistLoopActionGroup->addAction(playlistLoopOne)->setData(int(Loop_One)); connect(playlistLoopOne, SIGNAL(triggered()), this, SLOT(playlistLoop())); addBinoAction(playlistLoopOne, playlistMenu); QAction* playlistLoopAll = new QAction(loopModeToStringUI(Loop_All), this); playlistLoopAll->setCheckable(true); _playlistLoopActionGroup->addAction(playlistLoopAll)->setData(int(Loop_All)); connect(playlistLoopAll, SIGNAL(triggered()), this, SLOT(playlistLoop())); addBinoAction(playlistLoopAll, playlistMenu); playlistMenu->addSeparator(); _playlistWaitActionGroup = new QActionGroup(this); QAction* playlistWaitOff = new QAction(waitModeToStringUI(Wait_Off), this); playlistWaitOff->setCheckable(true); _playlistWaitActionGroup->addAction(playlistWaitOff)->setData(int(Wait_Off)); connect(playlistWaitOff, SIGNAL(triggered()), this, SLOT(playlistWait())); addBinoAction(playlistWaitOff, playlistMenu); QAction* playlistWaitOn = new QAction(waitModeToStringUI(Wait_On), this); playlistWaitOn->setCheckable(true); _playlistWaitActionGroup->addAction(playlistWaitOn)->setData(int(Wait_On)); connect(playlistWaitOn, SIGNAL(triggered()), this, SLOT(playlistWait())); addBinoAction(playlistWaitOn, playlistMenu); QMenu* threeDMenu = addBinoMenu(tr("&3D Modes")); _3dSurroundActionGroup = new QActionGroup(this); QAction* threeDSurroundOff = new QAction(surroundModeToStringUI(Surround_Off), this); threeDSurroundOff->setCheckable(true); _3dSurroundActionGroup->addAction(threeDSurroundOff)->setData(int(Surround_Off)); connect(threeDSurroundOff, SIGNAL(triggered()), this, SLOT(threeDSurround())); addBinoAction(threeDSurroundOff, threeDMenu); QAction* threeDSurround180 = new QAction(surroundModeToStringUI(Surround_180), this); threeDSurround180->setCheckable(true); _3dSurroundActionGroup->addAction(threeDSurround180)->setData(int(Surround_180)); connect(threeDSurround180, SIGNAL(triggered()), this, SLOT(threeDSurround())); addBinoAction(threeDSurround180, threeDMenu); QAction* threeDSurround360 = new QAction(surroundModeToStringUI(Surround_360), this); threeDSurround360->setCheckable(true); _3dSurroundActionGroup->addAction(threeDSurround360)->setData(int(Surround_360)); connect(threeDSurround360, SIGNAL(triggered()), this, SLOT(threeDSurround())); addBinoAction(threeDSurround360, threeDMenu); threeDMenu->addSeparator(); _3dInputActionGroup = new QActionGroup(this); QAction* threeDInMono = new QAction(tr("Input 2D"), this); threeDInMono->setCheckable(true); _3dInputActionGroup->addAction(threeDInMono)->setData(int(Input_Mono)); connect(threeDInMono, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInMono, threeDMenu); QMenu* threeDInputAboveBelowMenu = threeDMenu->addMenu(tr("Input 3D above-below")); QAction* threeDInTopBottom = new QAction(inputModeToStringUI(Input_Top_Bottom), this); threeDInTopBottom->setCheckable(true); _3dInputActionGroup->addAction(threeDInTopBottom)->setData(int(Input_Top_Bottom)); connect(threeDInTopBottom, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInTopBottom, threeDInputAboveBelowMenu); QAction* threeDInTopBottomHalf = new QAction(inputModeToStringUI(Input_Top_Bottom_Half), this); threeDInTopBottomHalf->setCheckable(true); _3dInputActionGroup->addAction(threeDInTopBottomHalf)->setData(int(Input_Top_Bottom_Half)); connect(threeDInTopBottomHalf, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInTopBottomHalf, threeDInputAboveBelowMenu); QAction* threeDInBottomTop = new QAction(inputModeToStringUI(Input_Bottom_Top), this); threeDInBottomTop->setCheckable(true); _3dInputActionGroup->addAction(threeDInBottomTop)->setData(int(Input_Bottom_Top)); connect(threeDInBottomTop, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInBottomTop, threeDInputAboveBelowMenu); QAction* threeDInBottomTopHalf = new QAction(inputModeToStringUI(Input_Bottom_Top_Half), this); threeDInBottomTopHalf->setCheckable(true); _3dInputActionGroup->addAction(threeDInBottomTopHalf)->setData(int(Input_Bottom_Top_Half)); connect(threeDInBottomTopHalf, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInBottomTopHalf, threeDInputAboveBelowMenu); QMenu* threeDInputSideBySideMenu = threeDMenu->addMenu(tr("Input 3D side-by-side")); QAction* threeDInLeftRight = new QAction(inputModeToStringUI(Input_Left_Right), this); threeDInLeftRight->setCheckable(true); _3dInputActionGroup->addAction(threeDInLeftRight)->setData(int(Input_Left_Right)); connect(threeDInLeftRight, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInLeftRight, threeDInputSideBySideMenu); QAction* threeDInLeftRightHalf = new QAction(inputModeToStringUI(Input_Left_Right_Half), this); threeDInLeftRightHalf->setCheckable(true); _3dInputActionGroup->addAction(threeDInLeftRightHalf)->setData(int(Input_Left_Right_Half)); connect(threeDInLeftRightHalf, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInLeftRightHalf, threeDInputSideBySideMenu); QAction* threeDInRightLeft = new QAction(inputModeToStringUI(Input_Right_Left), this); threeDInRightLeft->setCheckable(true); _3dInputActionGroup->addAction(threeDInRightLeft)->setData(int(Input_Right_Left)); connect(threeDInRightLeft, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInRightLeft, threeDInputSideBySideMenu); QAction* threeDInRightLeftHalf = new QAction(inputModeToStringUI(Input_Right_Left_Half), this); threeDInRightLeftHalf->setCheckable(true); _3dInputActionGroup->addAction(threeDInRightLeftHalf)->setData(int(Input_Right_Left_Half)); connect(threeDInRightLeftHalf, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInRightLeftHalf, threeDInputSideBySideMenu); QMenu* threeDInputAlternatingMenu = threeDMenu->addMenu(tr("Input 3D alternating")); QAction* threeDInAlternatingLR = new QAction(inputModeToStringUI(Input_Alternating_LR), this); threeDInAlternatingLR->setCheckable(true); _3dInputActionGroup->addAction(threeDInAlternatingLR)->setData(int(Input_Alternating_LR)); connect(threeDInAlternatingLR, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInAlternatingLR, threeDInputAlternatingMenu); QAction* threeDInAlternatingRL = new QAction(inputModeToStringUI(Input_Alternating_RL), this); threeDInAlternatingRL->setCheckable(true); _3dInputActionGroup->addAction(threeDInAlternatingRL)->setData(int(Input_Alternating_RL)); connect(threeDInAlternatingRL, SIGNAL(triggered()), this, SLOT(threeDInput())); addBinoAction(threeDInAlternatingRL, threeDInputAlternatingMenu); threeDMenu->addSeparator(); _3dOutputActionGroup = new QActionGroup(this); QAction* threeDOutLeft = new QAction(outputModeToStringUI(Output_Left), this); threeDOutLeft->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutLeft)->setData(int(Output_Left)); connect(threeDOutLeft, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutLeft, threeDMenu); QAction* threeDOutRight = new QAction(outputModeToStringUI(Output_Right), this); threeDOutRight->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRight)->setData(int(Output_Right)); connect(threeDOutRight, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRight, threeDMenu); QAction* threeDOutStereo = new QAction(outputModeToStringUI(Output_OpenGL_Stereo), this); threeDOutStereo->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutStereo)->setData(int(Output_OpenGL_Stereo)); connect(threeDOutStereo, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutStereo, threeDMenu); QAction* threeDOutAlternating = new QAction(outputModeToStringUI(Output_Alternating), this); threeDOutAlternating->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutAlternating)->setData(int(Output_Alternating)); connect(threeDOutAlternating, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutAlternating, threeDMenu); QAction* threeDOutHDMIFramePack = new QAction(outputModeToStringUI(Output_HDMI_Frame_Pack), this); threeDOutHDMIFramePack->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutHDMIFramePack)->setData(int(Output_HDMI_Frame_Pack)); connect(threeDOutHDMIFramePack, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutHDMIFramePack, threeDMenu); QMenu* threeDOutputSideBySideMenu = threeDMenu->addMenu(tr("Output 3D side-by-side")); QAction* threeDOutLR = new QAction(outputModeToStringUI(Output_Left_Right), this); threeDOutLR->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutLR)->setData(int(Output_Left_Right)); connect(threeDOutLR, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutLR, threeDOutputSideBySideMenu); QAction* threeDOutLRH = new QAction(outputModeToStringUI(Output_Left_Right_Half), this); threeDOutLRH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutLRH)->setData(int(Output_Left_Right_Half)); connect(threeDOutLRH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutLRH, threeDOutputSideBySideMenu); QAction* threeDOutRL = new QAction(outputModeToStringUI(Output_Right_Left), this); threeDOutRL->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRL)->setData(int(Output_Right_Left)); connect(threeDOutRL, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRL, threeDOutputSideBySideMenu); QAction* threeDOutRLH = new QAction(outputModeToStringUI(Output_Right_Left_Half), this); threeDOutRLH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRLH)->setData(int(Output_Right_Left_Half)); connect(threeDOutRLH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRLH, threeDOutputSideBySideMenu); QMenu* threeDOutputAboveBelowMenu = threeDMenu->addMenu(tr("Output 3D above-below")); QAction* threeDOutTB = new QAction(outputModeToStringUI(Output_Top_Bottom), this); threeDOutTB->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutTB)->setData(int(Output_Top_Bottom)); connect(threeDOutTB, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutTB, threeDOutputAboveBelowMenu); QAction* threeDOutTBH = new QAction(outputModeToStringUI(Output_Top_Bottom_Half), this); threeDOutTBH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutTBH)->setData(int(Output_Top_Bottom_Half)); connect(threeDOutTBH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutTBH, threeDOutputAboveBelowMenu); QAction* threeDOutBT = new QAction(outputModeToStringUI(Output_Bottom_Top), this); threeDOutBT->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutBT)->setData(int(Output_Bottom_Top)); connect(threeDOutBT, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutBT, threeDOutputAboveBelowMenu); QAction* threeDOutBTH = new QAction(outputModeToStringUI(Output_Bottom_Top_Half), this); threeDOutBTH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutBTH)->setData(int(Output_Bottom_Top_Half)); connect(threeDOutBTH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutBTH, threeDOutputAboveBelowMenu); QMenu* threeDOutputInterleavedMenu = threeDMenu->addMenu(tr("Output 3D interleaved")); QAction* threeDOutEOR = new QAction(outputModeToStringUI(Output_Even_Odd_Rows), this); threeDOutEOR->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutEOR)->setData(int(Output_Even_Odd_Rows)); connect(threeDOutEOR, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutEOR, threeDOutputInterleavedMenu); QAction* threeDOutEOC = new QAction(outputModeToStringUI(Output_Even_Odd_Columns), this); threeDOutEOC->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutEOC)->setData(int(Output_Even_Odd_Columns)); connect(threeDOutEOC, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutEOC, threeDOutputInterleavedMenu); QAction* threeDOutCheckerboard = new QAction(outputModeToStringUI(Output_Checkerboard), this); threeDOutCheckerboard->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutCheckerboard)->setData(int(Output_Checkerboard)); connect(threeDOutCheckerboard, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutCheckerboard, threeDOutputInterleavedMenu); QMenu* threeDOutputAnaglyphMenu = threeDMenu->addMenu(tr("Output 3D anaglyph")); QAction* threeDOutRCD = new QAction(outputModeToStringUI(Output_Red_Cyan_Dubois), this); threeDOutRCD->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRCD)->setData(int(Output_Red_Cyan_Dubois)); connect(threeDOutRCD, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRCD, threeDOutputAnaglyphMenu); QAction* threeDOutRCF = new QAction(outputModeToStringUI(Output_Red_Cyan_FullColor), this); threeDOutRCF->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRCF)->setData(int(Output_Red_Cyan_FullColor)); connect(threeDOutRCF, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRCF, threeDOutputAnaglyphMenu); QAction* threeDOutRCH = new QAction(outputModeToStringUI(Output_Red_Cyan_HalfColor), this); threeDOutRCH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRCH)->setData(int(Output_Red_Cyan_HalfColor)); connect(threeDOutRCH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRCH, threeDOutputAnaglyphMenu); QAction* threeDOutRCM = new QAction(outputModeToStringUI(Output_Red_Cyan_Monochrome), this); threeDOutRCM->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRCM)->setData(int(Output_Red_Cyan_Monochrome)); connect(threeDOutRCM, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRCM, threeDOutputAnaglyphMenu); QAction* threeDOutGMD = new QAction(outputModeToStringUI(Output_Green_Magenta_Dubois), this); threeDOutGMD->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutGMD)->setData(int(Output_Green_Magenta_Dubois)); connect(threeDOutGMD, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutGMD, threeDOutputAnaglyphMenu); QAction* threeDOutGMF = new QAction(outputModeToStringUI(Output_Green_Magenta_FullColor), this); threeDOutGMF->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutGMF)->setData(int(Output_Green_Magenta_FullColor)); connect(threeDOutGMF, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutGMF, threeDOutputAnaglyphMenu); QAction* threeDOutGMH = new QAction(outputModeToStringUI(Output_Green_Magenta_HalfColor), this); threeDOutGMH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutGMH)->setData(int(Output_Green_Magenta_HalfColor)); connect(threeDOutGMH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutGMH, threeDOutputAnaglyphMenu); QAction* threeDOutGMM = new QAction(outputModeToStringUI(Output_Green_Magenta_Monochrome), this); threeDOutGMM->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutGMM)->setData(int(Output_Green_Magenta_Monochrome)); connect(threeDOutGMM, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutGMM, threeDOutputAnaglyphMenu); QAction* threeDOutABD = new QAction(outputModeToStringUI(Output_Amber_Blue_Dubois), this); threeDOutABD->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutABD)->setData(int(Output_Amber_Blue_Dubois)); connect(threeDOutABD, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutABD, threeDOutputAnaglyphMenu); QAction* threeDOutABF = new QAction(outputModeToStringUI(Output_Amber_Blue_FullColor), this); threeDOutABF->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutABF)->setData(int(Output_Amber_Blue_FullColor)); connect(threeDOutABF, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutABF, threeDOutputAnaglyphMenu); QAction* threeDOutABH = new QAction(outputModeToStringUI(Output_Amber_Blue_HalfColor), this); threeDOutABH->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutABH)->setData(int(Output_Amber_Blue_HalfColor)); connect(threeDOutABH, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutABH, threeDOutputAnaglyphMenu); QAction* threeDOutABM = new QAction(outputModeToStringUI(Output_Amber_Blue_Monochrome), this); threeDOutABM->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutABM)->setData(int(Output_Amber_Blue_Monochrome)); connect(threeDOutABM, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutABM, threeDOutputAnaglyphMenu); QAction* threeDOutRGM = new QAction(outputModeToStringUI(Output_Red_Green_Monochrome), this); threeDOutRGM->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRGM)->setData(int(Output_Red_Green_Monochrome)); connect(threeDOutRGM, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRGM, threeDOutputAnaglyphMenu); QAction* threeDOutRBM = new QAction(outputModeToStringUI(Output_Red_Blue_Monochrome), this); threeDOutRBM->setCheckable(true); _3dOutputActionGroup->addAction(threeDOutRBM)->setData(int(Output_Red_Blue_Monochrome)); connect(threeDOutRBM, SIGNAL(triggered()), this, SLOT(threeDOutput())); addBinoAction(threeDOutRBM, threeDOutputAnaglyphMenu); QMenu* mediaMenu = addBinoMenu(tr("&Media")); _mediaToggleVolumeMuteAction = new QAction(tr("Mute audio"), this); _mediaToggleVolumeMuteAction->setShortcuts({ Qt::Key_M }); _mediaToggleVolumeMuteAction->setCheckable(true); connect(_mediaToggleVolumeMuteAction, SIGNAL(triggered()), this, SLOT(mediaToggleVolumeMute())); addBinoAction(_mediaToggleVolumeMuteAction, mediaMenu); _mediaVolumeIncAction = new QAction(tr("Increase audio volume"), this); _mediaVolumeIncAction->setShortcuts({ Qt::Key_VolumeUp }); connect(_mediaVolumeIncAction, SIGNAL(triggered()), this, SLOT(mediaVolumeInc())); addBinoAction(_mediaVolumeIncAction, mediaMenu); _mediaVolumeDecAction = new QAction(tr("Decrease audio volume"), this); _mediaVolumeDecAction->setShortcuts({ Qt::Key_VolumeDown }); connect(_mediaVolumeDecAction, SIGNAL(triggered()), this, SLOT(mediaVolumeDec())); addBinoAction(_mediaVolumeDecAction, mediaMenu); mediaMenu->addSeparator(); _mediaTogglePauseAction = new QAction(tr("Pause"), this); _mediaTogglePauseAction->setShortcuts({ Qt::Key_Space }); _mediaTogglePauseAction->setCheckable(true); connect(_mediaTogglePauseAction, SIGNAL(triggered()), this, SLOT(mediaTogglePause())); addBinoAction(_mediaTogglePauseAction, mediaMenu); _mediaSeekFwd1SecAction = new QAction(tr("Seek forward 1 second"), this); _mediaSeekFwd1SecAction->setShortcuts({ Qt::Key_Period }); connect(_mediaSeekFwd1SecAction, SIGNAL(triggered()), this, SLOT(mediaSeekFwd1Sec())); addBinoAction(_mediaSeekFwd1SecAction, mediaMenu); _mediaSeekBwd1SecAction = new QAction(tr("Seek backwards 1 second"), this); _mediaSeekBwd1SecAction->setShortcuts({ Qt::Key_Comma }); connect(_mediaSeekBwd1SecAction, SIGNAL(triggered()), this, SLOT(mediaSeekBwd1Sec())); addBinoAction(_mediaSeekBwd1SecAction, mediaMenu); _mediaSeekFwd10SecsAction = new QAction(tr("Seek forward 10 seconds"), this); _mediaSeekFwd10SecsAction->setShortcuts({ Qt::Key_Right }); connect(_mediaSeekFwd10SecsAction, SIGNAL(triggered()), this, SLOT(mediaSeekFwd10Secs())); addBinoAction(_mediaSeekFwd10SecsAction, mediaMenu); _mediaSeekBwd10SecsAction = new QAction(tr("Seek backwards 10 seconds"), this); _mediaSeekBwd10SecsAction->setShortcuts({ Qt::Key_Left }); connect(_mediaSeekBwd10SecsAction, SIGNAL(triggered()), this, SLOT(mediaSeekBwd10Secs())); addBinoAction(_mediaSeekBwd10SecsAction, mediaMenu); _mediaSeekFwd1MinAction = new QAction(tr("Seek forward 1 minute"), this); _mediaSeekFwd1MinAction->setShortcuts({ Qt::Key_Up }); connect(_mediaSeekFwd1MinAction, SIGNAL(triggered()), this, SLOT(mediaSeekFwd1Min())); addBinoAction(_mediaSeekFwd1MinAction, mediaMenu); _mediaSeekBwd1MinAction = new QAction(tr("Seek backwards 1 minute"), this); _mediaSeekBwd1MinAction->setShortcuts({ Qt::Key_Down }); connect(_mediaSeekBwd1MinAction, SIGNAL(triggered()), this, SLOT(mediaSeekBwd1Min())); addBinoAction(_mediaSeekBwd1MinAction, mediaMenu); _mediaSeekFwd10MinsAction = new QAction(tr("Seek forward 10 minutes"), this); _mediaSeekFwd10MinsAction->setShortcuts({ Qt::Key_PageUp }); connect(_mediaSeekFwd10MinsAction, SIGNAL(triggered()), this, SLOT(mediaSeekFwd10Mins())); addBinoAction(_mediaSeekFwd10MinsAction, mediaMenu); _mediaSeekBwd10MinsAction = new QAction(tr("Seek backwards 10 minutes"), this); _mediaSeekBwd10MinsAction->setShortcuts({ Qt::Key_PageDown }); connect(_mediaSeekBwd10MinsAction, SIGNAL(triggered()), this, SLOT(mediaSeekBwd10Mins())); addBinoAction(_mediaSeekBwd10MinsAction, mediaMenu); QMenu* viewMenu = addBinoMenu(tr("&View")); _viewToggleFullscreenAction = new QAction(tr("&Fullscreen"), this); _viewToggleFullscreenAction->setShortcuts({ Qt::Key_F11, Qt::Key_F, QKeySequence::FullScreen }); _viewToggleFullscreenAction->setCheckable(true); connect(_viewToggleFullscreenAction, SIGNAL(triggered()), this, SLOT(viewToggleFullscreen())); addBinoAction(_viewToggleFullscreenAction, viewMenu); _viewToggleSwapEyesAction = new QAction(tr("&Swap eyes"), this); _viewToggleSwapEyesAction->setShortcuts({ Qt::Key_F7 }); _viewToggleSwapEyesAction->setCheckable(true); connect(_viewToggleSwapEyesAction, SIGNAL(triggered()), this, SLOT(viewToggleSwapEyes())); addBinoAction(_viewToggleSwapEyesAction, viewMenu); viewMenu->addSeparator(); _viewResetSurroundAction = new QAction(tr("Reset surround view"), this); _viewResetSurroundAction->setShortcuts({ Qt::Key_Z }); connect(_viewResetSurroundAction, SIGNAL(triggered()), this, SLOT(viewResetSurround())); addBinoAction(_viewResetSurroundAction, viewMenu); QMenu* helpMenu = addBinoMenu(tr("&Help")); QAction* helpAboutAction = new QAction(tr("&About..."), this); connect(helpAboutAction, SIGNAL(triggered()), this, SLOT(helpAbout())); addBinoAction(helpAboutAction, helpMenu); updateActions(); connect(Bino::instance(), SIGNAL(stateChanged()), this, SLOT(updateActions())); connect(_widget, SIGNAL(toggleFullscreen()), this, SLOT(viewToggleFullscreen())); setCentralWidget(_widget); _widget->show(); connect(Bino::instance(), SIGNAL(wantQuit()), this, SLOT(fileQuit())); setMinimumSize(menuBar()->sizeHint().width(), menuBar()->sizeHint().width() / 2); setAcceptDrops(true); if (fullscreen) viewToggleFullscreen(); Q_ASSERT(!GuiSingleton); GuiSingleton = this; // Use this to make a 800x450 screenshot for the website and Flathub: //resize(800 - 24, 450 - 70); } Gui* Gui::instance() { return GuiSingleton; } void Gui::fileOpen() { QStringList names = QFileDialog::getOpenFileNames(this); if (!names.isEmpty()) { bool playlistWasEmpty = Playlist::instance()->length() == 0; Bino::instance()->startPlaylistMode(); for (int i = 0; i < names.size(); i++) { QUrl url = QUrl::fromLocalFile(names[i]); MetaData metaData; QString errMsg; if (metaData.detectCached(url, &errMsg)) { Playlist::instance()->append(url); } else { QMessageBox::critical(this, tr("Error"), errMsg); } } Playlist::instance()->setWaitModeAuto(); if (playlistWasEmpty) Playlist::instance()->next(); } } void Gui::fileOpenURL() { QDialog *dialog = new QDialog(this); dialog->setWindowTitle(tr("Open URL")); QLabel *label = new QLabel(tr("URL:")); QLineEdit *edit = new QLineEdit(""); edit->setMinimumWidth(256); QPushButton *cancelBtn = new QPushButton(tr("Cancel")); QPushButton *okBtn = new QPushButton(tr("OK")); okBtn->setDefault(true); connect(cancelBtn, SIGNAL(clicked()), dialog, SLOT(reject())); connect(okBtn, SIGNAL(clicked()), dialog, SLOT(accept())); QGridLayout *layout = new QGridLayout(); layout->addWidget(label, 0, 0); layout->addWidget(edit, 0, 1, 1, 3); layout->addWidget(cancelBtn, 2, 2); layout->addWidget(okBtn, 2, 3); layout->setColumnStretch(1, 1); dialog->setLayout(layout); dialog->exec(); if (dialog->result() == QDialog::Accepted && !edit->text().isEmpty()) { QUrl url = QUrl::fromUserInput(edit->text()); MetaData metaData; QString errMsg; if (metaData.detectCached(url, &errMsg)) { Bino::instance()->startPlaylistMode(); Playlist::instance()->clear(); Playlist::instance()->append(url); Playlist::instance()->start(); } else { QMessageBox::critical(this, tr("Error"), errMsg); } } } void Gui::fileOpenCamera() { QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); QList audioOutputDevices = QMediaDevices::audioOutputs(); QList audioInputDevices = QMediaDevices::audioInputs(); QList videoInputDevices = QMediaDevices::videoInputs(); QList screenInputDevices = QGuiApplication::screens(); QList windowInputDevices = QWindowCapture::capturableWindows(); QGuiApplication::restoreOverrideCursor(); QDialog *dialog = new QDialog(this); dialog->setWindowTitle(tr("Open Capture Device")); QRadioButton *videoBtn = new QRadioButton(tr("Camera Input:")); QComboBox* videoBox = new QComboBox; videoBox->addItem(tr("Default")); for (int i = 0; i < videoInputDevices.size(); i++) videoBox->addItem(videoInputDevices[i].description()); QRadioButton *screenBtn = new QRadioButton(tr("Screen Input:")); QComboBox* screenBox = new QComboBox; for (int i = 0; i < screenInputDevices.size(); i++) screenBox->addItem(screenInputDevices[i]->name()); QRadioButton *windowBtn = new QRadioButton(tr("Window Input:")); QComboBox* windowBox = new QComboBox; for (int i = 0; i < windowInputDevices.size(); i++) windowBox->addItem(windowInputDevices[i].description()); videoBtn->setChecked(true); screenBtn->setChecked(false); windowBtn->setChecked(false); if (screenInputDevices.size() == 0) { screenBtn->setEnabled(false); screenBox->setEnabled(false); } if (windowInputDevices.size() == 0) { windowBtn->setEnabled(false); windowBox->setEnabled(false); } QLabel *audioLabel = new QLabel(tr("Audio Input:")); QComboBox* audioBox = new QComboBox; audioBox->addItem(tr("None")); audioBox->addItem(tr("Default")); for (int i = 0; i < audioInputDevices.size(); i++) audioBox->addItem(audioInputDevices[i].description()); QPushButton *cancelBtn = new QPushButton(tr("Cancel")); QPushButton *okBtn = new QPushButton(tr("OK")); okBtn->setDefault(true); connect(cancelBtn, SIGNAL(clicked()), dialog, SLOT(reject())); connect(okBtn, SIGNAL(clicked()), dialog, SLOT(accept())); QGridLayout *layout = new QGridLayout(); layout->addWidget(videoBtn, 0, 0); layout->addWidget(videoBox, 0, 1, 1, 3); layout->addWidget(screenBtn, 1, 0); layout->addWidget(screenBox, 1, 1, 1, 3); layout->addWidget(windowBtn, 2, 0); layout->addWidget(windowBox, 2, 1, 1, 3); layout->addWidget(audioLabel, 3, 0); layout->addWidget(audioBox, 3, 1, 1, 3); layout->addWidget(cancelBtn, 4, 2); layout->addWidget(okBtn, 4, 3); layout->setColumnStretch(1, 1); dialog->setLayout(layout); dialog->exec(); if (dialog->result() == QDialog::Accepted) { int audioInputDeviceIndex = audioBox->currentIndex() - 2; if (videoBtn->isChecked()) { int videoInputDeviceIndex = videoBox->currentIndex() - 1; Bino::instance()->startCaptureModeCamera(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), videoInputDeviceIndex >= 0 ? videoInputDevices[videoInputDeviceIndex] : QMediaDevices::defaultVideoInput()); } else if (screenBtn->isChecked()) { int screenInputDeviceIndex = screenBox->currentIndex(); Bino::instance()->startCaptureModeScreen(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), screenInputDevices[screenInputDeviceIndex]); } else if (windowBtn->isChecked()) { int windowInputDeviceIndex = windowBox->currentIndex(); Bino::instance()->startCaptureModeWindow(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), windowInputDevices[windowInputDeviceIndex]); } } } void Gui::fileQuit() { close(); } void Gui::trackVideo() { QAction* a = _trackVideoActionGroup->checkedAction(); if (a) Bino::instance()->setVideoTrack(a->data().toInt()); } void Gui::trackAudio() { QAction* a = _trackAudioActionGroup->checkedAction(); if (a) Bino::instance()->setAudioTrack(a->data().toInt()); } void Gui::trackSubtitle() { QAction* a = _trackSubtitleActionGroup->checkedAction(); if (a) Bino::instance()->setSubtitleTrack(a->data().toInt()); } void Gui::playlistLoad() { QString name = QFileDialog::getOpenFileName(this, QString(), QString(), tr("Playlists (*.m3u)")); if (!name.isEmpty()) { QString errStr; if (!Playlist::instance()->load(name, errStr)) { QMessageBox::critical(this, tr("Error"), errStr); } } } void Gui::playlistSave() { QFileDialog fileDialog(this, QString(), QString(), tr("Playlists (*.m3u)")); fileDialog.setDefaultSuffix(".m3u"); fileDialog.setAcceptMode(QFileDialog::AcceptSave); if (fileDialog.exec()) { QString name = fileDialog.selectedFiles().front(); QString errStr; if (!Playlist::instance()->save(name, errStr)) { QMessageBox::critical(this, tr("Error"), errStr); } } } void Gui::playlistEdit() { PlaylistEditor editor(this); editor.exec(); } void Gui::playlistNext() { Playlist::instance()->next(); } void Gui::playlistPrevious() { Playlist::instance()->prev(); } void Gui::playlistLoop() { QAction* a = _playlistLoopActionGroup->checkedAction(); if (a) Playlist::instance()->setLoopMode(static_cast(a->data().toInt())); } void Gui::playlistWait() { QAction* a = _playlistWaitActionGroup->checkedAction(); if (a) Playlist::instance()->setWaitMode(static_cast(a->data().toInt())); } void Gui::threeDSurround() { QAction* a = _3dSurroundActionGroup->checkedAction(); if (a) { Bino::instance()->setSurroundMode(static_cast(a->data().toInt())); _widget->update(); } } void Gui::threeDInput() { QAction* a = _3dInputActionGroup->checkedAction(); if (a) { Bino::instance()->setInputMode(static_cast(a->data().toInt())); _widget->update(); } } void Gui::threeDOutput() { QAction* a = _3dOutputActionGroup->checkedAction(); if (a) { _widget->setOutputMode(static_cast(a->data().toInt())); _widget->update(); } } void Gui::mediaTogglePause() { Bino::instance()->togglePause(); } void Gui::mediaToggleVolumeMute() { Bino::instance()->toggleMute(); } void Gui::mediaVolumeInc() { Bino::instance()->changeVolume(+0.05f); } void Gui::mediaVolumeDec() { Bino::instance()->changeVolume(-0.05f); } void Gui::mediaSeekFwd1Sec() { Bino::instance()->seek(+1000); } void Gui::mediaSeekBwd1Sec() { Bino::instance()->seek(-1000); } void Gui::mediaSeekFwd10Secs() { Bino::instance()->seek(+10000); } void Gui::mediaSeekBwd10Secs() { Bino::instance()->seek(-10000); } void Gui::mediaSeekFwd1Min() { Bino::instance()->seek(+60000); } void Gui::mediaSeekBwd1Min() { Bino::instance()->seek(-60000); } void Gui::mediaSeekFwd10Mins() { Bino::instance()->seek(+600000); } void Gui::mediaSeekBwd10Mins() { Bino::instance()->seek(-600000); } void Gui::viewToggleFullscreen() { if (windowState() & Qt::WindowFullScreen) { showNormal(); menuBar()->show(); activateWindow(); } else { menuBar()->hide(); showFullScreen(); activateWindow(); } } void Gui::viewToggleSwapEyes() { Bino::instance()->toggleSwapEyes(); _widget->update(); } void Gui::viewResetSurround() { _widget->resetSurroundView(); _widget->update(); } void Gui::helpAbout() { QMessageBox::about(this, tr("About Bino"), QString("

") + tr("Bino version %1").arg(BINO_VERSION) + QString("
") + QString("https://bino3d.org") + QString("

") + tr("Copyright (C) %1 Martin Lambers").arg(2023) + QString("
") + tr("This is free software. You may redistribute copies of it " "under the terms of the " "GNU General Public License. " "There is NO WARRANTY, to the extent permitted by law.") + QString("

")); } void Gui::updateActions() { LOG_DEBUG("updating Gui menu state"); _viewToggleSwapEyesAction->setChecked(Bino::instance()->swapEyes()); _mediaTogglePauseAction->setChecked(Bino::instance()->paused()); _mediaToggleVolumeMuteAction->setChecked(Bino::instance()->muted()); _trackMenu->clear(); QUrl url = Bino::instance()->url(); MetaData metaData; if (!url.isEmpty() && metaData.detectCached(url)) { for (int i = 0; i < metaData.videoTracks.size(); i++) { QString s = QString(tr("Video track %1")).arg(i + 1); QLocale::Language l = static_cast(metaData.videoTracks[i].value(QMediaMetaData::Language).toInt()); if (l != QLocale::AnyLanguage) s += QString(tr(" (%1)")).arg(QLocale::languageToString(l)); QAction* a = new QAction(s, this); a->setCheckable(true); _trackVideoActionGroup->addAction(a)->setData(i); connect(a, SIGNAL(triggered()), this, SLOT(trackVideo())); addBinoAction(a, _trackMenu); a->setChecked(Bino::instance()->videoTrack() == i); } if (metaData.videoTracks.size() > 0) _trackMenu->addSeparator(); for (int i = 0; i < metaData.audioTracks.size(); i++) { QString s = QString(tr("Audio track %1")).arg(i + 1); QLocale::Language l = static_cast(metaData.audioTracks[i].value(QMediaMetaData::Language).toInt()); if (l != QLocale::AnyLanguage) s += QString(tr(" (%1)")).arg(QLocale::languageToString(l)); QAction* a = new QAction(s, this); a->setCheckable(true); _trackAudioActionGroup->addAction(a)->setData(i); connect(a, SIGNAL(triggered()), this, SLOT(trackAudio())); addBinoAction(a, _trackMenu); a->setChecked(Bino::instance()->audioTrack() == i); } if (metaData.subtitleTracks.size() > 0) { if (metaData.audioTracks.size() > 0 || metaData.videoTracks.size() > 0) _trackMenu->addSeparator(); QAction* a = new QAction(tr("No subtitles"), this); a->setCheckable(true); _trackSubtitleActionGroup->addAction(a)->setData(-1); connect(a, SIGNAL(triggered()), this, SLOT(trackSubtitle())); addBinoAction(a, _trackMenu); a->setChecked(Bino::instance()->subtitleTrack() < 0); for (int i = 0; i < metaData.subtitleTracks.size(); i++) { QString s = QString(tr("Subtitle track %1")).arg(i + 1); QLocale::Language l = static_cast(metaData.subtitleTracks[i].value(QMediaMetaData::Language).toInt()); if (l != QLocale::AnyLanguage) s += QString(" (%1)").arg(QLocale::languageToString(l)); QAction* a = new QAction(s, this); a->setCheckable(true); _trackSubtitleActionGroup->addAction(a)->setData(i); connect(a, SIGNAL(triggered()), this, SLOT(trackSubtitle())); addBinoAction(a, _trackMenu); a->setChecked(Bino::instance()->subtitleTrack() == i); } } } else { QAction* a = new QAction(tr("None"), this); a->setEnabled(false); addBinoAction(a, _trackMenu); } _trackVideoActionGroup->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _trackAudioActionGroup->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _trackSubtitleActionGroup->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); LoopMode loopMode = Playlist::instance()->loopMode(); for (int i = 0; i < _playlistLoopActionGroup->actions().size(); i++) { QAction* a = _playlistLoopActionGroup->actions()[i]; a->setChecked(a->data().toInt() == int(loopMode)); } WaitMode waitMode = Playlist::instance()->waitMode(); for (int i = 0; i < _playlistWaitActionGroup->actions().size(); i++) { QAction* a = _playlistWaitActionGroup->actions()[i]; a->setChecked(a->data().toInt() == int(waitMode)); } SurroundMode surroundMode = Bino::instance()->assumeSurroundMode(); _viewResetSurroundAction->setEnabled(surroundMode != Surround_Off); for (int i = 0; i < _3dSurroundActionGroup->actions().size(); i++) { QAction* a = _3dSurroundActionGroup->actions()[i]; a->setChecked(a->data().toInt() == int(surroundMode)); } InputMode inputMode = Bino::instance()->assumeInputMode(); for (int i = 0; i < _3dInputActionGroup->actions().size(); i++) { QAction* a = _3dInputActionGroup->actions()[i]; a->setChecked(a->data().toInt() == int(inputMode)); } for (int i = 0; i < _3dOutputActionGroup->actions().size(); i++) { QAction* a = _3dOutputActionGroup->actions()[i]; if (Bino::instance()->assumeStereoInputMode()) { a->setEnabled(true); a->setChecked(a->data().toInt() == int(_widget->outputMode())); OutputMode outputMode = static_cast(a->data().toInt()); if (outputMode == Output_OpenGL_Stereo) a->setEnabled(_widget->isOpenGLStereo()); } else { a->setEnabled(false); a->setChecked(false); } } _mediaTogglePauseAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekFwd1SecAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekBwd1SecAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekFwd10SecsAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekBwd10SecsAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekFwd1MinAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekBwd1MinAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekFwd10MinsAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _mediaSeekBwd10MinsAction->setEnabled(Bino::instance()->playlistMode() && !Bino::instance()->stopped()); _widget->update(); } void Gui::setOutputMode(OutputMode mode) { _widget->setOutputMode(mode); _widget->update(); } void Gui::setSurroundVerticalFieldOfView(float vfov) { _widget->setSurroundVerticalFieldOfView(vfov); _widget->update(); } void Gui::setFullscreen(bool f) { if (f && !(windowState() & Qt::WindowFullScreen)) { viewToggleFullscreen(); } else if (!f && (windowState() & Qt::WindowFullScreen)) { viewToggleFullscreen(); } } void Gui::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); } void Gui::dropEvent(QDropEvent* event) { if (event->mimeData()->hasUrls() && event->mimeData()->urls().size() > 0) { QUrl url = event->mimeData()->urls()[0]; MetaData metaData; QString errMsg; if (metaData.detectCached(url, &errMsg)) { Bino::instance()->startPlaylistMode(); Playlist::instance()->clear(); Playlist::instance()->append(url); Playlist::instance()->start(); } else { QMessageBox::critical(this, tr("Error"), errMsg); } event->acceptProposedAction(); } } #ifndef QT_NO_CONTEXTMENU void Gui::contextMenuEvent(QContextMenuEvent* event) { _contextMenu->exec(event->globalPos()); } #endif // QT_NO_CONTEXTMENU void Gui::moveEvent(QMoveEvent*) { if (_widget->outputMode() == Output_Even_Odd_Rows || _widget->outputMode() == Output_Even_Odd_Columns || _widget->outputMode() == Output_Checkerboard) { _widget->update(); } } bino-2.5/src/gui.hpp000066400000000000000000000066111475415313200144040ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #pragma once #include #include "modes.hpp" #include "widget.hpp" class Gui : public QMainWindow { Q_OBJECT private: Widget* _widget; QMenu* _contextMenu; QActionGroup* _playlistLoopActionGroup; QActionGroup* _playlistWaitActionGroup; QMenu* _trackMenu; QActionGroup* _trackVideoActionGroup; QActionGroup* _trackAudioActionGroup; QActionGroup* _trackSubtitleActionGroup; QActionGroup* _3dSurroundActionGroup; QActionGroup* _3dInputActionGroup; QActionGroup* _3dOutputActionGroup; QAction* _mediaTogglePauseAction; QAction* _mediaToggleVolumeMuteAction; QAction* _mediaVolumeIncAction; QAction* _mediaVolumeDecAction; QAction* _mediaSeekFwd1SecAction; QAction* _mediaSeekBwd1SecAction; QAction* _mediaSeekFwd10SecsAction; QAction* _mediaSeekBwd10SecsAction; QAction* _mediaSeekFwd1MinAction; QAction* _mediaSeekBwd1MinAction; QAction* _mediaSeekFwd10MinsAction; QAction* _mediaSeekBwd10MinsAction; QAction* _viewToggleFullscreenAction; QAction* _viewToggleSwapEyesAction; QAction* _viewResetSurroundAction; QMenu* addBinoMenu(const QString& title); void addBinoAction(QAction* action, QMenu* menu); public slots: void fileOpen(); void fileOpenURL(); void fileOpenCamera(); void fileQuit(); void trackVideo(); void trackAudio(); void trackSubtitle(); void playlistLoad(); void playlistSave(); void playlistEdit(); void playlistNext(); void playlistPrevious(); void playlistLoop(); void playlistWait(); void threeDSurround(); void threeDInput(); void threeDOutput(); void mediaTogglePause(); void mediaToggleVolumeMute(); void mediaVolumeInc(); void mediaVolumeDec(); void mediaSeekFwd1Sec(); void mediaSeekBwd1Sec(); void mediaSeekFwd10Secs(); void mediaSeekBwd10Secs(); void mediaSeekFwd1Min(); void mediaSeekBwd1Min(); void mediaSeekFwd10Mins(); void mediaSeekBwd10Mins(); void viewToggleFullscreen(); void viewToggleSwapEyes(); void viewResetSurround(); void helpAbout(); void updateActions(); protected: virtual void dragEnterEvent(QDragEnterEvent*) override; virtual void dropEvent(QDropEvent*) override; #ifndef QT_NO_CONTEXTMENU virtual void contextMenuEvent(QContextMenuEvent* event) override; #endif virtual void moveEvent(QMoveEvent*) override; public: Gui(OutputMode outputMode, float surroundVerticalFOV, bool fullscreen); static Gui* instance(); void setOutputMode(OutputMode mode); void setSurroundVerticalFieldOfView(float vfov); void setFullscreen(bool f); }; bino-2.5/src/log.cpp000066400000000000000000000064011475415313200143710ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2016, 2017 Computer Graphics Group, University of Siegen * Written by Martin Lambers * * Copyright (C) 2022 * Martin Lambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #ifdef ANDROID # include #endif #include "log.hpp" static LogLevel logLevel; static std::string logFile; static FILE* logStream = nullptr; void SetLogLevel(LogLevel l) { logLevel = l; } LogLevel GetLogLevel() { return logLevel; } void SetLogFile(const char* name, bool truncate) { if (!name) { logFile.clear(); logStream = nullptr; } else { if (truncate) std::remove(name); if (!(logStream = std::fopen(name, "a"))) { LOG_WARNING("cannot open log file %s", name); } else { std::setbuf(logStream, nullptr); logFile = name; } } } const char* GetLogFile() { return logFile.empty() ? nullptr : logFile.c_str(); } void Log(LogLevel level, const char* s) { #ifdef ANDROID // On Android, if there is no log file we always use the system log facility // instead of stderr so that all messages are easily available in the Android monitor. if (!logStream) { int androidlogLevel = ( level == Log_Level_Fatal ? ANDROID_LOG_ERROR : level == Log_Level_Warning ? ANDROID_LOG_WARN : level == Log_Level_Info ? ANDROID_LOG_INFO : level == Log_Level_Debug ? ANDROID_LOG_DEBUG : ANDROID_LOG_VERBOSE); __android_log_write(androidlogLevel, "Bino", s); return; } #endif (void)level; // We want to print one complete line with exactly one call to fputs to // line-buffered stderr so that the output of different processes is not // mangled. Therefore we buffer what we want to print. char buf[LOG_BUFSIZE] = "Bino"; int bufIndex = 4; bufIndex += snprintf(buf + bufIndex, LOG_BUFSIZE - bufIndex, ": %s", s); buf[std::min(bufIndex, LOG_BUFSIZE - 2)] = '\n'; bufIndex++; buf[std::min(bufIndex, LOG_BUFSIZE - 1)] = '\0'; std::fputs(buf, logStream ? logStream : stderr); } bino-2.5/src/log.hpp000066400000000000000000000054531475415313200144040ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2016, 2017 Computer Graphics Group, University of Siegen * Written by Martin Lambers * * Copyright (C) 2022 * Martin Lambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* This logging functionality is adapted from QVR */ #pragma once #include typedef enum { /*! Print only fatal errors */ Log_Level_Fatal = 0, /*! Additionally print warnings (default) */ Log_Level_Warning = 1, /*! Additionally print informational messages */ Log_Level_Info = 2, /*! Additionally print debugging information */ Log_Level_Debug = 3, /*! Additionally print super verbose debugging information */ Log_Level_Firehose = 4 } LogLevel; void SetLogLevel(LogLevel l); LogLevel GetLogLevel(); void SetLogFile(const char* name, bool truncate); /* nullptr means stderr */ const char* GetLogFile(); /* nullptr means stderr */ /* Send one line to the log (\n will be appended) */ void Log(LogLevel level, const char* s); #define LOG_BUFSIZE 1024 #define LOG_MSG(level, ...) { char buf[LOG_BUFSIZE]; snprintf(buf, LOG_BUFSIZE, __VA_ARGS__); Log(level, buf); } #define LOG_REQUESTED(...) { LOG_MSG(Log_Level_Info, __VA_ARGS__); } #define LOG_FATAL(...) { LOG_MSG(Log_Level_Fatal, __VA_ARGS__); } #define LOG_WARNING(...) { if (GetLogLevel() >= Log_Level_Warning) { LOG_MSG(Log_Level_Warning, __VA_ARGS__); } } #define LOG_INFO(...) { if (GetLogLevel() >= Log_Level_Info) { LOG_MSG(Log_Level_Info, __VA_ARGS__); } } #define LOG_DEBUG(...) { if (GetLogLevel() >= Log_Level_Debug) { LOG_MSG(Log_Level_Debug, __VA_ARGS__); } } #define LOG_FIREHOSE(...) { if (GetLogLevel() >= Log_Level_Firehose) { LOG_MSG(Log_Level_Firehose, __VA_ARGS__); } } bino-2.5/src/main.cpp000066400000000000000000000751761475415313200145530ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_QVR # include #endif #include "version.hpp" #include "log.hpp" #include "videosink.hpp" #include "playlist.hpp" #include "metadata.hpp" #include "screen.hpp" #include "qvrapp.hpp" #include "gui.hpp" #include "commandinterpreter.hpp" #include "modes.hpp" #include "tools.hpp" #include "bino.hpp" void logQtMsg(QtMsgType type, const QMessageLogContext&, const QString& msg) { switch (type) { case QtDebugMsg: LOG_DEBUG("Qt debug: %s", qPrintable(msg)); break; case QtInfoMsg: LOG_INFO("Qt info: %s", qPrintable(msg)); break; case QtWarningMsg: LOG_WARNING("Qt warning: %s", qPrintable(msg)); break; case QtCriticalMsg: LOG_FATAL("Qt critical: %s", qPrintable(msg)); break; case QtFatalMsg: LOG_FATAL("Qt fatal: %s", qPrintable(msg)); break; } } int main(int argc, char* argv[]) { // Early check before command line options are consumed: is this process a QVR child process? bool vrChildProcess = false; #ifdef WITH_QVR for (int i = 1; i < argc; i++) { if (QString(argv[i]).startsWith("--qvr-process")) { vrChildProcess = true; break; } } #endif // Initialize Qt qInstallMessageHandler(logQtMsg); QApplication app(argc, argv); QApplication::setApplicationName("Bino"); QApplication::setApplicationVersion(BINO_VERSION); #ifdef WITH_QVR QVRManager manager(argc, argv); #endif QTranslator qtTranslator; if (qtTranslator.load(QLocale(), "qt", "_", QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { app.installTranslator(&qtTranslator); } QTranslator binoTranslator; if (binoTranslator.load(QLocale(), "bino", "_", ":/i18n")) { app.installTranslator(&binoTranslator); } // Process command line QCommandLineParser parser; parser.setApplicationDescription(QCommandLineParser::tr("3D video player -- see https://bino3d.org")); parser.addPositionalArgument("[URL...]", QCommandLineParser::tr("Media to play.")); parser.addHelpOption(); parser.addVersionOption(); parser.addOption({ "log-level", QCommandLineParser::tr("Set log level (%1).").arg("fatal, warning, info, debug, firehose"), "level" }); parser.addOption({ "log-file", QCommandLineParser::tr("Set log file."), "file" }); parser.addOption({ "read-commands", QCommandLineParser::tr("Read commands from a script file."), "script" }); parser.addOption({ "opengles", QCommandLineParser::tr("Use OpenGL ES instead of Desktop OpenGL.") }); parser.addOption({ "stereo", QCommandLineParser::tr("Use OpenGL quad-buffered stereo in GUI mode.")}); parser.addOption({ "vr", QCommandLineParser::tr("Start in VR mode instead of GUI mode.")}); parser.addOption({ "vr-screen", QCommandLineParser::tr("Set VR screen geometry, either as the special values 'united' or 'intersected', or " "as a comma-separated list of nine values representing three 3D coordinates that define a planar screen (bottom left, bottom right, top left), " "or as a an aspect ratio followed by the name of an OBJ file that contains the screen geometry with texture coordinates " "(example: '16:9,myscreen.obj')."), "screen" }); parser.addOption({ "capture", QCommandLineParser::tr("Capture audio/video input from microphone and camera/screen/window.") }); parser.addOption({ "list-audio-outputs", QCommandLineParser::tr("List audio outputs.") }); parser.addOption({ "list-audio-inputs", QCommandLineParser::tr("List audio inputs.") }); parser.addOption({ "list-video-inputs", QCommandLineParser::tr("List video inputs.") }); parser.addOption({ "list-screen-inputs", QCommandLineParser::tr("List screen inputs.") }); parser.addOption({ "list-window-inputs", QCommandLineParser::tr("List window inputs.") }); parser.addOption({ "audio-output", QCommandLineParser::tr("Choose audio output via its index."), "ao" }); parser.addOption({ "audio-input", QCommandLineParser::tr("Choose audio input via its index. Can be empty."), "ai" }); parser.addOption({ "video-input", QCommandLineParser::tr("Choose video input via its index."), "vi" }); parser.addOption({ "screen-input", QCommandLineParser::tr("Choose screen input via its index."), "si" }); parser.addOption({ "window-input", QCommandLineParser::tr("Choose window input via its index."), "si" }); parser.addOption({ "list-tracks", QCommandLineParser::tr("List all video, audio and subtitle tracks in the media.") }); parser.addOption({ "preferred-audio", QCommandLineParser::tr("Set preferred audio track language (en, de, fr, ...)."), "lang" }); parser.addOption({ "preferred-subtitle", QCommandLineParser::tr("Set preferred subtitle track language (en, de, fr, ...). Can be empty."), "lang" }); parser.addOption({ "video-track", QCommandLineParser::tr("Choose video track via its index."), "track" }); parser.addOption({ "audio-track", QCommandLineParser::tr("Choose audio track via its index."), "track" }); parser.addOption({ "subtitle-track", QCommandLineParser::tr("Choose subtitle track via its index. Can be empty."), "track" }); parser.addOption({ { "p", "playlist" }, QCommandLineParser::tr("Load playlist."), "file" }); parser.addOption({ { "l", "loop" }, QCommandLineParser::tr("Set loop mode (%1).").arg("off, one, all"), "mode" }); parser.addOption({ { "w", "wait" }, QCommandLineParser::tr("Set wait mode (%1).").arg("off, on"), "mode" }); parser.addOption({ { "i", "input" }, QCommandLineParser::tr("Set input mode (%1).").arg("mono, " "top-bottom, top-bottom-half, bottom-top, bottom-top-half, " "left-right, left-right-half, right-left, right-left-half, " "alternating-left-right, alternating-right-left"), "mode" }); parser.addOption({ { "o", "output" }, QCommandLineParser::tr("Set output mode (%1).").arg("left, right, stereo, alternating, " "hdmi-frame-pack, " "left-right, left-right-half, right-left, right-left-half, " "top-bottom, top-bottom-half, bottom-top, bottom-top-half, " "even-odd-rows, even-odd-columns, checkerboard, " "red-cyan-dubois, red-cyan-full-color, red-cyan-half-color, red-cyan-monochrome, " "green-magenta-dubois, green-magenta-full-color, green-magenta-half-color, green-magenta-monochrome, " "amber-blue-dubois, amber-blue-full-color, amber-blue-half-color, amber-blue-monochrome, " "red-green-monochrome, red-blue-monochrome"), "mode" }); parser.addOption({ "surround", QCommandLineParser::tr("Set surround mode (%1).").arg("360, 180, off"), "mode" }); parser.addOption({ "surround-vfov", QCommandLineParser::tr("Set surround vertical field of view (default 50, range 5-115)."), "degrees" }); parser.addOption({ { "S", "swap-eyes" }, QCommandLineParser::tr("Swap left/right eye.") }); parser.addOption({ { "f", "fullscreen" }, QCommandLineParser::tr("Start in fullscreen mode.") }); parser.process(app); // Initialize logging SetLogLevel(Log_Level_Warning); if (parser.isSet("log-file")) { SetLogFile(qPrintable(parser.value("log-file")), true); } if (parser.isSet("log-level")) { if (parser.value("log-level") == "fatal") SetLogLevel(Log_Level_Fatal); else if (parser.value("log-level") == "warning") SetLogLevel(Log_Level_Warning); else if (parser.value("log-level") == "info") SetLogLevel(Log_Level_Info); else if (parser.value("log-level") == "debug") SetLogLevel(Log_Level_Debug); else if (parser.value("log-level") == "firehose") SetLogLevel(Log_Level_Firehose); else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--log-level"))); return 1; } } // Check if VR mode is available if requested #ifndef WITH_QVR if (parser.isSet("vr")) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("VR mode unavailable - recompile Bino with QVR support!"))); return 1; } #endif // Set modes SurroundMode surroundMode = Surround_Unknown; if (parser.isSet("surround")) { bool ok; surroundMode = surroundModeFromString(parser.value("surround"), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--surround"))); return 1; } } float surroundVerticalFOV = 50.0f; if (parser.isSet("surround-vfov")) { bool ok; surroundVerticalFOV = parser.value("surround-vfov").toFloat(&ok); if (!ok || surroundVerticalFOV < 5.0f || surroundVerticalFOV > 115.0f) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--surround-vfov"))); return 1; } } InputMode inputMode = Input_Unknown; if (parser.isSet("input")) { bool ok; inputMode = inputModeFromString(parser.value("input"), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--input"))); return 1; } } OutputMode outputMode = Output_Red_Cyan_Dubois; if (parser.isSet("output")) { bool ok; outputMode = outputModeFromString(parser.value("output"), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--output"))); return 1; } } // Lists of available devices. Initialize these lists only when necessary because // this can take some time! QList audioOutputDevices; QList audioInputDevices; QList videoInputDevices; QList screenInputDevices; QList windowInputDevices; // List devices bool deviceListRequested = false; if (parser.isSet("list-audio-outputs")) { audioOutputDevices = QMediaDevices::audioOutputs(); if (audioOutputDevices.size() == 0) { LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("No audio outputs available."))); } else { for (qsizetype i = 0; i < audioOutputDevices.size(); i++) LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("Audio output %1: %2").arg(i).arg(audioOutputDevices[i].description()))); } deviceListRequested = true; } if (parser.isSet("list-audio-inputs")) { audioInputDevices = QMediaDevices::audioInputs(); if (audioInputDevices.size() == 0) { LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("No audio inputs available."))); } else { for (qsizetype i = 0; i < audioInputDevices.size(); i++) LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("Audio input %1: %2").arg(i).arg(audioInputDevices[i].description()))); } deviceListRequested = true; } if (parser.isSet("list-video-inputs")) { videoInputDevices = QMediaDevices::videoInputs(); if (videoInputDevices.size() == 0) { LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("No video inputs available."))); } else { for (qsizetype i = 0; i < videoInputDevices.size(); i++) LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("Video input %1: %2").arg(i).arg(videoInputDevices[i].description()))); } deviceListRequested = true; } if (parser.isSet("list-screen-inputs")) { screenInputDevices = QGuiApplication::screens(); if (screenInputDevices.size() == 0) { LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("No screen inputs available."))); } else { for (qsizetype i = 0; i < screenInputDevices.size(); i++) LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("Screen input %1: %2").arg(i).arg(screenInputDevices[i]->name()))); } deviceListRequested = true; } if (parser.isSet("list-window-inputs")) { windowInputDevices = QWindowCapture::capturableWindows(); if (windowInputDevices.size() == 0) { LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("No window inputs available."))); } else { for (qsizetype i = 0; i < windowInputDevices.size(); i++) LOG_REQUESTED("%s", qPrintable(QCommandLineParser::tr("Window input %1: %2").arg(i).arg(windowInputDevices[i].description()))); } deviceListRequested = true; } if (deviceListRequested) { return 0; } // Get requested device indices; -1 means default int audioOutputDeviceIndex = -1; int audioInputDeviceIndex = -1; int videoInputDeviceIndex = -1; int screenInputDeviceIndex = -1; int windowInputDeviceIndex = -1; if (parser.isSet("audio-output")) { audioOutputDevices = QMediaDevices::audioOutputs(); bool ok; int ao = parser.value("audio-output").toInt(&ok); if (ok && ao >= 0 && ao < audioOutputDevices.size()) { audioOutputDeviceIndex = ao; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--audio-output"))); return 1; } } if (parser.isSet("capture")) { bool ok; if (parser.isSet("audio-input")) { if (parser.value("audio-input").length() == 0) { audioInputDeviceIndex = -2; // this means no audio input } else { if (audioInputDevices.size() == 0) audioInputDevices = QMediaDevices::audioInputs(); int ai = parser.value("audio-input").toInt(&ok); if (ok && ai >= 0 && ai < audioInputDevices.size()) { audioInputDeviceIndex = ai; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--audio-input"))); return 1; } } } if (parser.isSet("video-input")) { if (videoInputDevices.size() == 0) videoInputDevices = QMediaDevices::videoInputs(); int vi = parser.value("video-input").toInt(&ok); if (ok && vi >= 0 && vi < videoInputDevices.size()) { videoInputDeviceIndex = vi; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--video-input"))); return 1; } } if (parser.isSet("screen-input")) { if (screenInputDevices.size() == 0) screenInputDevices = QGuiApplication::screens(); int vi = parser.value("screen-input").toInt(&ok); if (ok && vi >= 0 && vi < screenInputDevices.size()) { screenInputDeviceIndex = vi; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--screen-input"))); return 1; } } if (parser.isSet("window-input")) { if (windowInputDevices.size() == 0) windowInputDevices = QWindowCapture::capturableWindows(); int vi = parser.value("window-input").toInt(&ok); if (ok && vi >= 0 && vi < windowInputDevices.size()) { windowInputDeviceIndex = vi; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--window-input"))); return 1; } } } // Get playlist. Playlist playlist; if (parser.isSet("preferred-audio")) { QLocale::Language lang = QLocale::codeToLanguage(parser.value("preferred-audio")); if (lang == QLocale::AnyLanguage) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--preferred-audio"))); return 1; } else { playlist.setPreferredAudio(lang); } } if (parser.isSet("preferred-subtitle")) { if (parser.value("preferred-subtitle").length() == 0) { playlist.setWantSubtitle(false); } else { QLocale::Language lang = QLocale::codeToLanguage(parser.value("preferred-subtitle")); if (lang == QLocale::AnyLanguage) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--preferred-subtitle"))); return 1; } else { playlist.setWantSubtitle(true); playlist.setPreferredSubtitle(lang); } } } if (parser.isSet("loop")) { bool ok; LoopMode loopMode = loopModeFromString(parser.value("loop"), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--loop"))); return 1; } playlist.setLoopMode(loopMode); } if (parser.isSet("wait")) { bool ok; WaitMode waitMode = waitModeFromString(parser.value("wait"), &ok); if (!ok) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--wait"))); return 1; } playlist.setWaitMode(waitMode); } int videoTrack = PlaylistEntry::DefaultTrack; int audioTrack = PlaylistEntry::DefaultTrack; int subtitleTrack = PlaylistEntry::DefaultTrack; if (parser.isSet("video-track")) { bool ok; int vt = parser.value("video-track").toInt(&ok); if (ok && vt >= 0) { videoTrack = vt; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--video-track"))); return 1; } } if (parser.isSet("audio-track")) { bool ok; int at = parser.value("audio-track").toInt(&ok); if (ok && at >= 0) { audioTrack = at; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--audio-track"))); return 1; } } if (parser.isSet("subtitle-track")) { if (parser.value("subtitle-track").length() == 0) { subtitleTrack = PlaylistEntry::NoTrack; } else { bool ok; int st = parser.value("subtitle-track").toInt(&ok); if (ok && st >= 0) { subtitleTrack = st; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--subtitle-track"))); return 1; } } } for (qsizetype i = 0; i < parser.positionalArguments().length(); i++) { QUrl url = parser.positionalArguments()[i]; if (url.isRelative()) { QFileInfo fileInfo(parser.positionalArguments()[i]); if (fileInfo.exists()) { url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); } else { LOG_WARNING("%s", qPrintable(QCommandLineParser::tr("File does not exist: %1").arg(parser.positionalArguments()[i]))); continue; } } playlist.append(PlaylistEntry(url, inputMode, surroundMode, videoTrack, audioTrack, subtitleTrack)); } if (parser.positionalArguments().length() > 0 && playlist.length() == 0) { return 1; } if (parser.isSet("playlist")) { QString errStr; if (!playlist.load(parser.value("playlist"), errStr)) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("%1: %2") .arg(parser.value("playlist")) .arg(errStr.isEmpty() ? QString("invalid playlist file") : errStr))); return 1; } } if (!parser.isSet("wait")) { playlist.setWaitModeAuto(); } if (parser.isSet("capture") && playlist.length() > 0) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Cannot capture and play URL at the same time."))); return 1; } // List tracks if (parser.isSet("list-tracks")) { MetaData metaData; for (qsizetype i = 0; i < playlist.length(); i++) { if (!metaData.detectCached(playlist.entries()[i].url)) return 1; LOG_REQUESTED("%s", qPrintable(metaData.url.toString())); for (qsizetype l = 0; l < metaData.global.keys().size(); l++) { LOG_REQUESTED(" %s: %s", qPrintable(QMediaMetaData::metaDataKeyToString(metaData.global.keys()[l])), qPrintable(metaData.global.stringValue(metaData.global.keys()[l]))); } for (int k = 0; k < 3; k++) { QString trackType = (k == 0 ? QCommandLineParser::tr("video") : k == 1 ? QCommandLineParser::tr("audio") : QCommandLineParser::tr("subtitle")); const QList& metaDataList = (k == 0 ? metaData.videoTracks : k == 1 ? metaData.audioTracks : metaData.subtitleTracks); if (metaDataList.size() == 0) { LOG_REQUESTED(" %s", qPrintable(QCommandLineParser::tr("no %1 tracks").arg(trackType))); } else { for (qsizetype l = 0; l < metaDataList.size(); l++) { LOG_REQUESTED(" %s", qPrintable(QCommandLineParser::tr("%1 track %2").arg(qPrintable(trackType)).arg(l))); QMediaMetaData lmd = metaDataList[l]; for (qsizetype m = 0; m < lmd.keys().size(); m++) { QMediaMetaData::Key key = lmd.keys()[m]; if (metaData.global.stringValue(key) == lmd.stringValue(key)) continue; LOG_REQUESTED(" %s: %s", qPrintable(QMediaMetaData::metaDataKeyToString(key)), qPrintable(lmd.stringValue(key))); } } } } } return 0; } // Handle the VR Screen Bino::ScreenType screenType = Bino::ScreenGeometry; Screen screen; // default screen for non-VR mode if (parser.isSet("vr")) { float screenCenterHeight = 1.76f - 0.15f; // does not matter, never used #ifdef WITH_QVR screenCenterHeight = QVRObserverConfig::defaultEyeHeight; #endif screen = Screen( // default screen for VR mode QVector3D(-16.0f / 9.0f, -1.0f + screenCenterHeight, -8.0f), QVector3D(+16.0f / 9.0f, -1.0f + screenCenterHeight, -8.0f), QVector3D(-16.0f / 9.0f, +1.0f + screenCenterHeight, -8.0f)); if (parser.isSet("vr-screen")) { QStringList paramList = parser.value("vr-screen").split(','); float values[9]; if (parser.value("vr-screen") == "united") { screenType = Bino::ScreenUnited; } else if (parser.value("vr-screen") == "intersected") { screenType = Bino::ScreenIntersected; } else if (paramList.length() == 9 && 9 == std::sscanf(qPrintable(parser.value("vr-screen")), "%f,%f,%f,%f,%f,%f,%f,%f,%f", values + 0, values + 1, values + 2, values + 3, values + 4, values + 5, values + 6, values + 7, values + 8)) { screenType = Bino::ScreenGeometry; screen = Screen( QVector3D(values[0], values[1], values[2]), QVector3D(values[3], values[4], values[5]), QVector3D(values[6], values[7], values[8])); } else if (paramList.length() == 2) { screenType = Bino::ScreenGeometry; float ar; float ar2[2]; if (2 == std::sscanf(qPrintable(paramList[0]), "%f:%f", ar2 + 0, ar2 + 1)) { ar = ar2[0] / ar2[1]; } else if (1 != std::sscanf(qPrintable(paramList[0]), "%f", &ar)) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--vr-screen"))); return 1; } screen = Screen(paramList[1], "", ar); if (screen.indices.size() == 0) return 1; } else { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Invalid argument for option %1").arg("--vr-screen"))); return 1; } } else { LOG_DEBUG("using default VR screen"); } } // Initialize the command interpreter (but don't start it yet) CommandInterpreter cmdInterpreter; if (parser.isSet("read-commands") && !cmdInterpreter.init(parser.value("read-commands"))) { return 1; } // Determine VR or GUI mode bool vrMainProcess = parser.isSet("vr"); bool vrMode = (vrMainProcess || vrChildProcess); bool guiMode = !vrMode; // Set the OpenGL context parameters QSurfaceFormat format; format.setRedBufferSize(10); format.setGreenBufferSize(10); format.setBlueBufferSize(10); format.setAlphaBufferSize(0); format.setStencilBufferSize(0); bool wantOpenGLES = parser.isSet("opengles"); #if defined Q_PROCESSOR_ARM wantOpenGLES = true; #endif if (wantOpenGLES) format.setRenderableType(QSurfaceFormat::OpenGLES); initializeIsOpenGLES(format); if (IsOpenGLES) { format.setVersion(3, 1); } else { format.setProfile(QSurfaceFormat::CoreProfile); format.setVersion(3, 3); } if (guiMode && (parser.isSet("stereo") || parser.value("output") == "stereo")) { // The user has to explicitly request stereo mode, we cannot simply // try it and fall back to normal mode if it is not available: // Somehow Qt messes up something in the OpenGL context or widget setup // when stereo was requested but is not available. This was a problem at // least from Qt 5.7-5.9 (see comments in Bino 1.x src/video_output_qt.cpp), // and now again with Qt 6.3. // So now we only try to use it when explicitly requested, and we immediately // quit when we don't get it (see Bino::initializeGL). format.setStereo(true); if (!parser.isSet("output")) outputMode = Output_OpenGL_Stereo; } QSurfaceFormat::setDefaultFormat(format); // Initialize Bino (in VR mode: only from the main process!) Bino bino(screenType, screen, parser.isSet("swap-eyes")); if (guiMode || !vrChildProcess) { bino.initializeOutput(audioOutputDeviceIndex >= 0 ? audioOutputDevices[audioOutputDeviceIndex] : QMediaDevices::defaultAudioOutput()); if (parser.isSet("capture")) { if (screenInputDeviceIndex < 0 && windowInputDeviceIndex < 0) { bino.startCaptureModeCamera(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), videoInputDeviceIndex >= 0 ? videoInputDevices[videoInputDeviceIndex] : QMediaDevices::defaultVideoInput()); } else if (screenInputDeviceIndex >= 0) { bino.startCaptureModeScreen(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), screenInputDevices[screenInputDeviceIndex]); } else { bino.startCaptureModeWindow(audioInputDeviceIndex >= -1, audioInputDeviceIndex >= 0 ? audioInputDevices[audioInputDeviceIndex] : QMediaDevices::defaultAudioInput(), windowInputDevices[windowInputDeviceIndex]); } } else { bino.startPlaylistMode(); } } // Start VR or GUI mode if (vrMode) { #ifdef WITH_QVR BinoQVRApp qvrapp; if (!manager.init(&qvrapp)) { LOG_FATAL("%s", qPrintable(QCommandLineParser::tr("Cannot initialize QVR manager"))); return 1; } playlist.start(); cmdInterpreter.start(); return app.exec(); #else return 1; #endif } else { Gui gui(outputMode, surroundVerticalFOV, parser.isSet("fullscreen")); gui.show(); // wait for several seconds to process all events before starting // the playlist, because otherwise playing might be finished before // the first frame rendering, e.g. if you just want to "play" an image QGuiApplication::processEvents(QEventLoop::AllEvents, 3000); playlist.start(); cmdInterpreter.start(); return app.exec(); } } bino-2.5/src/metadata.cpp000066400000000000000000000252761475415313200154030ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include #include #include #include #include #include #include "metadata.hpp" #include "tools.hpp" #include "log.hpp" MetaData::MetaData() { } static int haveFFprobe = -1; static QMap cache; bool MetaData::detectCached(const QUrl& url, QString* errMsg) { /* Try to find the url in the cache */ *this = cache[url]; if (!this->url.isEmpty()) return true; /* Detection via file name hints - this sets default modes that can be overridden per stream */ InputMode defaultInputMode = Input_Unknown; SurroundMode defaultSurroundMode = Surround_Unknown; QString fileName = url.fileName(); QString extension = getExtension(fileName); if (extension == "jps" || extension == "pns") { defaultInputMode = Input_Right_Left; } else if (extension == "mpo") { defaultInputMode = Input_Top_Bottom; // this was converted; see digestiblemedia } else { /* Try to guess the input mode from a marker contained in the file name. * This should be compatible to the Bino 1.x naming conventions. */ QString marker = fileName.left(fileName.lastIndexOf('.')); marker = marker.right(marker.length() - marker.lastIndexOf('-') - 1); marker = marker.toLower(); if (marker == "lr") defaultInputMode = Input_Left_Right; else if (marker == "rl") defaultInputMode = Input_Right_Left; else if (marker == "lrh" || marker == "lrq") defaultInputMode = Input_Left_Right_Half; else if (marker == "rlh" || marker == "rlq") defaultInputMode = Input_Right_Left_Half; else if (marker == "tb" || marker == "ab") defaultInputMode = Input_Top_Bottom; else if (marker == "bt" || marker == "ba") defaultInputMode = Input_Bottom_Top; else if (marker == "tbh" || marker == "abq") defaultInputMode = Input_Top_Bottom_Half; else if (marker == "bth" || marker == "baq") defaultInputMode = Input_Bottom_Top_Half; else if (marker == "2d") defaultInputMode = Input_Mono; } if (defaultInputMode != Input_Unknown) { LOG_DEBUG("guessing input mode %s from file name %s", inputModeToString(defaultInputMode), qPrintable(fileName)); } int i360 = fileName.indexOf("360"); if (i360 >= 0 && (i360 == 0 || !fileName[i360 - 1].isLetterOrNumber()) && !fileName[i360 + 3].isLetterOrNumber()) { defaultSurroundMode = Surround_360; } else { int i180 = fileName.indexOf("180"); if (i180 >= 0 && (i180 == 0 || !fileName[i180 - 1].isLetterOrNumber()) && !fileName[i180 + 3].isLetterOrNumber()) defaultSurroundMode = Surround_180; } if (defaultSurroundMode != Surround_Unknown) { LOG_DEBUG("guessing surround mode %s from file name %s", surroundModeToString(defaultSurroundMode), qPrintable(fileName)); } /* Detection via QMediaPlayer */ QMediaPlayer player; bool failure = false; bool available = false; bool didFFprobe = false; QString errorMessage; player.connect(&player, &QMediaPlayer::errorOccurred, [&](QMediaPlayer::Error, const QString& errorString) { errorMessage = errorString; LOG_WARNING("%s", qPrintable(tr("Cannot get meta data from %1: %2").arg(player.source().toString()).arg(errorString))); failure = true; }); player.connect(&player, &QMediaPlayer::metaDataChanged, [&]() { available = true; }); player.setSource(url); do { // while waiting for the QMediaPlayer meta data to arrive, run ffprobe in parallel if (!didFFprobe) { detectViaFFprobe(url, defaultInputMode, defaultSurroundMode); didFFprobe = true; } QGuiApplication::processEvents(); } while (!failure && !available); if (failure) { if (errMsg) *errMsg = errorMessage; return false; } this->url = url; global = player.metaData(); videoTracks = player.videoTracks(); audioTracks = player.audioTracks(); subtitleTracks = player.subtitleTracks(); inputModes.resize(videoTracks.size(), defaultInputMode); surroundModes.resize(videoTracks.size(), defaultSurroundMode); cache.insert(url, *this); return true; } void MetaData::detectViaFFprobe(const QUrl& url, InputMode& defaultInputMode, SurroundMode& defaultSurroundMode) { /* Detection via ffprobe (if available) */ if (haveFFprobe == -1) { // check once whether we can run ffprobe QProcess prc; prc.startCommand("ffprobe --help"); prc.waitForFinished(1000); haveFFprobe = (prc.exitStatus() == QProcess::NormalExit && prc.exitCode() == 0); LOG_DEBUG("ffprobe available: %d (exit status %d, exit code %d)", haveFFprobe, prc.exitStatus() == QProcess::NormalExit ? 0 : 1, prc.exitCode()); } if (haveFFprobe) { QProcess prc; prc.setProgram("ffprobe"); QStringList args; args.append("-of"); args.append("json"); args.append("-show_format"); args.append("-show_streams"); args.append(url.toString()); prc.setArguments(args); prc.start(); prc.waitForFinished(5000); if (prc.exitStatus() == QProcess::NormalExit && prc.exitCode() == 0) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(prc.readAllStandardOutput(), &err); if (doc.isNull()) { LOG_DEBUG("cannot parse ffprobe json output: %s", qPrintable(err.errorString())); } else { // First check 3dtv.at conventions. These are not interpreted by FFmpeg. // See https://www.3dtv.at/Knowhow/StereoWmvSpec_en.aspx QString layout = doc["format"]["tags"]["StereoscopicLayout"].toString(); QString halfWidth = doc["format"]["tags"]["StereoscopicHalfWidth"].toString(); QString halfHeight = doc["format"]["tags"]["StereoscopicHalfHeight"].toString(); if (layout == "SideBySideRF") defaultInputMode = (halfWidth == "1") ? Input_Right_Left_Half : Input_Right_Left; else if (layout == "SideBySideLF") defaultInputMode = (halfWidth == "1") ? Input_Left_Right_Half : Input_Left_Right; else if (layout == "OverUnderRT") defaultInputMode = (halfHeight == "1") ? Input_Bottom_Top_Half : Input_Bottom_Top; else if (layout == "OverUnderLT") defaultInputMode = (halfHeight == "1") ? Input_Top_Bottom_Half : Input_Top_Bottom; // FFmpeg metadata, per stream, see libavutil/stereo3d.h and libavutil/spherical.h QJsonValue streamArray = doc["streams"]; if (!streamArray.isNull() && !streamArray.isUndefined()) { for (int i = 0; ; i++) { QString codecType = streamArray[i]["codec_type"].toString(); if (codecType.isEmpty()) { break; } else if (codecType == "video") { inputModes.append(defaultInputMode); surroundModes.append(defaultSurroundMode); QJsonValue sideDataArray = streamArray[i]["side_data_list"]; if (!sideDataArray.isNull() && !sideDataArray.isUndefined()) { for (int j = 0; ; j++) { QString type = sideDataArray[j]["side_data_type"].toString(); if (type.isEmpty()) { break; } else if (type == "Stereo 3D") { QString layout = sideDataArray[j]["type"].toString(); QString inverted = sideDataArray[j]["inverted"].toString(); if (layout == "top and bottom") { inputModes.last() = (inverted == "1" ? Input_Bottom_Top : Input_Top_Bottom); } else if (layout == "side by side") { inputModes.last() = (inverted == "1" ? Input_Right_Left : Input_Left_Right); } else if (layout == "frame alternate") { inputModes.last() = (inverted == "1" ? Input_Alternating_RL : Input_Alternating_LR); } else if (layout == "2D") { inputModes.last() = Input_Mono; } } else if (type == "Spherical Mapping") { QString proj = sideDataArray[j]["projection"].toString(); if (proj == "equirectangular") { surroundModes.last() = Surround_360; } else if (proj == "half equirectangular") { surroundModes.last() = Surround_180; } } } } LOG_DEBUG("meta data from ffprobe for video stream %d: input mode %s, surround mode %s", int(inputModes.size() - 1), inputModeToString(inputModes.last()), surroundModeToString(surroundModes.last())); } } } } } else { LOG_DEBUG("cannot run ffprobe: exit status %d, exit code %d", prc.exitStatus() == QProcess::NormalExit ? 0 : 1, prc.exitCode()); } } } bino-2.5/src/metadata.hpp000066400000000000000000000026631475415313200154030ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #pragma once #include #include #include #include #include "modes.hpp" class MetaData { Q_DECLARE_TR_FUNCTIONS(MetaData) private: void detectViaFFprobe(const QUrl& url, InputMode& defaultInputMode, SurroundMode& defaultSurroundMode); public: QUrl url; QMediaMetaData global; QList videoTracks; QList audioTracks; QList subtitleTracks; // additional information for each video track: QList inputModes; QList surroundModes; MetaData(); bool detectCached(const QUrl& url, QString* errMsg = nullptr); }; bino-2.5/src/modes.cpp000066400000000000000000000400251475415313200147170ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #include #include "modes.hpp" const char* inputModeToString(InputMode mode) { switch (mode) { case Input_Unknown: return "unknown"; break; case Input_Mono: return "mono"; break; case Input_Top_Bottom: return "top-bottom"; break; case Input_Top_Bottom_Half: return "top-bottom-half"; break; case Input_Bottom_Top: return "bottom-top"; break; case Input_Bottom_Top_Half: return "bottom-top-half"; break; case Input_Left_Right: return "left-right"; break; case Input_Left_Right_Half: return "left-right-half"; break; case Input_Right_Left: return "right-left"; break; case Input_Right_Left_Half: return "right-left-half"; break; case Input_Alternating_LR: return "alternating-left-right"; break; case Input_Alternating_RL: return "alternating-right-left"; break; }; return nullptr; } QString inputModeToStringUI(InputMode mode) { switch (mode) { case Input_Unknown: return QCoreApplication::translate("Mode", "Input autodetection"); break; case Input_Mono: return QCoreApplication::translate("Mode", "Input 2D"); break; case Input_Top_Bottom: return QCoreApplication::translate("Mode", "Input top/bottom"); break; case Input_Top_Bottom_Half: return QCoreApplication::translate("Mode", "Input top/bottom half height"); break; case Input_Bottom_Top: return QCoreApplication::translate("Mode", "Input bottom/top"); break; case Input_Bottom_Top_Half: return QCoreApplication::translate("Mode", "Input bottom/top half height"); break; case Input_Left_Right: return QCoreApplication::translate("Mode", "Input left/right"); break; case Input_Left_Right_Half: return QCoreApplication::translate("Mode", "Input left/right half width"); break; case Input_Right_Left: return QCoreApplication::translate("Mode", "Input right/left"); break; case Input_Right_Left_Half: return QCoreApplication::translate("Mode", "Input right/left half width"); break; case Input_Alternating_LR: return QCoreApplication::translate("Mode", "Input alternating left/right"); break; case Input_Alternating_RL: return QCoreApplication::translate("Mode", "Input alternating right/left"); break; }; return QString(); } InputMode inputModeFromString(const QString& s, bool* ok) { InputMode mode = Input_Unknown; bool r = true; if (s == "mono") mode = Input_Mono; else if (s == "top-bottom") mode = Input_Top_Bottom; else if (s == "top-bottom-half") mode = Input_Top_Bottom_Half; else if (s == "bottom-top") mode = Input_Bottom_Top; else if (s == "bottom-top-half") mode = Input_Bottom_Top_Half; else if (s == "left-right") mode = Input_Left_Right; else if (s == "left-right-half") mode = Input_Left_Right_Half; else if (s == "right-left") mode = Input_Right_Left; else if (s == "right-left-half") mode = Input_Right_Left_Half; else if (s == "alternating-left-right") mode = Input_Alternating_LR; else if (s == "alternating-right-left") mode = Input_Alternating_RL; else r = false; if (ok) *ok = r; return mode; } const char* surroundModeToString(SurroundMode mode) { switch (mode) { case Surround_Unknown: return "unknown"; break; case Surround_Off: return "off"; break; case Surround_180: return "180"; break; case Surround_360: return "360"; break; } return nullptr; } QString surroundModeToStringUI(SurroundMode mode) { switch (mode) { case Surround_Unknown: return QCoreApplication::translate("Mode", "Surround autodetection"); break; case Surround_Off: return QCoreApplication::translate("Mode", "Surround off"); break; case Surround_180: return QCoreApplication::translate("Mode", "Surround 180°"); break; case Surround_360: return QCoreApplication::translate("Mode", "Surround 360°"); break; } return QString(); } SurroundMode surroundModeFromString(const QString& s, bool* ok) { SurroundMode mode = Surround_Unknown; bool r = true; if (s == "off") mode = Surround_Off; else if (s == "180") mode = Surround_180; else if (s == "360") mode = Surround_360; else r = false; if (ok) *ok = r; return mode; } const char* outputModeToString(OutputMode mode) { switch (mode) { case Output_Left: return "left"; break; case Output_Right: return "right"; break; case Output_OpenGL_Stereo: return "stereo"; break; case Output_Alternating: return "alternating"; break; case Output_HDMI_Frame_Pack: return "hdmi-frame-pack"; break; case Output_Left_Right: return "left-right"; break; case Output_Left_Right_Half: return "left-right-half"; break; case Output_Right_Left: return "right-left"; break; case Output_Right_Left_Half: return "right-left-half"; break; case Output_Top_Bottom: return "top-bottom"; break; case Output_Top_Bottom_Half: return "top-bottom-half"; break; case Output_Bottom_Top: return "bottom-top"; break; case Output_Bottom_Top_Half: return "bottom-top-half"; break; case Output_Even_Odd_Rows: return "even-odd-rows"; break; case Output_Even_Odd_Columns: return "even-odd-columns"; break; case Output_Checkerboard: return "checkeroard"; break; case Output_Red_Cyan_Dubois: return "red-cyan-dubois"; break; case Output_Red_Cyan_FullColor: return "red-cyan-fullcolor"; break; case Output_Red_Cyan_HalfColor: return "red-cyan-halfcolor"; break; case Output_Red_Cyan_Monochrome: return "red-cyan-monochrome"; break; case Output_Green_Magenta_Dubois: return "green-magenta-dubois"; break; case Output_Green_Magenta_FullColor: return "green-magenta-fullcolor"; break; case Output_Green_Magenta_HalfColor: return "green-magenta-halfcolor"; break; case Output_Green_Magenta_Monochrome: return "green-magenta-monochrome"; break; case Output_Amber_Blue_Dubois: return "amber-blue-dubois"; break; case Output_Amber_Blue_FullColor: return "amber-blue-fullcolor"; break; case Output_Amber_Blue_HalfColor: return "amber-blue-halfcolor"; break; case Output_Amber_Blue_Monochrome: return "amber-blue-monochrome"; break; case Output_Red_Green_Monochrome: return "red-green-monochrome"; break; case Output_Red_Blue_Monochrome: return "red-blue-monochrome"; break; } return nullptr; } QString outputModeToStringUI(OutputMode mode) { switch (mode) { case Output_Left: return QCoreApplication::translate("Mode", "Output 2D left"); break; case Output_Right: return QCoreApplication::translate("Mode", "Output 2D right"); break; case Output_OpenGL_Stereo: return QCoreApplication::translate("Mode", "Output 3D OpenGL Stereo"); break; case Output_Alternating: return QCoreApplication::translate("Mode", "Output 3D alternating"); break; case Output_HDMI_Frame_Pack: return QCoreApplication::translate("Mode", "Output HDMI frame pack"); break; case Output_Left_Right: return QCoreApplication::translate("Mode", "Output left/right"); break; case Output_Left_Right_Half: return QCoreApplication::translate("Mode", "Output left/right half width"); break; case Output_Right_Left: return QCoreApplication::translate("Mode", "Output right/left"); break; case Output_Right_Left_Half: return QCoreApplication::translate("Mode", "Output right/left half width"); break; case Output_Top_Bottom: return QCoreApplication::translate("Mode", "Output top/bottom"); break; case Output_Top_Bottom_Half: return QCoreApplication::translate("Mode", "Output top/bottom half height"); break; case Output_Bottom_Top: return QCoreApplication::translate("Mode", "Output bottom/top"); break; case Output_Bottom_Top_Half: return QCoreApplication::translate("Mode", "Output bottom/top half height"); break; case Output_Even_Odd_Rows: return QCoreApplication::translate("Mode", "Output even/odd rows"); break; case Output_Even_Odd_Columns: return QCoreApplication::translate("Mode", "Output even/odd columns"); break; case Output_Checkerboard: return QCoreApplication::translate("Mode", "Output checkerboard"); break; case Output_Red_Cyan_Dubois: return QCoreApplication::translate("Mode", "Output red/cyan high quality"); break; case Output_Red_Cyan_FullColor: return QCoreApplication::translate("Mode", "Output red/cyan full color"); break; case Output_Red_Cyan_HalfColor: return QCoreApplication::translate("Mode", "Output red/cyan half color"); break; case Output_Red_Cyan_Monochrome: return QCoreApplication::translate("Mode", "Output red/cyan monochrome"); break; case Output_Green_Magenta_Dubois: return QCoreApplication::translate("Mode", "Output green/magenta high quality"); break; case Output_Green_Magenta_FullColor: return QCoreApplication::translate("Mode", "Output green/magenta full color"); break; case Output_Green_Magenta_HalfColor: return QCoreApplication::translate("Mode", "Output green/magenta half color"); break; case Output_Green_Magenta_Monochrome: return QCoreApplication::translate("Mode", "Output green/magenta monochrome"); break; case Output_Amber_Blue_Dubois: return QCoreApplication::translate("Mode", "Output amber/blue high quality"); break; case Output_Amber_Blue_FullColor: return QCoreApplication::translate("Mode", "Output amber/blue full color"); break; case Output_Amber_Blue_HalfColor: return QCoreApplication::translate("Mode", "Output amber/blue half color"); break; case Output_Amber_Blue_Monochrome: return QCoreApplication::translate("Mode", "Output amber/blue monochrome"); break; case Output_Red_Green_Monochrome: return QCoreApplication::translate("Mode", "Output red/green monochrome"); break; case Output_Red_Blue_Monochrome: return QCoreApplication::translate("Mode", "Output red/blue monochrome"); break; } return nullptr; } OutputMode outputModeFromString(const QString& s, bool* ok) { OutputMode mode = Output_Left; bool r = true; if (s == "left") mode = Output_Left; else if (s == "right") mode = Output_Right; else if (s == "stereo") mode = Output_OpenGL_Stereo; else if (s == "alternating") mode = Output_Alternating; else if (s == "hmdi-frame-pack") mode = Output_HDMI_Frame_Pack; else if (s == "left-right") mode = Output_Left_Right; else if (s == "left-right-half") mode = Output_Left_Right_Half; else if (s == "right-left") mode = Output_Right_Left; else if (s == "right-left-half") mode = Output_Right_Left_Half; else if (s == "top-bottom") mode = Output_Top_Bottom; else if (s == "top-bottom-half") mode = Output_Top_Bottom_Half; else if (s == "bottom-top") mode = Output_Bottom_Top; else if (s == "bottom-top-half") mode = Output_Bottom_Top_Half; else if (s == "even-odd-rows") mode = Output_Even_Odd_Rows; else if (s == "even-odd-columns") mode = Output_Even_Odd_Columns; else if (s == "checkerboard") mode = Output_Checkerboard; else if (s == "red-cyan-dubois") mode = Output_Red_Cyan_Dubois; else if (s == "red-cyan-fullcolor") mode = Output_Red_Cyan_FullColor; else if (s == "red-cyan-halfcolor") mode = Output_Red_Cyan_HalfColor; else if (s == "red-cyan-monochrome") mode = Output_Red_Cyan_Monochrome; else if (s == "green-magenta-dubois") mode = Output_Green_Magenta_Dubois; else if (s == "green-magenta-fullcolor") mode = Output_Green_Magenta_FullColor; else if (s == "green-magenta-halfcolor") mode = Output_Green_Magenta_HalfColor; else if (s == "green-magenta-monochrome") mode = Output_Green_Magenta_Monochrome; else if (s == "amber-blue-dubois") mode = Output_Amber_Blue_Dubois; else if (s == "amber-blue-fullcolor") mode = Output_Amber_Blue_FullColor; else if (s == "amber-blue-halfcolor") mode = Output_Amber_Blue_HalfColor; else if (s == "amber-blue-monochrome") mode = Output_Amber_Blue_Monochrome; else if (s == "red-green-monochrome") mode = Output_Red_Green_Monochrome; else if (s == "red-blue-monochrome") mode = Output_Red_Blue_Monochrome; else r = false; if (ok) *ok = r; return mode; } const char* loopModeToString(LoopMode mode) { switch (mode) { case Loop_Off: return "off"; break; case Loop_One: return "one"; break; case Loop_All: return "all"; break; } return nullptr; } QString loopModeToStringUI(LoopMode mode) { switch (mode) { case Loop_Off: return QCoreApplication::translate("Mode", "Loop off"); break; case Loop_One: return QCoreApplication::translate("Mode", "Loop one"); break; case Loop_All: return QCoreApplication::translate("Mode", "Loop all"); break; } return QString(); } LoopMode loopModeFromString(const QString& s, bool* ok) { LoopMode mode = Loop_Off; bool r = true; if (s == "off") mode = Loop_Off; else if (s == "one") mode = Loop_One; else if (s == "all") mode = Loop_All; else r = false; if (ok) *ok = r; return mode; } const char* waitModeToString(WaitMode mode) { switch (mode) { case Wait_Off: return "off"; break; case Wait_On: return "on"; break; } return nullptr; } QString waitModeToStringUI(WaitMode mode) { switch (mode) { case Wait_Off: return QCoreApplication::translate("Mode", "Wait off"); break; case Wait_On: return QCoreApplication::translate("Mode", "Wait on"); break; } return QString(); } WaitMode waitModeFromString(const QString& s, bool* ok) { WaitMode mode = Wait_Off; bool r = true; if (s == "off") mode = Wait_Off; else if (s == "on") mode = Wait_On; else r = false; if (ok) *ok = r; return mode; } bino-2.5/src/modes.hpp000066400000000000000000000105521475415313200147260ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #pragma once #include /* Input mode: Is this 2D or 3D, and in the latter case, how are the * left and right view arranged in one frame? */ enum InputMode { Input_Unknown = 0, // unknown; needs to be guessed Input_Mono = 1, // monoscopic video (2D) Input_Top_Bottom = 2, // stereoscopic video, left eye top, right eye bottom Input_Top_Bottom_Half = 3, // stereoscopic video, left eye top, right eye bottom, both half height Input_Bottom_Top = 4, // stereoscopic video, left eye bottom, right eye top Input_Bottom_Top_Half = 5, // stereoscopic video, left eye bottom, right eye top, both half height Input_Left_Right = 6, // stereoscopic video, left eye left, right eye right Input_Left_Right_Half = 7, // stereoscopic video, left eye left, right eye right, both half width Input_Right_Left = 8, // stereoscopic video, left eye right, right eye left Input_Right_Left_Half = 9, // stereoscopic video, left eye right, right eye left, both half width Input_Alternating_LR = 10, // stereoscopic video, alternating frames, left first Input_Alternating_RL = 11, // stereoscopic video, alternating frames, right first }; const char* inputModeToString(InputMode mode); QString inputModeToStringUI(InputMode mode); InputMode inputModeFromString(const QString& s, bool* ok = nullptr); /* Surround mode (180° / 360°) */ enum SurroundMode { Surround_Unknown = 0, // unknown; needs to be guessed Surround_Off = 1, // conventional video Surround_180 = 2, // 180° video Surround_360 = 3, // 360° video }; const char* surroundModeToString(SurroundMode mode); QString surroundModeToStringUI(SurroundMode mode); SurroundMode surroundModeFromString(const QString& s, bool* ok = nullptr); /* Output mode: Is the output 2D or 3D, and in the latter case, how should * the left and right view be arranged on screen? */ // Note that this is mirrored in shader-display-frag.glsl! enum OutputMode { Output_Left = 0, Output_Right = 1, Output_OpenGL_Stereo = 2, Output_Alternating = 3, Output_HDMI_Frame_Pack = 4, Output_Left_Right = 5, Output_Left_Right_Half = 6, Output_Right_Left = 7, Output_Right_Left_Half = 8, Output_Top_Bottom = 9, Output_Top_Bottom_Half = 10, Output_Bottom_Top = 11, Output_Bottom_Top_Half = 12, Output_Even_Odd_Rows = 13, Output_Even_Odd_Columns = 14, Output_Checkerboard = 15, Output_Red_Cyan_Dubois = 16, Output_Red_Cyan_FullColor = 17, Output_Red_Cyan_HalfColor = 18, Output_Red_Cyan_Monochrome = 19, Output_Green_Magenta_Dubois = 20, Output_Green_Magenta_FullColor = 21, Output_Green_Magenta_HalfColor = 22, Output_Green_Magenta_Monochrome = 23, Output_Amber_Blue_Dubois = 24, Output_Amber_Blue_FullColor = 25, Output_Amber_Blue_HalfColor = 26, Output_Amber_Blue_Monochrome = 27, Output_Red_Green_Monochrome = 28, Output_Red_Blue_Monochrome = 29 }; const char* outputModeToString(OutputMode mode); QString outputModeToStringUI(OutputMode mode); OutputMode outputModeFromString(const QString& s, bool* ok = nullptr); /* Loop mode for the playlist */ enum LoopMode { Loop_Off, Loop_One, Loop_All }; const char* loopModeToString(LoopMode mode); QString loopModeToStringUI(LoopMode mode); LoopMode loopModeFromString(const QString& s, bool* ok = nullptr); /* Wait mode for the playlist */ enum WaitMode { Wait_Off, Wait_On }; const char* waitModeToString(WaitMode mode); QString waitModeToStringUI(WaitMode mode); WaitMode waitModeFromString(const QString& s, bool* ok = nullptr); bino-2.5/src/playlist.cpp000066400000000000000000000246361475415313200154630ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #include #include #include #include "playlist.hpp" #include "tools.hpp" #include "log.hpp" PlaylistEntry::PlaylistEntry() : url(), inputMode(Input_Unknown), surroundMode(Surround_Unknown), videoTrack(NoTrack), audioTrack(NoTrack), subtitleTrack(NoTrack) { } PlaylistEntry::PlaylistEntry(const QUrl& url, InputMode inputMode, SurroundMode surroundMode, int videoTrack, int audioTrack, int subtitleTrack) : url(url), inputMode(inputMode), surroundMode(surroundMode), videoTrack(videoTrack), audioTrack(audioTrack), subtitleTrack(subtitleTrack) { } bool PlaylistEntry::noMedia() const { return url.isEmpty(); } QString PlaylistEntry::optionsToString() const { QString s; if (inputMode != Input_Unknown) { s.append(" --input="); s.append(inputModeToString(inputMode)); } if (surroundMode != Surround_Unknown) { s.append(" --surround="); s.append(surroundModeToString(surroundMode)); } if (videoTrack >= 0) { s.append(" --video-track="); s.append(QString::number(videoTrack)); } if (audioTrack >= 0) { s.append(" --audio-track="); s.append(QString::number(audioTrack)); } if (subtitleTrack >= 0) { s.append(" --subtitle-track="); s.append(QString::number(subtitleTrack)); } return s; } bool PlaylistEntry::optionsFromString(const QString& s) { QCommandLineParser parser; parser.addOption({ "input", "", "x" }); parser.addOption({ "surround", "", "x" }); parser.addOption({ "video-track", "", "x" }); parser.addOption({ "audio-track", "", "x" }); parser.addOption({ "subtitle-track", "", "x" }); if (!parser.parse((QString("dummy ") + s).split(' ')) || parser.positionalArguments().length() != 0) { return false; } InputMode inputMode = Input_Unknown; SurroundMode surroundMode = Surround_Unknown; int videoTrack = PlaylistEntry::DefaultTrack; int audioTrack = PlaylistEntry::DefaultTrack; int subtitleTrack = PlaylistEntry::DefaultTrack; bool ok = true; if (parser.isSet("input")) { inputMode = inputModeFromString(parser.value("input"), &ok); } if (parser.isSet("surround")) { surroundMode = surroundModeFromString(parser.value("surround"), &ok); } if (parser.isSet("video-track")) { int t = parser.value("video-track").toInt(&ok); if (ok && t >= 0) videoTrack = t; else ok = false; } if (parser.isSet("audio-track")) { int t = parser.value("audio-track").toInt(&ok); if (ok && t >= 0) audioTrack = t; else ok = false; } if (parser.isSet("subtitle-track") && parser.value("subtitle-track").length() > 0) { int t = parser.value("subtitle-track").toInt(&ok); if (ok && t >= 0) subtitleTrack = t; else ok = false; } if (ok) { *this = PlaylistEntry(this->url, inputMode, surroundMode, videoTrack, audioTrack, subtitleTrack); } return ok; } static Playlist* playlistSingleton = nullptr; Playlist::Playlist() : _preferredAudio(QLocale::system().language()), _preferredSubtitle(QLocale::system().language()), _wantSubtitle(false), _loopMode(Loop_Off), _waitMode(Wait_Off), _currentIndex(-1) { Q_ASSERT(!playlistSingleton); playlistSingleton = this; } Playlist* Playlist::instance() { return playlistSingleton; } QLocale::Language Playlist::preferredAudio() const { return _preferredAudio; } void Playlist::setPreferredAudio(const QLocale::Language& lang) { _preferredAudio = lang; } QLocale::Language Playlist::preferredSubtitle() const { return _preferredSubtitle; } void Playlist::setPreferredSubtitle(const QLocale::Language& lang) { _preferredSubtitle = lang; } bool Playlist::wantSubtitle() const { return _wantSubtitle; } void Playlist::setWantSubtitle(bool want) { _wantSubtitle = want; } QList& Playlist::entries() { return _entries; } const QList& Playlist::entries() const { return _entries; } void Playlist::emitMediaChanged() { if (_currentIndex >= 0 && _currentIndex < length()) { emit mediaChanged(_entries[_currentIndex]); } else { emit mediaChanged(PlaylistEntry()); } } int Playlist::length() const { return _entries.length(); } void Playlist::append(const PlaylistEntry& entry) { _entries.append(entry); } void Playlist::insert(int index, const PlaylistEntry& entry) { _entries.insert(index, entry); } void Playlist::remove(int index) { if (index >= 0 && index < length()) { _entries.remove(index); if (_currentIndex == index) { if (_currentIndex >= length()) _currentIndex--; emitMediaChanged(); } else if (_currentIndex > index) { _currentIndex--; } } } void Playlist::clear() { _entries.clear(); int prevIndex = _currentIndex; _currentIndex = -1; if (prevIndex >= 0) emitMediaChanged(); } void Playlist::start() { if (length() > 0 && _currentIndex < 0) setCurrentIndex(0); } void Playlist::stop() { setCurrentIndex(-1); } void Playlist::next() { if (length() > 0) { switch (loopMode()) { case Loop_Off: if (_currentIndex < length() - 1) setCurrentIndex(_currentIndex + 1); break; case Loop_One: // keep current index but play again emitMediaChanged(); break; case Loop_All: if (_currentIndex == length() - 1) setCurrentIndex(0); else setCurrentIndex(_currentIndex + 1); break; } } } void Playlist::prev() { if (length() > 0) { switch (loopMode()) { case Loop_Off: if (_currentIndex > 0) setCurrentIndex(_currentIndex - 1); break; case Loop_One: // keep current index but play again emitMediaChanged(); break; case Loop_All: if (_currentIndex == 0) setCurrentIndex(length() - 1); else setCurrentIndex(_currentIndex - 1); break; } } } void Playlist::setCurrentIndex(int index) { if (index == _currentIndex) { return; } if (length() == 0 || index < 0) { _currentIndex = -1; emitMediaChanged(); return; } if (index >= length()) { index = length() - 1; } if (_currentIndex != index) { LOG_DEBUG("playlist: switching index from %d to %d", _currentIndex, index); _currentIndex = index; emitMediaChanged(); } } LoopMode Playlist::loopMode() const { return _loopMode; } WaitMode Playlist::waitMode() const { return _waitMode; } void Playlist::setLoopMode(LoopMode loopMode) { _loopMode = loopMode; } void Playlist::setWaitMode(WaitMode waitMode) { _waitMode = waitMode; } void Playlist::setWaitModeAuto() { WaitMode waitMode = Wait_Off; for (int i = 0; i < length(); i++) { QString extension = getExtension(entries()[i].url); if (extension == "jpg" || extension == "jpeg" || extension == "png" || extension == "jps" || extension == "mpo") { waitMode = Wait_On; break; } } _waitMode = waitMode; } void Playlist::mediaEnded() { if (waitMode() == Wait_Off) { next(); } } bool Playlist::save(const QString& fileName, QString& errStr) const { QFile file(fileName); if (!file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate | QIODeviceBase::Text)) { errStr = file.errorString(); return false; } QTextStream out(&file); out << "#EXTM3U\n"; for (int i = 0; i < length(); i++) { out << "#EXTINF:0,\n"; out << "#EXTBINOOPT:" << entries()[i].optionsToString() << "\n"; out << entries()[i].url.toString() << "\n"; } file.flush(); file.close(); if (file.error() != QFileDevice::NoError) { errStr = file.errorString(); return false; } return true; } bool Playlist::load(const QString& fileName, QString& errStr) { QFile file(fileName); if (!file.open(QIODeviceBase::ReadOnly | QIODeviceBase::Text)) { errStr = file.errorString(); return false; } QList entries; PlaylistEntry entry; QTextStream in(&file); int lineIndex = 0; while (!in.atEnd()) { QString line = in.readLine(); lineIndex++; if (line.startsWith("#EXTBINOOPT: ")) { if (!entry.optionsFromString(line.mid(13))) { LOG_DEBUG("%s line %d: ignoring invalid Bino options", qPrintable(fileName), lineIndex); } } else if (line.isEmpty() || line.startsWith("#")) { continue; } else { entry.url = QUrl::fromUserInput(line, QString("."), QUrl::AssumeLocalFile); if (entry.url.isValid() && ( entry.url.scheme() == "file" || entry.url.scheme() == "https" || entry.url.scheme() == "http")) { entries.append(entry); } else { LOG_DEBUG("%s line %d: ignoring invalid URL", qPrintable(fileName), lineIndex); } } } if (file.error() == QFileDevice::NoError) { // overwrite this playlist with new information _entries = entries; _currentIndex = -1; return true; } else { errStr = file.errorString(); return false; } } bino-2.5/src/playlist.hpp000066400000000000000000000055201475415313200154570ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #pragma once #include #include #include "modes.hpp" class PlaylistEntry { public: static const int NoTrack = -2; static const int DefaultTrack = -1; QUrl url; InputMode inputMode; SurroundMode surroundMode; int videoTrack; int audioTrack; int subtitleTrack; PlaylistEntry(); PlaylistEntry(const QUrl& url, InputMode inputMode = Input_Unknown, SurroundMode surroundMode = Surround_Unknown, int videoTrack = DefaultTrack, int audioTrack = DefaultTrack, int subtitleTrack = DefaultTrack); bool noMedia() const; QString optionsToString() const; bool optionsFromString(const QString& s); }; class Playlist : public QObject { Q_OBJECT private: QLocale::Language _preferredAudio; QLocale::Language _preferredSubtitle; bool _wantSubtitle; LoopMode _loopMode; WaitMode _waitMode; QList _entries; int _currentIndex; void emitMediaChanged(); public: Playlist(); static Playlist* instance(); QLocale::Language preferredAudio() const; void setPreferredAudio(const QLocale::Language& lang); QLocale::Language preferredSubtitle() const; void setPreferredSubtitle(const QLocale::Language& lang); bool wantSubtitle() const; void setWantSubtitle(bool want); QList& entries(); const QList& entries() const; int length() const; void append(const PlaylistEntry& entry); void insert(int index, const PlaylistEntry& entry); void remove(int index); void clear(); LoopMode loopMode() const; WaitMode waitMode() const; bool save(const QString& fileName, QString& errStr) const; bool load(const QString& fileName, QString& errStr); public slots: void start(); void stop(); void next(); void prev(); void setCurrentIndex(int index); void setLoopMode(LoopMode loopMode); void setWaitMode(WaitMode waitMode); void setWaitModeAuto(); void mediaEnded(); signals: void mediaChanged(PlaylistEntry entry); }; bino-2.5/src/playlisteditor.cpp000066400000000000000000000330361475415313200166640ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2023 * Martin Lambers * * 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 . */ #include #include #include #include #include #include #include #include #include #include "playlist.hpp" #include "playlisteditor.hpp" #include "modes.hpp" #include "metadata.hpp" PlaylistEntryEditor::PlaylistEntryEditor(const PlaylistEntry& entry, QWidget* parent) : QDialog(parent), entry(entry) { setModal(true); setWindowTitle(tr("Edit Playlist Entry")); QGridLayout* layout = new QGridLayout; QLabel* urlLabel0 = new QLabel(tr("URL:")); layout->addWidget(urlLabel0, 0, 0); urlLabel = new QLabel(entry.url.toString()); layout->addWidget(urlLabel, 0, 1, 1, 2); QPushButton* setFileBtn = new QPushButton(tr("Set File...")); connect(setFileBtn, SIGNAL(clicked()), this, SLOT(setFile())); layout->addWidget(setFileBtn, 1, 1); QPushButton* setURLBtn = new QPushButton(tr("Set URL...")); connect(setURLBtn, SIGNAL(clicked()), this, SLOT(setURL())); layout->addWidget(setURLBtn, 1, 2); QLabel* inputModeLabel = new QLabel(tr("Input Mode:")); layout->addWidget(inputModeLabel, 2, 0); inputModeBox = new QComboBox(this); for (int i = 0; i < 12; i++) inputModeBox->addItem(inputModeToStringUI(static_cast(i))); connect(inputModeBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateEntry())); layout->addWidget(inputModeBox, 2, 1, 1, 2); QLabel* surroundModeLabel = new QLabel(tr("Surround Mode:")); layout->addWidget(surroundModeLabel, 3, 0); surroundModeBox = new QComboBox(this); for (int i = 0; i < 4; i++) surroundModeBox->addItem(surroundModeToStringUI(static_cast(i))); connect(surroundModeBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateEntry())); layout->addWidget(surroundModeBox, 3, 1, 1, 2); QLabel* videoTrackLabel = new QLabel(tr("Video Track:")); layout->addWidget(videoTrackLabel, 4, 0); videoTrackBox = new QComboBox(this); connect(videoTrackBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateEntry())); layout->addWidget(videoTrackBox, 4, 1, 1, 2); QLabel* audioTrackLabel = new QLabel(tr("Audio Track:")); layout->addWidget(audioTrackLabel, 5, 0); audioTrackBox = new QComboBox(this); connect(audioTrackBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateEntry())); layout->addWidget(audioTrackBox, 5, 1, 1, 2); QLabel* subtitleTrackLabel = new QLabel(tr("Subtitle Track:")); layout->addWidget(subtitleTrackLabel, 6, 0); subtitleTrackBox = new QComboBox(this); connect(subtitleTrackBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateEntry())); layout->addWidget(subtitleTrackBox, 6, 1, 1, 2); QPushButton* doneBtn = new QPushButton(tr("Done"), this); doneBtn->setDefault(true); connect(doneBtn, SIGNAL(clicked()), this, SLOT(accept())); layout->addWidget(doneBtn, 7, 0, 1, 3); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 1); layout->setRowStretch(0, 1); setLayout(layout); updateBoxStates(); } void PlaylistEntryEditor::setFile() { QString name = QFileDialog::getOpenFileName(this); if (!name.isEmpty()) { entry.url = QUrl::fromLocalFile(name); urlLabel->setText(entry.url.toString()); updateBoxStates(); updateEntry(); } } void PlaylistEntryEditor::setURL() { QDialog *dialog = new QDialog(this); dialog->setWindowTitle(tr("Open URL")); QLabel *label = new QLabel(tr("URL:")); QLineEdit *edit = new QLineEdit(""); edit->setMinimumWidth(256); QPushButton *cancelBtn = new QPushButton(tr("Cancel")); QPushButton *okBtn = new QPushButton(tr("OK")); okBtn->setDefault(true); connect(cancelBtn, SIGNAL(clicked()), dialog, SLOT(reject())); connect(okBtn, SIGNAL(clicked()), dialog, SLOT(accept())); QGridLayout *layout = new QGridLayout(); layout->addWidget(label, 0, 0); layout->addWidget(edit, 0, 1, 1, 3); layout->addWidget(cancelBtn, 2, 2); layout->addWidget(okBtn, 2, 3); layout->setColumnStretch(1, 1); dialog->setLayout(layout); dialog->exec(); if (dialog->result() == QDialog::Accepted && !edit->text().isEmpty()) { entry.url = QUrl::fromUserInput(edit->text()); urlLabel->setText(entry.url.toString()); updateBoxStates(); updateEntry(); } } void PlaylistEntryEditor::updateEntry() { entry.inputMode = static_cast(inputModeBox->currentIndex()); entry.surroundMode = static_cast(surroundModeBox->currentIndex()); entry.videoTrack = videoTrackBox->currentIndex() >= 0 ? videoTrackBox->currentIndex() - 1 : -1; entry.audioTrack = audioTrackBox->currentIndex() >= 0 ? audioTrackBox->currentIndex() - 1 : -1; entry.subtitleTrack = subtitleTrackBox->currentIndex() >= 0 ? subtitleTrackBox->currentIndex() - 2 : -2; } void PlaylistEntryEditor::updateBoxStates() { MetaData metaData; bool haveMetaData = false; if (!entry.url.isEmpty()) { haveMetaData = metaData.detectCached(entry.url); if (haveMetaData && metaData.videoTracks.size() < 1) haveMetaData = false; } inputModeBox->setCurrentIndex(0); inputModeBox->setEnabled(false); surroundModeBox->setCurrentIndex(0); surroundModeBox->setEnabled(false); videoTrackBox->clear(); videoTrackBox->addItem(tr("default")); videoTrackBox->setCurrentIndex(0); videoTrackBox->setEnabled(false); audioTrackBox->clear(); audioTrackBox->addItem(tr("default")); audioTrackBox->setCurrentIndex(0); audioTrackBox->setEnabled(false); subtitleTrackBox->clear(); subtitleTrackBox->addItem(tr("none")); subtitleTrackBox->setCurrentIndex(0); subtitleTrackBox->setEnabled(false); if (haveMetaData) { inputModeBox->setEnabled(true); surroundModeBox->setEnabled(true); for (int i = 0; i < metaData.videoTracks.size(); i++) { QString s = QString::number(i); QLocale::Language l = static_cast(metaData.videoTracks[i].value(QMediaMetaData::Language).toInt()); if (l != QLocale::AnyLanguage) s += QString(tr(" (%1)")).arg(QLocale::languageToString(l)); videoTrackBox->addItem(s); } videoTrackBox->setEnabled(true); for (int i = 0; i < metaData.audioTracks.size(); i++) { QString s = QString::number(i); QLocale::Language l = static_cast(metaData.audioTracks[i].value(QMediaMetaData::Language).toInt()); if (l != QLocale::AnyLanguage) s += QString(tr(" (%1)")).arg(QLocale::languageToString(l)); audioTrackBox->addItem(s); } audioTrackBox->setEnabled(true); if (metaData.subtitleTracks.size() > 0) subtitleTrackBox->addItem(tr("default")); for (int i = 0; i < metaData.subtitleTracks.size(); i++) { QString s = QString::number(i); QLocale::Language l = static_cast(metaData.subtitleTracks[i].value(QMediaMetaData::Language).toInt()); if (l != QLocale::AnyLanguage) s += QString(tr(" (%1)")).arg(QLocale::languageToString(l)); subtitleTrackBox->addItem(s); } subtitleTrackBox->setEnabled(true); } } PlaylistEditor::PlaylistEditor(QWidget* parent) : QDialog(parent) { setModal(true); setWindowTitle(tr("Edit Playlist")); QGridLayout* layout = new QGridLayout; table = new QTableWidget(this); table->setColumnCount(6); table->setHorizontalHeaderLabels({ tr("URL"), tr("Input Mode"), tr("Surround Mode"), tr("Video Track"), tr("Audio Track"), tr("Subtitle Track") }); table->setSelectionMode(QAbstractItemView::SingleSelection); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->horizontalHeader()->setHighlightSections(false); table->verticalHeader()->setHighlightSections(false); table->resizeColumnsToContents(); connect(table, SIGNAL(itemSelectionChanged()), this, SLOT(updateButtonState())); layout->addWidget(table, 0, 0, 7, 7); upBtn = new QPushButton(tr("Move up"), this); connect(upBtn, SIGNAL(clicked()), this, SLOT(up())); layout->addWidget(upBtn, 0, 7, 1, 1); downBtn = new QPushButton(tr("Move down"), this); connect(downBtn, SIGNAL(clicked()), this, SLOT(down())); layout->addWidget(downBtn, 1, 7, 1, 1); addBtn = new QPushButton(tr("Add..."), this); connect(addBtn, SIGNAL(clicked()), this, SLOT(add())); layout->addWidget(addBtn, 2, 7, 1, 1); delBtn = new QPushButton(tr("Remove"), this); connect(delBtn, SIGNAL(clicked()), this, SLOT(del())); layout->addWidget(delBtn, 3, 7, 1, 1); editBtn = new QPushButton(tr("Edit..."), this); connect(editBtn, SIGNAL(clicked()), this, SLOT(edit())); layout->addWidget(editBtn, 4, 7, 1, 1); QPushButton* doneBtn = new QPushButton(tr("Done"), this); doneBtn->setDefault(true); connect(doneBtn, SIGNAL(clicked()), this, SLOT(accept())); layout->addWidget(doneBtn, 5, 7, 1, 1); layout->addItem(new QSpacerItem(0, 0), 6, 7, 1, 1); layout->setColumnStretch(0, 1); layout->setRowStretch(0, 1); setLayout(layout); updateTable(); updateButtonState(); } void PlaylistEditor::updateTable() { const Playlist* playlist = Playlist::instance(); int row = table->currentRow(); table->clearContents(); table->setRowCount(playlist->length()); for (int i = 0; i < playlist->length(); i++) { const PlaylistEntry& entry = playlist->entries()[i]; table->setItem(i, 0, new QTableWidgetItem(entry.url.toString())); table->setItem(i, 1, new QTableWidgetItem(inputModeToStringUI(entry.inputMode))); table->setItem(i, 2, new QTableWidgetItem(surroundModeToStringUI(entry.surroundMode))); table->setItem(i, 3, new QTableWidgetItem( entry.videoTrack < 0 ? tr("default") : QString::number(entry.videoTrack))); table->setItem(i, 4, new QTableWidgetItem( entry.audioTrack < 0 ? tr("default") : QString::number(entry.audioTrack))); table->setItem(i, 5, new QTableWidgetItem( entry.subtitleTrack < -1 ? tr("none") : entry.subtitleTrack < 0 ? tr("default") : QString::number(entry.subtitleTrack))); for (int j = 0; j < 6; j++) table->item(i, j)->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren); } table->setCurrentCell(row, 0, QItemSelectionModel::Rows); table->resizeColumnsToContents(); } void PlaylistEditor::updateButtonState() { int row = selectedRow(); bool haveSelection = (row >= 0 && row < table->rowCount()); upBtn->setEnabled(haveSelection && row > 0); downBtn->setEnabled(haveSelection && row < table->rowCount() - 1); delBtn->setEnabled(haveSelection); editBtn->setEnabled(haveSelection); } int PlaylistEditor::selectedRow() { QList selection = table->selectedItems(); return (selection.isEmpty() ? -1 : selection[0]->row()); } void PlaylistEditor::up() { int row = selectedRow(); if (row > 0) { Playlist* playlist = Playlist::instance(); playlist->insert(row - 1, playlist->entries()[row]); playlist->remove(row + 1); updateTable(); table->setCurrentCell(row - 1, 0); updateButtonState(); } } void PlaylistEditor::down() { int row = selectedRow(); if (row < table->rowCount() - 1) { Playlist* playlist = Playlist::instance(); playlist->insert(row + 2, playlist->entries()[row]); playlist->remove(row); updateTable(); table->setCurrentCell(row + 1, 0); updateButtonState(); } } void PlaylistEditor::add() { int row = selectedRow(); if (row < 0 || row > table->rowCount() - 1) row = table->rowCount() - 1; Playlist* playlist = Playlist::instance(); playlist->insert(row + 1, PlaylistEntry(QUrl(""))); updateTable(); table->setCurrentCell(row + 1, 0); updateButtonState(); edit(); } void PlaylistEditor::del() { int row = selectedRow(); if (row >= 0 && row < table->rowCount()) { Playlist* playlist = Playlist::instance(); playlist->remove(row); updateTable(); table->setCurrentCell(row, 0); updateButtonState(); } } void PlaylistEditor::edit() { int row = selectedRow(); if (row >= 0 && row < table->rowCount()) { Playlist* playlist = Playlist::instance(); PlaylistEntryEditor editor(playlist->entries()[row], this); if (editor.exec() == QDialog::Accepted) { playlist->entries()[row] = editor.entry; updateTable(); table->setCurrentCell(row, 0); } } } bino-2.5/src/playlisteditor.hpp000066400000000000000000000033751475415313200166740ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2023 * Martin Lambers * * 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 . */ #pragma once #include class QLabel; class QComboBox; class QPushButton; class QTableWidget; #include "playlist.hpp" class PlaylistEntryEditor : public QDialog { Q_OBJECT private: QLabel* urlLabel; QComboBox* inputModeBox; QComboBox* surroundModeBox; QComboBox* videoTrackBox; QComboBox* audioTrackBox; QComboBox* subtitleTrackBox; void updateBoxStates(); private slots: void updateEntry(); void setFile(); void setURL(); public: PlaylistEntry entry; PlaylistEntryEditor(const PlaylistEntry& entry, QWidget* parent); }; class PlaylistEditor : public QDialog { Q_OBJECT private: QTableWidget* table; QPushButton* upBtn; QPushButton* downBtn; QPushButton* addBtn; QPushButton* delBtn; QPushButton* editBtn; void updateTable(); int selectedRow(); private slots: void updateButtonState(); void up(); void down(); void add(); void del(); void edit(); public: PlaylistEditor(QWidget* parent); }; bino-2.5/src/qvrapp.cpp000066400000000000000000000205301475415313200151200ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022 * Computer Graphics Group, University of Siegen * Written by Martin Lambers * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #ifdef WITH_QVR #include #include #include "qvrapp.hpp" #include "bino.hpp" #include "tools.hpp" BinoQVRApp::BinoQVRApp() { } unsigned int BinoQVRApp::setupTex(const QImage& img) { unsigned int tex; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width(), img.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, img.constBits()); glGenerateMipmap(GL_TEXTURE_2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); if (_haveAnisotropicFiltering) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 4.0f); return tex; } unsigned int BinoQVRApp::setupVao(int vertexCount, const float* positions, const float* normals, const float* texcoords, int indexCount, const unsigned short* indices) { GLuint vao; GLuint positionBuf, normalBuf, texcoordBuf, indexBuf; glGenVertexArrays(1, &vao); glBindVertexArray(vao); glGenBuffers(1, &positionBuf); glBindBuffer(GL_ARRAY_BUFFER, positionBuf); glBufferData(GL_ARRAY_BUFFER, vertexCount * 3 * sizeof(float), positions, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); glGenBuffers(1, &normalBuf); glBindBuffer(GL_ARRAY_BUFFER, normalBuf); glBufferData(GL_ARRAY_BUFFER, vertexCount * 3 * sizeof(float), normals, GL_STATIC_DRAW); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); glGenBuffers(1, &texcoordBuf); glBindBuffer(GL_ARRAY_BUFFER, texcoordBuf); glBufferData(GL_ARRAY_BUFFER, vertexCount * 2 * sizeof(float), texcoords, GL_STATIC_DRAW); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(2); glGenBuffers(1, &indexBuf); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuf); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * sizeof(unsigned short), indices, GL_STATIC_DRAW); return vao; } void BinoQVRApp::serializeStaticData(QDataStream& ds) const { Bino::instance()->serializeStaticData(ds); } void BinoQVRApp::deserializeStaticData(QDataStream& ds) { Bino::instance()->deserializeStaticData(ds); } void BinoQVRApp::serializeDynamicData(QDataStream& ds) const { Bino::instance()->serializeDynamicData(ds); } void BinoQVRApp::deserializeDynamicData(QDataStream& ds) { Bino::instance()->deserializeDynamicData(ds); } bool BinoQVRApp::wantExit() { return Bino::instance()->wantExit(); } bool BinoQVRApp::initProcess(QVRProcess*) { initializeOpenGLFunctions(); _haveAnisotropicFiltering = checkTextureAnisotropicFilterAvailability(); // Shader program QString vrdeviceVS = readFile(":src/shader-vrdevice.vert.glsl"); QString vrdeviceFS = readFile(":src/shader-vrdevice.frag.glsl"); if (IsOpenGLES) { vrdeviceVS.prepend("#version 300 es\n"); vrdeviceFS.prepend("#version 300 es\n" "precision mediump float;\n"); } else { vrdeviceVS.prepend("#version 330\n"); vrdeviceFS.prepend("#version 330\n"); } _prg.addShaderFromSourceCode(QOpenGLShader::Vertex, vrdeviceVS); _prg.addShaderFromSourceCode(QOpenGLShader::Fragment, vrdeviceFS); _prg.link(); // Get device model data for (int i = 0; i < QVRManager::deviceModelVertexDataCount(); i++) { _devModelVaos.append(setupVao( QVRManager::deviceModelVertexCount(i), QVRManager::deviceModelVertexPositions(i), QVRManager::deviceModelVertexNormals(i), QVRManager::deviceModelVertexTexCoords(i), QVRManager::deviceModelVertexIndexCount(i), QVRManager::deviceModelVertexIndices(i))); _devModelVaoIndices.append(QVRManager::deviceModelVertexIndexCount(i)); } for (int i = 0; i < QVRManager::deviceModelTextureCount(); i++) { _devModelTextures.append(setupTex(QVRManager::deviceModelTexture(i))); } return Bino::instance()->initProcess(); } void BinoQVRApp::preRenderProcess(QVRProcess*) { Bino::instance()->preRenderProcess(); } void BinoQVRApp::render(QVRWindow*, const QVRRenderContext& context, const unsigned int* textures) { for (int view = 0; view < context.viewCount(); view++) { // Render Bino view QMatrix4x4 projectionMatrix = context.frustum(view).toMatrix4x4(); QMatrix4x4 orientationMatrix; orientationMatrix.rotate(context.navigationOrientation().inverted()); orientationMatrix.rotate(context.trackingOrientation(view).inverted()); QMatrix4x4 viewMatrix = context.viewMatrix(view); QMatrix4x4 viewMatrixPure = context.viewMatrixPure(view); int v = (context.eye(view) == QVR_Eye_Right ? 1 : 0); int texWidth = context.textureSize(view).width(); int texHeight = context.textureSize(view).height(); Bino::instance()->render( context.unitedScreenWallBottomLeft(), context.unitedScreenWallBottomRight(), context.unitedScreenWallTopLeft(), context.intersectedScreenWallBottomLeft(), context.intersectedScreenWallBottomRight(), context.intersectedScreenWallTopLeft(), projectionMatrix, orientationMatrix, viewMatrix, v, texWidth, texHeight, textures[view]); // Render VR device models (optional) glUseProgram(_prg.programId()); for (int i = 0; i < QVRManager::deviceCount(); i++) { const QVRDevice& device = QVRManager::device(i); for (int j = 0; j < device.modelNodeCount(); j++) { QMatrix4x4 nodeMatrix = device.matrix(); nodeMatrix.translate(device.modelNodePosition(j)); nodeMatrix.rotate(device.modelNodeOrientation(j)); QMatrix4x4 modelViewMatrix = viewMatrixPure * nodeMatrix; int vertexDataIndex = device.modelNodeVertexDataIndex(j); int textureIndex = device.modelNodeTextureIndex(j); _prg.setUniformValue("modelViewMatrix", modelViewMatrix); _prg.setUniformValue("projectionModelViewMatrix", projectionMatrix * modelViewMatrix); _prg.setUniformValue("normalMatrix", modelViewMatrix.normalMatrix()); _prg.setUniformValue("hasDiffTex", _devModelTextures[textureIndex] == 0 ? 0 : 1); _prg.setUniformValue("diffTex", 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _devModelTextures[textureIndex]); glBindVertexArray(_devModelVaos[vertexDataIndex]); glDrawElements(GL_TRIANGLES, _devModelVaoIndices[vertexDataIndex], GL_UNSIGNED_SHORT, 0); } } } // Invalidate depth attachment (to help OpenGL ES performance) const GLenum fboInvalidations[] = { GL_DEPTH_ATTACHMENT }; glInvalidateFramebuffer(GL_FRAMEBUFFER, 1, fboInvalidations); } void BinoQVRApp::keyPressEvent(const QVRRenderContext&, QKeyEvent* event) { Bino::instance()->keyPressEvent(event); } #endif bino-2.5/src/qvrapp.hpp000066400000000000000000000041641475415313200151320ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ #pragma once #ifdef WITH_QVR #include #include #include class BinoQVRApp : public QVRApp, protected QOpenGLExtraFunctions { private: /* Static per-process data for rendering */ bool _haveAnisotropicFiltering; QOpenGLShaderProgram _prg; // Data to render device models QVector _devModelVaos; QVector _devModelVaoIndices; QVector _devModelTextures; /* Helper function for texture loading */ unsigned int setupTex(const QImage& img); /* Helper function for VAO setup */ unsigned int setupVao(int vertexCount, const float* positions, const float* normals, const float* texcoords, int indexCount, const unsigned short* indices); public: BinoQVRApp(); void serializeStaticData(QDataStream& ds) const override; void deserializeStaticData(QDataStream& ds) override; void serializeDynamicData(QDataStream& ds) const override; void deserializeDynamicData(QDataStream& ds) override; bool wantExit() override; bool initProcess(QVRProcess* p) override; void preRenderProcess(QVRProcess* p) override; void render(QVRWindow* w, const QVRRenderContext& c, const unsigned int* textures) override; void keyPressEvent(const QVRRenderContext& context, QKeyEvent* event) override; }; #endif bino-2.5/src/screen.cpp000066400000000000000000000141341475415313200150710ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2017 Computer Graphics Group, University of Siegen * Written by Martin Lambers * * Copyright (C) 2022 * Martin Lambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include "screen.hpp" #include "log.hpp" #define TINYOBJLOADER_IMPLEMENTATION #include "tiny_obj_loader.h" Screen::Screen() { positions = { -1.0f, +1.0f, 0.0f, +1.0f, +1.0f, 0.0f, +1.0f, -1.0f, 0.0f, -1.0f, -1.0f, 0.0f }; texcoords = { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f }; indices = { 0, 3, 1, 1, 3, 2 }; aspectRatio = 0.0f; } Screen::Screen(const QVector3D& bottomLeftCorner, const QVector3D& bottomRightCorner, const QVector3D& topLeftCorner) { QVector3D topRightCorner = bottomRightCorner + (topLeftCorner - bottomLeftCorner); positions = { topLeftCorner.x(), topLeftCorner.y(), topLeftCorner.z(), topRightCorner.x(), topRightCorner.y(), topRightCorner.z(), bottomRightCorner.x(), bottomRightCorner.y(), bottomRightCorner.z(), bottomLeftCorner.x(), bottomLeftCorner.y(), bottomLeftCorner.z() }; texcoords = { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f }; indices = { 0, 3, 1, 1, 3, 2 }; float width = (bottomRightCorner - bottomLeftCorner).length(); float height = (topLeftCorner - bottomLeftCorner).length(); aspectRatio = width / height; } /* Helper function to split a multiline TinyObjLoader message */ static std::vector tinyObjMsgToLines(const std::string& s) { std::vector lines; size_t i = 0; for (;;) { size_t j = s.find_first_of('\n', i); if (j < std::string::npos) { lines.push_back(s.substr(i, j - i)); i = j + 1; } else { break; } } return lines; } Screen::Screen(const QString& objFileName, const QString& shapeName, float aspectRatio) { LOG_INFO("%s", qPrintable(tr("Loading screen from %1").arg(objFileName))); tinyobj::ObjReaderConfig conf; conf.triangulate = true; conf.vertex_color = false; tinyobj::ObjReader reader; reader.ParseFromFile(qPrintable(objFileName), conf); if (reader.Warning().size() > 0) { std::vector lines = tinyObjMsgToLines(reader.Warning()); for (size_t i = 0; i < lines.size(); i++) LOG_WARNING(" %s", qPrintable(tr("Warning: %1").arg(lines[i].c_str()))); } if (!reader.Valid()) { if (reader.Error().size() > 0) { std::vector lines = tinyObjMsgToLines(reader.Error()); for (size_t i = 0; i < lines.size(); i++) LOG_FATAL(" %s", qPrintable(tr("Error: %1").arg(lines[i].c_str()))); } else { LOG_FATAL(" %s", qPrintable(tr("Unknown error"))); } return; } // Read all geometry (or at least the shape with the given name) // and ignore all materials. const tinyobj::attrib_t& attrib = reader.GetAttrib(); const std::vector& shapes = reader.GetShapes(); std::map, unsigned int> indexTupleMap; positions.clear(); texcoords.clear(); indices.clear(); bool haveTexcoords = true; for (size_t s = 0; s < shapes.size(); s++) { if (!shapeName.isEmpty() && shapeName != shapes[s].name.c_str()) continue; const tinyobj::mesh_t& mesh = shapes[s].mesh; for (size_t i = 0; i < mesh.indices.size(); i++) { const tinyobj::index_t& index = mesh.indices[i]; int vi = index.vertex_index; int ti = index.texcoord_index; std::tuple indexTuple = std::make_tuple(vi, ti); auto it = indexTupleMap.find(indexTuple); if (it == indexTupleMap.end()) { unsigned int newIndex = indexTupleMap.size(); assert(vi >= 0); positions.append(attrib.vertices[3 * vi + 0]); positions.append(attrib.vertices[3 * vi + 1]); positions.append(attrib.vertices[3 * vi + 2]); if (ti < 0) haveTexcoords = false; if (haveTexcoords) { texcoords.append(attrib.texcoords[2 * ti + 0]); texcoords.append(attrib.texcoords[2 * ti + 1]); } indices.push_back(newIndex); indexTupleMap.insert(std::make_pair(indexTuple, newIndex)); } else { indices.push_back(it->second); } } } if (!haveTexcoords) { texcoords.clear(); } this->aspectRatio = aspectRatio; } QDataStream &operator<<(QDataStream& ds, const Screen& s) { ds << s.positions << s.texcoords << s.indices << s.aspectRatio; return ds; } QDataStream &operator>>(QDataStream& ds, Screen& s) { ds >> s.positions >> s.texcoords >> s.indices >> s.aspectRatio; return ds; } bino-2.5/src/screen.hpp000066400000000000000000000053221475415313200150750ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2017 Computer Graphics Group, University of Siegen * Written by Martin Lambers * * Copyright (C) 2022 * Martin Lambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* This VR screen functionality is adapted from qvr-videoplayer */ #pragma once #include #include #include class Screen { Q_DECLARE_TR_FUNCTIONS(Screen) public: QVector positions; // each position consists of 3 floats QVector texcoords; // each texcoord consists of 2 floats QVector indices; float aspectRatio; // Default constructor for a viewport-filling quad for GUI mode. // The aspectRatio will unknown (set to 0) since it depends on the viewport. Screen(); // Construct a planar screen from three corners. // The aspect ratio is computed automatically. Screen(const QVector3D& bottomLeftCorner, const QVector3D& bottomRightCorner, const QVector3D& topLeftCorner); // Construct a screen by reading the specified OBJ file. // If the given shape name is not empty, only this shape will be considered. // Since the aspect ratio cannot be computed, it has to be specified. // The OBJ data must contain positions and texture coordinates; // everything else is ignored. If indices.size() == 0 // after constructing the screen in this way, then loading // the OBJ file failed. Screen(const QString& objFileName, const QString& shapeName, float aspectRatio); }; QDataStream &operator<<(QDataStream& ds, const Screen& screen); QDataStream &operator>>(QDataStream& ds, Screen& screen); bino-2.5/src/shader-color.frag.glsl000066400000000000000000000155231475415313200172740ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ uniform sampler2D plane0; uniform sampler2D plane1; uniform sampler2D plane2; uniform sampler2D plane3; const int Format_RGB = 1; const int Format_YUVp = 2; const int Format_YVUp = 3; const int Format_YUVsp = 4; const int Format_Y = 5; const int planeFormat = $PLANE_FORMAT; const bool colorRangeSmall = $COLOR_RANGE_SMALL; const int CS_BT601 = 1; const int CS_BT709 = 2; const int CS_AdobeRGB = 3; const int CS_BT2020 = 4; const int colorSpace = $COLOR_SPACE; const int CT_NOOP = 1; const int CT_ST2084 = 2; const int CT_STD_B67 = 3; const int colorTransfer = $COLOR_TRANSFER; uniform float masteringWhite; smooth in vec2 vtexcoord; layout(location = 0) out vec4 fcolor; float to_linear(float x) { const float c0 = 0.077399380805; // 1.0 / 12.92 const float c1 = 0.947867298578; // 1.0 / 1.055; return (x <= 0.04045 ? (x * c0) : pow((x + 0.055) * c1, 2.4)); } vec3 rgb_to_linear(vec3 rgb) { return vec3(to_linear(rgb.r), to_linear(rgb.g), to_linear(rgb.b)); } void main(void) { vec3 yuv = vec3(1.0, 0.0, 0.0); vec3 rgb = vec3(0.0, 1.0, 0.0); if (planeFormat == Format_RGB) { rgb = texture(plane0, vtexcoord).rgb; } else if (planeFormat == Format_Y) { rgb = texture(plane0, vtexcoord).rrr; } else { if (planeFormat == Format_YUVp) { yuv = vec3( texture(plane0, vtexcoord).r, texture(plane1, vtexcoord).r, texture(plane2, vtexcoord).r); } else if (planeFormat == Format_YVUp) { yuv = vec3( texture(plane0, vtexcoord).r, texture(plane2, vtexcoord).r, texture(plane1, vtexcoord).r); } else if (planeFormat == Format_YUVsp) { yuv = vec3( texture(plane0, vtexcoord).r, texture(plane1, vtexcoord).rg); } mat4 m; // The following matrices are the same as used by Qt, // see qtmultimedia/src/multimedia/video/qvideotexturehelper.cpp if (colorSpace == CS_AdobeRGB) { m = mat4( 1.0, 1.0, 1.0, 0.0, 0.0, -0.344, 1.772, 0.0, 1.402, -0.714, 0.0, 0.0, -0.701, 0.529, -0.886, 1.0); } else if (colorSpace == CS_BT709) { if (colorRangeSmall) { m = mat4( 1.1644, 1.1644, 1.1644, 0.0, 0.0, -0.2132, 2.1124, 0.0, 1.7927, -0.5329, 0.0, 0.0, -0.9729, 0.3015, -1.1334, 1.0); } else { m = mat4( 1.0, 1.0, 1.0, 0.0, 0.0, -0.187324, 1.8556, 0.0, 1.5748, -0.468124, 0.0, 0.0, -0.790488, 0.329010, -0.931439, 1.0); } } else if (colorSpace == CS_BT2020) { if (colorRangeSmall) { m = mat4( 1.1644, 1.1644, 1.1644, 0.0, 0.0, -0.1874, 2.1418, 0.0, 1.6787, -0.6504, 0.0, 0.0, -0.9157, 0.3475, -1.1483, 1.0); } else { m = mat4( 1.0, 1.0, 1.0, 0.0, 0.0, -0.1646, 1.8814, 0.0, 1.4746, -0.5714, 0.0, 0.0, -0.7402, 0.3694, -0.9445, 1.0); } } else { if (colorRangeSmall) { m = mat4( 1.164, 1.164, 1.164, 0.0, 0.0, -0.392, 2.017, 0.0, 1.596, -0.813, 0.0, 0.0, -0.8708, 0.5296, -1.081, 1.0); } else { m = mat4( 1.0, 1.0, 1.0, 0.0, 0.0, -0.1646, 1.42, 0.0, 1.772, -0.57135, 0.0, 0.0, -0.886, 0.36795, -0.71, 1.0); } } rgb = (m * vec4(yuv, 1.0)).rgb; } if (colorTransfer == CT_ST2084 || colorTransfer == CT_STD_B67) { // This code was reconstructed from the mess in qtmultimedia/src/multimedia/video; // it is distributed there over various shaders and C++ files. // 1. scale const float maxLum = 1.0; float scale = 1.0; float y = (yuv.x - 16.0 / 256.0) * 256.0 / 219.0; // XXX This looks wrong!? float p = y / masteringWhite; float ks = 1.5 * maxLum - 0.5; if (p > ks) { float t = (p - ks) / (1.0 - ks); float t2 = t * t; float t3 = t * t2; p = (2.0 * t3 - 3.0 * t2 + 1.0) * ks + (t3 - 2.0 * t2 + t) * (1.0 - ks) + (-2.0 * t3 + 3.0 * t2) * maxLum; float newY = p * masteringWhite; scale = newY / y; } rgb *= scale; // 2. tonemap if (colorTransfer == CT_ST2084) { const vec3 one_over_m1 = vec3(8192.0 / 1305.0); const vec3 one_over_m2 = vec3(32.0 / 2523.0); const float c1 = 107.0 / 128.0; const float c2 = 2413.0 / 128.0; const float c3 = 2392.0 / 128.0; vec3 e = pow(rgb, one_over_m2); vec3 num = max(e - c1, 0.0); vec3 den = c2 - c3 * e; rgb = pow(num / den, one_over_m1) * 10000.0 / 100.0; } else if (colorTransfer == CT_STD_B67) { const float a = 0.17883277; const float b = 0.28466892; // = 1 - 4a const float c = 0.55991073; // = 0.5 - a ln(4a) bvec3 cutoff = lessThan(rgb, vec3(0.5)); vec3 low = rgb * rgb / 3.0; vec3 high = (exp((rgb - c) / a) + b) / 12.0; rgb = mix(high, low, cutoff); float lum = dot(rgb, vec3(0.2627, 0.6780, 0.0593)); float y = pow(lum, 0.2); // gamma-1 with gamma = 1.2 rgb *= y; } // 3. convert rec2020 to sRGB rgb = rgb * mat3( 1.6605, -0.5876, -0.0728, -0.1246, 1.1329, -0.0083, -0.0182, -0.1006, 1.1187); } else { rgb = rgb_to_linear(rgb); } fcolor = vec4(rgb, 1.0); } bino-2.5/src/shader-color.vert.glsl000066400000000000000000000016651475415313200173370ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ layout(location = 0) in vec4 position; layout(location = 1) in vec2 texcoord; smooth out vec2 vtexcoord; void main(void) { vtexcoord = texcoord; gl_Position = position; } bino-2.5/src/shader-display.frag.glsl000066400000000000000000000235611475415313200176240ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023 * Martin Lambers * * 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 . */ uniform sampler2D view0; uniform sampler2D view1; uniform float relativeWidth; uniform float relativeHeight; uniform float fragOffsetX; uniform float fragOffsetY; // This must be the same as OutputMode from modes.hpp: const int Output_Left = 0; const int Output_Right = 1; const int Output_OpenGL_Stereo = 2; const int Output_Alternating = 3; const int Output_HDMI_Frame_Pack = 4; const int Output_Left_Right = 5; const int Output_Left_Right_Half = 6; const int Output_Right_Left = 7; const int Output_Right_Left_Half = 8; const int Output_Top_Bottom = 9; const int Output_Top_Bottom_Half = 10; const int Output_Bottom_Top = 11; const int Output_Bottom_Top_Half = 12; const int Output_Even_Odd_Rows = 13; const int Output_Even_Odd_Columns = 14; const int Output_Checkerboard = 15; const int Output_Red_Cyan_Dubois = 16; const int Output_Red_Cyan_FullColor = 17; const int Output_Red_Cyan_HalfColor = 18; const int Output_Red_Cyan_Monochrome = 19; const int Output_Green_Magenta_Dubois = 20; const int Output_Green_Magenta_FullColor = 21; const int Output_Green_Magenta_HalfColor = 22; const int Output_Green_Magenta_Monochrome = 23; const int Output_Amber_Blue_Dubois = 24; const int Output_Amber_Blue_FullColor = 25; const int Output_Amber_Blue_HalfColor = 26; const int Output_Amber_Blue_Monochrome = 27; const int Output_Red_Green_Monochrome = 28; const int Output_Red_Blue_Monochrome = 29; const int outputMode = $OUTPUT_MODE; uniform int outputModeLeftRightView; // to distinguish betwenen Output_Left and Output_Right; // we don't want both in separate shaders because // Output_OpenGL_Stereo and Ouput_Alternating switch // in-frame or between frames between those two. smooth in vec2 vtexcoord; layout(location = 0) out vec4 fcolor; // linear RGB to luminance, as used by Mitsuba2 and pbrt float rgb_to_lum(vec3 rgb) { return dot(rgb, vec3(0.212671, 0.715160, 0.072169)); } // linear RGB to non-linear RGB float to_nonlinear(float x) { const float c0 = 0.416666666667; // 1.0 / 2.4 return (x <= 0.0031308 ? (x * 12.92) : (1.055 * pow(x, c0) - 0.055)); } vec3 rgb_to_nonlinear(vec3 rgb) { return vec3(to_nonlinear(rgb.r), to_nonlinear(rgb.g), to_nonlinear(rgb.b)); } void main(void) { float tx = (vtexcoord.x - 0.5 * (1.0 - relativeWidth )) / relativeWidth; float ty = (vtexcoord.y - 0.5 * (1.0 - relativeHeight)) / relativeHeight; vec3 rgb = vec3(0.0, 0.0, 0.0); if (outputMode == Output_HDMI_Frame_Pack) { // HDMI frame pack has left view on top, right view at bottom, and adds 1/49 vertical blank space // between the two views: // - For 720p: 720 lines left view, 30 blank lines, 720 lines right view = 1470 lines; 1470/49=30 // - For 1080p: 1080 lines left view, 45 blank lines, 1080 lines right view = 2205 lines; 2205/49=45 // See the document "High-Definition Multimedia Interface Specification Version 1.4a Extraction // of 3D Signaling Portion" from hdmi.org. const float blankPortion = 1.0 / 49.0; const float a = 0.5 + 0.5 * blankPortion; const float b = 0.5 - 0.5 * blankPortion; if (ty >= a) { if (tx >= 0.0 && tx <= 1.0) { float tty = (ty - a) / (1.0 - a); rgb = texture(view0, vec2(tx, tty)).rgb; } } else if (ty < b) { if (tx >= 0.0 && tx <= 1.0) { float tty = ty / b; rgb = texture(view1, vec2(tx, tty)).rgb; } } } else if (outputMode == Output_Left || outputMode == Output_Right) { if (outputModeLeftRightView == 0) rgb = texture(view0, vec2(tx, ty)).rgb; else rgb = texture(view1, vec2(tx, ty)).rgb; } else if (outputMode == Output_Left_Right || outputMode == Output_Left_Right_Half) { if (tx < 0.5) { if (ty >= 0.0 && ty <= 1.0) rgb = texture(view0, vec2(2.0 * tx, ty)).rgb; } else { if (ty >= 0.0 && ty <= 1.0) rgb = texture(view1, vec2(2.0 * tx - 1.0, ty)).rgb; } } else if (outputMode == Output_Right_Left || outputMode == Output_Right_Left_Half) { if (tx < 0.5) { if (ty >= 0.0 && ty <= 1.0) rgb = texture(view1, vec2(2.0 * tx, ty)).rgb; } else { if (ty >= 0.0 && ty <= 1.0) rgb = texture(view0, vec2(2.0 * tx - 1.0, ty)).rgb; } } else if (outputMode == Output_Top_Bottom || outputMode == Output_Top_Bottom_Half) { if (ty >= 0.5) { if (tx >= 0.0 && tx <= 1.0) rgb = texture(view0, vec2(tx, 2.0 * ty - 1.0)).rgb; } else { if (tx >= 0.0 && tx <= 1.0) rgb = texture(view1, vec2(tx, 2.0 * ty)).rgb; } } else if (outputMode == Output_Bottom_Top || outputMode == Output_Bottom_Top_Half) { if (ty >= 0.5) { if (tx >= 0.0 && tx <= 1.0) rgb = texture(view1, vec2(tx, 2.0 * ty - 1.0)).rgb; } else { if (tx >= 0.0 && tx <= 1.0) rgb = texture(view0, vec2(tx, 2.0 * ty)).rgb; } } else if (outputMode == Output_Even_Odd_Rows) { float fragmentY = gl_FragCoord.y - 0.5 + fragOffsetY; if (mod(fragmentY, 2.0) < 0.5) { rgb = texture(view0, vec2(tx, ty)).rgb; } else { rgb = texture(view1, vec2(tx, ty)).rgb; } } else if (outputMode == Output_Even_Odd_Columns) { float fragmentX = gl_FragCoord.x - 0.5 + fragOffsetX; if (mod(fragmentX, 2.0) < 0.5) { rgb = texture(view0, vec2(tx, ty)).rgb; } else { rgb = texture(view1, vec2(tx, ty)).rgb; } } else if (outputMode == Output_Checkerboard) { float fragmentX = gl_FragCoord.x - 0.5 + fragOffsetX; float fragmentY = gl_FragCoord.y - 0.5 + fragOffsetY; if (abs(mod(fragmentX, 2.0) - mod(fragmentY, 2.0)) < 0.5) { rgb = texture(view0, vec2(tx, ty)).rgb; } else { rgb = texture(view1, vec2(tx, ty)).rgb; } } else { vec3 rgb0 = texture(view0, vec2(tx, ty)).rgb; vec3 rgb1 = texture(view1, vec2(tx, ty)).rgb; if (outputMode == Output_Red_Cyan_Dubois) { // Source of this matrix: http://www.site.uottawa.ca/~edubois/anaglyph/LeastSquaresHowToPhotoshop.pdf mat3 m0 = mat3( 0.437, -0.062, -0.048, 0.449, -0.062, -0.050, 0.164, -0.024, -0.017); mat3 m1 = mat3( -0.011, 0.377, -0.026, -0.032, 0.761, -0.093, -0.007, 0.009, 1.234); rgb = m0 * rgb0 + m1 * rgb1; } else if (outputMode == Output_Red_Cyan_FullColor) { rgb = vec3(rgb0.r, rgb1.g, rgb1.b); } else if (outputMode == Output_Red_Cyan_HalfColor) { rgb = vec3(rgb_to_lum(rgb0), rgb1.g, rgb1.b); } else if (outputMode == Output_Red_Cyan_Monochrome) { rgb = vec3(rgb_to_lum(rgb0), rgb_to_lum(rgb1), rgb_to_lum(rgb1)); } else if (outputMode == Output_Green_Magenta_Dubois) { // Source of this matrix: http://www.flickr.com/photos/e_dubois/5132528166/ mat3 m0 = mat3( -0.062, 0.284, -0.015, -0.158, 0.668, -0.027, -0.039, 0.143, 0.021); mat3 m1 = mat3( 0.529, -0.016, 0.009, 0.705, -0.015, 0.075, 0.024, -0.065, 0.937); rgb = m0 * rgb0 + m1 * rgb1; } else if (outputMode == Output_Green_Magenta_FullColor) { rgb = vec3(rgb1.r, rgb0.g, rgb1.b); } else if (outputMode == Output_Green_Magenta_HalfColor) { rgb = vec3(rgb1.r, rgb_to_lum(rgb0), rgb1.b); } else if (outputMode == Output_Green_Magenta_Monochrome) { rgb = vec3(rgb_to_lum(rgb1), rgb_to_lum(rgb0), rgb_to_lum(rgb1)); } else if (outputMode == Output_Amber_Blue_Dubois) { // Source of this matrix: http://www.flickr.com/photos/e_dubois/5230654930/ mat3 m0 = mat3( 1.062, -0.026, -0.038, -0.205, 0.908, -0.173, 0.299, 0.068, 0.022); mat3 m1 = mat3( -0.016, 0.006, 0.094, -0.123, 0.062, 0.185, -0.017, -0.017, 0.911); rgb = m0 * rgb0 + m1 * rgb1; } else if (outputMode == Output_Amber_Blue_FullColor) { rgb = vec3(rgb0.r, rgb0.g, rgb1.b); } else if (outputMode == Output_Amber_Blue_HalfColor) { rgb = vec3(rgb_to_lum(rgb0), rgb_to_lum(rgb0), rgb1.b); } else if (outputMode == Output_Amber_Blue_Monochrome) { rgb = vec3(rgb_to_lum(rgb0), rgb_to_lum(rgb0), rgb_to_lum(rgb1)); } else if (outputMode == Output_Red_Green_Monochrome) { rgb = vec3(rgb_to_lum(rgb0), rgb_to_lum(rgb1), 0.0); } else if (outputMode == Output_Red_Blue_Monochrome) { rgb = vec3(rgb_to_lum(rgb0), 0.0, rgb_to_lum(rgb1)); } } fcolor = vec4(rgb_to_nonlinear(rgb), 1.0); } bino-2.5/src/shader-display.vert.glsl000066400000000000000000000016651475415313200176660ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ layout(location = 0) in vec4 position; layout(location = 1) in vec2 texcoord; smooth out vec2 vtexcoord; void main(void) { vtexcoord = texcoord; gl_Position = position; } bino-2.5/src/shader-view.frag.glsl000066400000000000000000000050661475415313200171310ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ uniform sampler2D frameTex; uniform sampler2D subtitleTex; uniform float relative_width; uniform float relative_height; uniform float view_offset_x; uniform float view_factor_x; uniform float view_offset_y; uniform float view_factor_y; int surroundDegrees = $SURROUND_DEGREES; const bool nonlinear_output = $NONLINEAR_OUTPUT; smooth in vec2 vtexcoord; smooth in vec3 vdirection; const float pi = 3.14159265358979323846; layout(location = 0) out vec4 fcolor; // linear RGB to non-linear RGB float to_nonlinear(float x) { const float c0 = 0.416666666667; // 1.0 / 2.4 return (x <= 0.0031308 ? (x * 12.92) : (1.055 * pow(x, c0) - 0.055)); } vec3 rgb_to_nonlinear(vec3 rgb) { return vec3(to_nonlinear(rgb.r), to_nonlinear(rgb.g), to_nonlinear(rgb.b)); } void main(void) { vec3 rgb; if (surroundDegrees > 0) { vec3 dir = normalize(vdirection); float theta = asin(clamp(-dir.y, -1.0, 1.0)); float phi = atan(dir.x, -dir.z); float tmp = (surroundDegrees == 360 ? 2.0f * pi : pi); float u = phi / tmp + 0.5; float v = theta / pi + 0.5; float vtx = view_offset_x + view_factor_x * u; float vty = view_offset_y + view_factor_y * v; rgb = texture(frameTex, vec2(vtx, vty)).rgb; } else { float vtx = view_offset_x + view_factor_x * vtexcoord.x; float vty = view_offset_y + view_factor_y * vtexcoord.y; float tx = ( vtx - 0.5 * (1.0 - relative_width )) / relative_width; float ty = (1.0 - vty - 0.5 * (1.0 - relative_height)) / relative_height; rgb = texture(frameTex, vec2(tx, ty)).rgb; vec4 sub = texture(subtitleTex, vec2(vtexcoord.x, 1.0 - vtexcoord.y)).rgba; rgb = mix(rgb, sub.rgb, sub.a); } if (nonlinear_output) { rgb = rgb_to_nonlinear(rgb); } fcolor = vec4(rgb, 1.0); } bino-2.5/src/shader-view.vert.glsl000066400000000000000000000021531475415313200171640ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ uniform mat4 projectionModelViewMatrix; uniform mat4 orientationMatrix; layout(location = 0) in vec4 position; layout(location = 1) in vec2 texcoord; smooth out vec2 vtexcoord; smooth out vec3 vdirection; void main(void) { vtexcoord = texcoord; vdirection = (position * orientationMatrix).xyz; gl_Position = projectionModelViewMatrix * position; } bino-2.5/src/shader-vrdevice.frag.glsl000066400000000000000000000026771475415313200177730ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022 * Martin Lambers * * 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 . */ uniform bool hasDiffTex; uniform sampler2D diffTex; smooth in vec3 vnormal; smooth in vec3 vlight; smooth in vec3 vview; smooth in vec2 vtexcoord; layout(location = 0) out vec4 fcolor; void main(void) { const vec3 lightAmbient = vec3(0.0); const vec3 lightColor = vec3(1.0); vec3 materialColor = vec3(1.0); if (hasDiffTex) materialColor = texture(diffTex, vtexcoord).rgb; vec3 normal = normalize(vnormal); vec3 light = normalize(vlight); vec3 view = normalize(vview); vec3 halfv = normalize(light + view); vec3 diffuse = materialColor * lightColor * max(dot(light, normal), 0.0); vec3 color = lightAmbient + diffuse; fcolor = vec4(color, 1.0); } bino-2.5/src/shader-vrdevice.vert.glsl000066400000000000000000000035371475415313200200300ustar00rootroot00000000000000/* * Copyright (C) 2016, 2017 Computer Graphics Group, University of Siegen * Written by Martin Lambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ uniform mat4 projectionModelViewMatrix; uniform mat4 modelViewMatrix; uniform mat3 normalMatrix; layout(location = 0) in vec4 pos; layout(location = 1) in vec3 normal; layout(location = 2) in vec2 texcoord; smooth out vec3 vnormal; // normal in eye space, not normalized smooth out vec3 vlight; // light vector in eye space, not normalized smooth out vec3 vview; // view vector in eye space, not normalized smooth out vec2 vtexcoord; void main(void) { vnormal = normalMatrix * normal; vview = -(modelViewMatrix * pos).xyz; vlight = vview; // light is always at camera pos vtexcoord = texcoord; gl_Position = projectionModelViewMatrix * pos; } bino-2.5/src/tiny_obj_loader.h000066400000000000000000003224001475415313200164200ustar00rootroot00000000000000/* The MIT License (MIT) Copyright (c) 2012-Present, Syoyo Fujita and many contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // // version 2.0.0 : Add new object oriented API. 1.x API is still provided. // * Add python binding. // * Support line primitive. // * Support points primitive. // * Support multiple search path for .mtl(v1 API). // * Support vertex skinning weight `vw`(as an tinyobj // extension). Note that this differs vertex weight([w] // component in `v` line) // * Support escaped whitespece in mtllib // * Add robust triangulation using Mapbox // earcut(TINYOBJLOADER_USE_MAPBOX_EARCUT). // version 1.4.0 : Modifed ParseTextureNameAndOption API // version 1.3.1 : Make ParseTextureNameAndOption API public // version 1.3.0 : Separate warning and error message(breaking API of LoadObj) // version 1.2.3 : Added color space extension('-colorspace') to tex opts. // version 1.2.2 : Parse multiple group names. // version 1.2.1 : Added initial support for line('l') primitive(PR #178) // version 1.2.0 : Hardened implementation(#175) // version 1.1.1 : Support smoothing groups(#162) // version 1.1.0 : Support parsing vertex color(#144) // version 1.0.8 : Fix parsing `g` tag just after `usemtl`(#138) // version 1.0.7 : Support multiple tex options(#126) // version 1.0.6 : Add TINYOBJLOADER_USE_DOUBLE option(#124) // version 1.0.5 : Ignore `Tr` when `d` exists in MTL(#43) // version 1.0.4 : Support multiple filenames for 'mtllib'(#112) // version 1.0.3 : Support parsing texture options(#85) // version 1.0.2 : Improve parsing speed by about a factor of 2 for large // files(#105) // version 1.0.1 : Fixes a shape is lost if obj ends with a 'usemtl'(#104) // version 1.0.0 : Change data structure. Change license from BSD to MIT. // // // Use this in *one* .cc // #define TINYOBJLOADER_IMPLEMENTATION // #include "tiny_obj_loader.h" // #ifndef TINY_OBJ_LOADER_H_ #define TINY_OBJ_LOADER_H_ #include #include #include namespace tinyobj { // TODO(syoyo): Better C++11 detection for older compiler #if __cplusplus > 199711L #define TINYOBJ_OVERRIDE override #else #define TINYOBJ_OVERRIDE #endif #ifdef __clang__ #pragma clang diagnostic push #if __has_warning("-Wzero-as-null-pointer-constant") #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" #endif #pragma clang diagnostic ignored "-Wpadded" #endif // https://en.wikipedia.org/wiki/Wavefront_.obj_file says ... // // -blendu on | off # set horizontal texture blending // (default on) // -blendv on | off # set vertical texture blending // (default on) // -boost real_value # boost mip-map sharpness // -mm base_value gain_value # modify texture map values (default // 0 1) // # base_value = brightness, // gain_value = contrast // -o u [v [w]] # Origin offset (default // 0 0 0) // -s u [v [w]] # Scale (default // 1 1 1) // -t u [v [w]] # Turbulence (default // 0 0 0) // -texres resolution # texture resolution to create // -clamp on | off # only render texels in the clamped // 0-1 range (default off) // # When unclamped, textures are // repeated across a surface, // # when clamped, only texels which // fall within the 0-1 // # range are rendered. // -bm mult_value # bump multiplier (for bump maps // only) // // -imfchan r | g | b | m | l | z # specifies which channel of the file // is used to // # create a scalar or bump texture. // r:red, g:green, // # b:blue, m:matte, l:luminance, // z:z-depth.. // # (the default for bump is 'l' and // for decal is 'm') // bump -imfchan r bumpmap.tga # says to use the red channel of // bumpmap.tga as the bumpmap // // For reflection maps... // // -type sphere # specifies a sphere for a "refl" // reflection map // -type cube_top | cube_bottom | # when using a cube map, the texture // file for each // cube_front | cube_back | # side of the cube is specified // separately // cube_left | cube_right // // TinyObjLoader extension. // // -colorspace SPACE # Color space of the texture. e.g. // 'sRGB` or 'linear' // #ifdef TINYOBJLOADER_USE_DOUBLE //#pragma message "using double" typedef double real_t; #else //#pragma message "using float" typedef float real_t; #endif typedef enum { TEXTURE_TYPE_NONE, // default TEXTURE_TYPE_SPHERE, TEXTURE_TYPE_CUBE_TOP, TEXTURE_TYPE_CUBE_BOTTOM, TEXTURE_TYPE_CUBE_FRONT, TEXTURE_TYPE_CUBE_BACK, TEXTURE_TYPE_CUBE_LEFT, TEXTURE_TYPE_CUBE_RIGHT } texture_type_t; struct texture_option_t { texture_type_t type; // -type (default TEXTURE_TYPE_NONE) real_t sharpness; // -boost (default 1.0?) real_t brightness; // base_value in -mm option (default 0) real_t contrast; // gain_value in -mm option (default 1) real_t origin_offset[3]; // -o u [v [w]] (default 0 0 0) real_t scale[3]; // -s u [v [w]] (default 1 1 1) real_t turbulence[3]; // -t u [v [w]] (default 0 0 0) int texture_resolution; // -texres resolution (No default value in the spec. // We'll use -1) bool clamp; // -clamp (default false) char imfchan; // -imfchan (the default for bump is 'l' and for decal is 'm') bool blendu; // -blendu (default on) bool blendv; // -blendv (default on) real_t bump_multiplier; // -bm (for bump maps only, default 1.0) // extension std::string colorspace; // Explicitly specify color space of stored texel // value. Usually `sRGB` or `linear` (default empty). }; struct material_t { std::string name; real_t ambient[3]; real_t diffuse[3]; real_t specular[3]; real_t transmittance[3]; real_t emission[3]; real_t shininess; real_t ior; // index of refraction real_t dissolve; // 1 == opaque; 0 == fully transparent // illumination model (see http://www.fileformat.info/format/material/) int illum; int dummy; // Suppress padding warning. std::string ambient_texname; // map_Ka. For ambient or ambient occlusion. std::string diffuse_texname; // map_Kd std::string specular_texname; // map_Ks std::string specular_highlight_texname; // map_Ns std::string bump_texname; // map_bump, map_Bump, bump std::string displacement_texname; // disp std::string alpha_texname; // map_d std::string reflection_texname; // refl texture_option_t ambient_texopt; texture_option_t diffuse_texopt; texture_option_t specular_texopt; texture_option_t specular_highlight_texopt; texture_option_t bump_texopt; texture_option_t displacement_texopt; texture_option_t alpha_texopt; texture_option_t reflection_texopt; // PBR extension // http://exocortex.com/blog/extending_wavefront_mtl_to_support_pbr real_t roughness; // [0, 1] default 0 real_t metallic; // [0, 1] default 0 real_t sheen; // [0, 1] default 0 real_t clearcoat_thickness; // [0, 1] default 0 real_t clearcoat_roughness; // [0, 1] default 0 real_t anisotropy; // aniso. [0, 1] default 0 real_t anisotropy_rotation; // anisor. [0, 1] default 0 real_t pad0; std::string roughness_texname; // map_Pr std::string metallic_texname; // map_Pm std::string sheen_texname; // map_Ps std::string emissive_texname; // map_Ke std::string normal_texname; // norm. For normal mapping. texture_option_t roughness_texopt; texture_option_t metallic_texopt; texture_option_t sheen_texopt; texture_option_t emissive_texopt; texture_option_t normal_texopt; int pad2; std::map unknown_parameter; #ifdef TINY_OBJ_LOADER_PYTHON_BINDING // For pybind11 std::array GetDiffuse() { std::array values; values[0] = double(diffuse[0]); values[1] = double(diffuse[1]); values[2] = double(diffuse[2]); return values; } std::array GetSpecular() { std::array values; values[0] = double(specular[0]); values[1] = double(specular[1]); values[2] = double(specular[2]); return values; } std::array GetTransmittance() { std::array values; values[0] = double(transmittance[0]); values[1] = double(transmittance[1]); values[2] = double(transmittance[2]); return values; } std::array GetEmission() { std::array values; values[0] = double(emission[0]); values[1] = double(emission[1]); values[2] = double(emission[2]); return values; } std::array GetAmbient() { std::array values; values[0] = double(ambient[0]); values[1] = double(ambient[1]); values[2] = double(ambient[2]); return values; } void SetDiffuse(std::array &a) { diffuse[0] = real_t(a[0]); diffuse[1] = real_t(a[1]); diffuse[2] = real_t(a[2]); } void SetAmbient(std::array &a) { ambient[0] = real_t(a[0]); ambient[1] = real_t(a[1]); ambient[2] = real_t(a[2]); } void SetSpecular(std::array &a) { specular[0] = real_t(a[0]); specular[1] = real_t(a[1]); specular[2] = real_t(a[2]); } void SetTransmittance(std::array &a) { transmittance[0] = real_t(a[0]); transmittance[1] = real_t(a[1]); transmittance[2] = real_t(a[2]); } std::string GetCustomParameter(const std::string &key) { std::map::const_iterator it = unknown_parameter.find(key); if (it != unknown_parameter.end()) { return it->second; } return std::string(); } #endif }; struct tag_t { std::string name; std::vector intValues; std::vector floatValues; std::vector stringValues; }; struct joint_and_weight_t { int joint_id; real_t weight; }; struct skin_weight_t { int vertex_id; // Corresponding vertex index in `attrib_t::vertices`. // Compared to `index_t`, this index must be positive and // start with 0(does not allow relative indexing) std::vector weightValues; }; // Index struct to support different indices for vtx/normal/texcoord. // -1 means not used. struct index_t { int vertex_index; int normal_index; int texcoord_index; }; struct mesh_t { std::vector indices; std::vector num_face_vertices; // The number of vertices per // face. 3 = triangle, 4 = quad, ... std::vector material_ids; // per-face material ID std::vector smoothing_group_ids; // per-face smoothing group // ID(0 = off. positive value // = group id) std::vector tags; // SubD tag }; // struct path_t { // std::vector indices; // pairs of indices for lines //}; struct lines_t { // Linear flattened indices. std::vector indices; // indices for vertices(poly lines) std::vector num_line_vertices; // The number of vertices per line. }; struct points_t { std::vector indices; // indices for points }; struct shape_t { std::string name; mesh_t mesh; lines_t lines; points_t points; }; // Vertex attributes struct attrib_t { std::vector vertices; // 'v'(xyz) // For backward compatibility, we store vertex weight in separate array. std::vector vertex_weights; // 'v'(w) std::vector normals; // 'vn' std::vector texcoords; // 'vt'(uv) // For backward compatibility, we store texture coordinate 'w' in separate // array. std::vector texcoord_ws; // 'vt'(w) std::vector colors; // extension: vertex colors // // TinyObj extension. // // NOTE(syoyo): array index is based on the appearance order. // To get a corresponding skin weight for a specific vertex id `vid`, // Need to reconstruct a look up table: `skin_weight_t::vertex_id` == `vid` // (e.g. using std::map, std::unordered_map) std::vector skin_weights; attrib_t() {} // // For pybind11 // const std::vector &GetVertices() const { return vertices; } const std::vector &GetVertexWeights() const { return vertex_weights; } }; struct callback_t { // W is optional and set to 1 if there is no `w` item in `v` line void (*vertex_cb)(void *user_data, real_t x, real_t y, real_t z, real_t w); void (*vertex_color_cb)(void *user_data, real_t x, real_t y, real_t z, real_t r, real_t g, real_t b, bool has_color); void (*normal_cb)(void *user_data, real_t x, real_t y, real_t z); // y and z are optional and set to 0 if there is no `y` and/or `z` item(s) in // `vt` line. void (*texcoord_cb)(void *user_data, real_t x, real_t y, real_t z); // called per 'f' line. num_indices is the number of face indices(e.g. 3 for // triangle, 4 for quad) // 0 will be passed for undefined index in index_t members. void (*index_cb)(void *user_data, index_t *indices, int num_indices); // `name` material name, `material_id` = the array index of material_t[]. -1 // if // a material not found in .mtl void (*usemtl_cb)(void *user_data, const char *name, int material_id); // `materials` = parsed material data. void (*mtllib_cb)(void *user_data, const material_t *materials, int num_materials); // There may be multiple group names void (*group_cb)(void *user_data, const char **names, int num_names); void (*object_cb)(void *user_data, const char *name); callback_t() : vertex_cb(NULL), vertex_color_cb(NULL), normal_cb(NULL), texcoord_cb(NULL), index_cb(NULL), usemtl_cb(NULL), mtllib_cb(NULL), group_cb(NULL), object_cb(NULL) {} }; class MaterialReader { public: MaterialReader() {} virtual ~MaterialReader(); virtual bool operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, std::string *err) = 0; }; /// /// Read .mtl from a file. /// class MaterialFileReader : public MaterialReader { public: // Path could contain separator(';' in Windows, ':' in Posix) explicit MaterialFileReader(const std::string &mtl_basedir) : m_mtlBaseDir(mtl_basedir) {} virtual ~MaterialFileReader() TINYOBJ_OVERRIDE {} virtual bool operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, std::string *err) TINYOBJ_OVERRIDE; private: std::string m_mtlBaseDir; }; /// /// Read .mtl from a stream. /// class MaterialStreamReader : public MaterialReader { public: explicit MaterialStreamReader(std::istream &inStream) : m_inStream(inStream) {} virtual ~MaterialStreamReader() TINYOBJ_OVERRIDE {} virtual bool operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, std::string *err) TINYOBJ_OVERRIDE; private: std::istream &m_inStream; }; // v2 API struct ObjReaderConfig { bool triangulate; // triangulate polygon? // Currently not used. // "simple" or empty: Create triangle fan // "earcut": Use the algorithm based on Ear clipping std::string triangulation_method; /// Parse vertex color. /// If vertex color is not present, its filled with default value. /// false = no vertex color /// This will increase memory of parsed .obj bool vertex_color; /// /// Search path to .mtl file. /// Default = "" = search from the same directory of .obj file. /// Valid only when loading .obj from a file. /// std::string mtl_search_path; ObjReaderConfig() : triangulate(true), triangulation_method("simple"), vertex_color(true) {} }; /// /// Wavefront .obj reader class(v2 API) /// class ObjReader { public: ObjReader() : valid_(false) {} /// /// Load .obj and .mtl from a file. /// /// @param[in] filename wavefront .obj filename /// @param[in] config Reader configuration /// bool ParseFromFile(const std::string &filename, const ObjReaderConfig &config = ObjReaderConfig()); /// /// Parse .obj from a text string. /// Need to supply .mtl text string by `mtl_text`. /// This function ignores `mtllib` line in .obj text. /// /// @param[in] obj_text wavefront .obj filename /// @param[in] mtl_text wavefront .mtl filename /// @param[in] config Reader configuration /// bool ParseFromString(const std::string &obj_text, const std::string &mtl_text, const ObjReaderConfig &config = ObjReaderConfig()); /// /// .obj was loaded or parsed correctly. /// bool Valid() const { return valid_; } const attrib_t &GetAttrib() const { return attrib_; } const std::vector &GetShapes() const { return shapes_; } const std::vector &GetMaterials() const { return materials_; } /// /// Warning message(may be filled after `Load` or `Parse`) /// const std::string &Warning() const { return warning_; } /// /// Error message(filled when `Load` or `Parse` failed) /// const std::string &Error() const { return error_; } private: bool valid_; attrib_t attrib_; std::vector shapes_; std::vector materials_; std::string warning_; std::string error_; }; /// ==>>========= Legacy v1 API ============================================= /// Loads .obj from a file. /// 'attrib', 'shapes' and 'materials' will be filled with parsed shape data /// 'shapes' will be filled with parsed shape data /// Returns true when loading .obj become success. /// Returns warning message into `warn`, and error message into `err` /// 'mtl_basedir' is optional, and used for base directory for .mtl file. /// In default(`NULL'), .mtl file is searched from an application's working /// directory. /// 'triangulate' is optional, and used whether triangulate polygon face in .obj /// or not. /// Option 'default_vcols_fallback' specifies whether vertex colors should /// always be defined, even if no colors are given (fallback to white). bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector *materials, std::string *warn, std::string *err, const char *filename, const char *mtl_basedir = NULL, bool triangulate = true, bool default_vcols_fallback = true); /// Loads .obj from a file with custom user callback. /// .mtl is loaded as usual and parsed material_t data will be passed to /// `callback.mtllib_cb`. /// Returns true when loading .obj/.mtl become success. /// Returns warning message into `warn`, and error message into `err` /// See `examples/callback_api/` for how to use this function. bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, void *user_data = NULL, MaterialReader *readMatFn = NULL, std::string *warn = NULL, std::string *err = NULL); /// Loads object from a std::istream, uses `readMatFn` to retrieve /// std::istream for materials. /// Returns true when loading .obj become success. /// Returns warning and error message into `err` bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector *materials, std::string *warn, std::string *err, std::istream *inStream, MaterialReader *readMatFn = NULL, bool triangulate = true, bool default_vcols_fallback = true); /// Loads materials into std::map void LoadMtl(std::map *material_map, std::vector *materials, std::istream *inStream, std::string *warning, std::string *err); /// /// Parse texture name and texture option for custom texture parameter through /// material::unknown_parameter /// /// @param[out] texname Parsed texture name /// @param[out] texopt Parsed texopt /// @param[in] linebuf Input string /// bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, const char *linebuf); /// =<<========== Legacy v1 API ============================================= } // namespace tinyobj #endif // TINY_OBJ_LOADER_H_ #ifdef TINYOBJLOADER_IMPLEMENTATION #include #include #include #include #include #include #include #include #include #include #include #ifdef TINYOBJLOADER_USE_MAPBOX_EARCUT #ifdef TINYOBJLOADER_DONOT_INCLUDE_MAPBOX_EARCUT // Assume earcut.hpp is included outside of tiny_obj_loader.h #else #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Weverything" #endif #include #include "mapbox/earcut.hpp" #ifdef __clang__ #pragma clang diagnostic pop #endif #endif #endif // TINYOBJLOADER_USE_MAPBOX_EARCUT namespace tinyobj { MaterialReader::~MaterialReader() {} struct vertex_index_t { int v_idx, vt_idx, vn_idx; vertex_index_t() : v_idx(-1), vt_idx(-1), vn_idx(-1) {} explicit vertex_index_t(int idx) : v_idx(idx), vt_idx(idx), vn_idx(idx) {} vertex_index_t(int vidx, int vtidx, int vnidx) : v_idx(vidx), vt_idx(vtidx), vn_idx(vnidx) {} }; // Internal data structure for face representation // index + smoothing group. struct face_t { unsigned int smoothing_group_id; // smoothing group id. 0 = smoothing groupd is off. int pad_; std::vector vertex_indices; // face vertex indices. face_t() : smoothing_group_id(0), pad_(0) {} }; // Internal data structure for line representation struct __line_t { // l v1/vt1 v2/vt2 ... // In the specification, line primitrive does not have normal index, but // TinyObjLoader allow it std::vector vertex_indices; }; // Internal data structure for points representation struct __points_t { // p v1 v2 ... // In the specification, point primitrive does not have normal index and // texture coord index, but TinyObjLoader allow it. std::vector vertex_indices; }; struct tag_sizes { tag_sizes() : num_ints(0), num_reals(0), num_strings(0) {} int num_ints; int num_reals; int num_strings; }; struct obj_shape { std::vector v; std::vector vn; std::vector vt; }; // // Manages group of primitives(face, line, points, ...) struct PrimGroup { std::vector faceGroup; std::vector<__line_t> lineGroup; std::vector<__points_t> pointsGroup; void clear() { faceGroup.clear(); lineGroup.clear(); pointsGroup.clear(); } bool IsEmpty() const { return faceGroup.empty() && lineGroup.empty() && pointsGroup.empty(); } // TODO(syoyo): bspline, surface, ... }; // See // http://stackoverflow.com/questions/6089231/getting-std-ifstream-to-handle-lf-cr-and-crlf static std::istream &safeGetline(std::istream &is, std::string &t) { t.clear(); // The characters in the stream are read one-by-one using a std::streambuf. // That is faster than reading them one-by-one using the std::istream. // Code that uses streambuf this way must be guarded by a sentry object. // The sentry object performs various tasks, // such as thread synchronization and updating the stream state. std::istream::sentry se(is, true); std::streambuf *sb = is.rdbuf(); if (se) { for (;;) { int c = sb->sbumpc(); switch (c) { case '\n': return is; case '\r': if (sb->sgetc() == '\n') sb->sbumpc(); return is; case EOF: // Also handle the case when the last line has no line ending if (t.empty()) is.setstate(std::ios::eofbit); return is; default: t += static_cast(c); } } } return is; } #define IS_SPACE(x) (((x) == ' ') || ((x) == '\t')) #define IS_DIGIT(x) \ (static_cast((x) - '0') < static_cast(10)) #define IS_NEW_LINE(x) (((x) == '\r') || ((x) == '\n') || ((x) == '\0')) template static inline std::string toString(const T &t) { std::stringstream ss; ss << t; return ss.str(); } struct warning_context { std::string *warn; size_t line_number; }; // Make index zero-base, and also support relative index. static inline bool fixIndex(int idx, int n, int *ret, bool allow_zero, const warning_context &context) { if (!ret) { return false; } if (idx > 0) { (*ret) = idx - 1; return true; } if (idx == 0) { // zero is not allowed according to the spec. if (context.warn) { (*context.warn) += "A zero value index found (will have a value of -1 for normal and " "tex indices. Line " + toString(context.line_number) + ").\n"; } (*ret) = idx - 1; return allow_zero; } if (idx < 0) { (*ret) = n + idx; // negative value = relative if ((*ret) < 0) { return false; // invalid relative index } return true; } return false; // never reach here. } static inline std::string parseString(const char **token) { std::string s; (*token) += strspn((*token), " \t"); size_t e = strcspn((*token), " \t\r"); s = std::string((*token), &(*token)[e]); (*token) += e; return s; } static inline int parseInt(const char **token) { (*token) += strspn((*token), " \t"); int i = atoi((*token)); (*token) += strcspn((*token), " \t\r"); return i; } // Tries to parse a floating point number located at s. // // s_end should be a location in the string where reading should absolutely // stop. For example at the end of the string, to prevent buffer overflows. // // Parses the following EBNF grammar: // sign = "+" | "-" ; // END = ? anything not in digit ? // digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; // integer = [sign] , digit , {digit} ; // decimal = integer , ["." , integer] ; // float = ( decimal , END ) | ( decimal , ("E" | "e") , integer , END ) ; // // Valid strings are for example: // -0 +3.1417e+2 -0.0E-3 1.0324 -1.41 11e2 // // If the parsing is a success, result is set to the parsed value and true // is returned. // // The function is greedy and will parse until any of the following happens: // - a non-conforming character is encountered. // - s_end is reached. // // The following situations triggers a failure: // - s >= s_end. // - parse failure. // static bool tryParseDouble(const char *s, const char *s_end, double *result) { if (s >= s_end) { return false; } double mantissa = 0.0; // This exponent is base 2 rather than 10. // However the exponent we parse is supposed to be one of ten, // thus we must take care to convert the exponent/and or the // mantissa to a * 2^E, where a is the mantissa and E is the // exponent. // To get the final double we will use ldexp, it requires the // exponent to be in base 2. int exponent = 0; // NOTE: THESE MUST BE DECLARED HERE SINCE WE ARE NOT ALLOWED // TO JUMP OVER DEFINITIONS. char sign = '+'; char exp_sign = '+'; char const *curr = s; // How many characters were read in a loop. int read = 0; // Tells whether a loop terminated due to reaching s_end. bool end_not_reached = false; bool leading_decimal_dots = false; /* BEGIN PARSING. */ // Find out what sign we've got. if (*curr == '+' || *curr == '-') { sign = *curr; curr++; if ((curr != s_end) && (*curr == '.')) { // accept. Somethig like `.7e+2`, `-.5234` leading_decimal_dots = true; } } else if (IS_DIGIT(*curr)) { /* Pass through. */ } else if (*curr == '.') { // accept. Somethig like `.7e+2`, `-.5234` leading_decimal_dots = true; } else { goto fail; } // Read the integer part. end_not_reached = (curr != s_end); if (!leading_decimal_dots) { while (end_not_reached && IS_DIGIT(*curr)) { mantissa *= 10; mantissa += static_cast(*curr - 0x30); curr++; read++; end_not_reached = (curr != s_end); } // We must make sure we actually got something. if (read == 0) goto fail; } // We allow numbers of form "#", "###" etc. if (!end_not_reached) goto assemble; // Read the decimal part. if (*curr == '.') { curr++; read = 1; end_not_reached = (curr != s_end); while (end_not_reached && IS_DIGIT(*curr)) { static const double pow_lut[] = { 1.0, 0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001, 0.0000001, }; const int lut_entries = sizeof pow_lut / sizeof pow_lut[0]; // NOTE: Don't use powf here, it will absolutely murder precision. mantissa += static_cast(*curr - 0x30) * (read < lut_entries ? pow_lut[read] : std::pow(10.0, -read)); read++; curr++; end_not_reached = (curr != s_end); } } else if (*curr == 'e' || *curr == 'E') { } else { goto assemble; } if (!end_not_reached) goto assemble; // Read the exponent part. if (*curr == 'e' || *curr == 'E') { curr++; // Figure out if a sign is present and if it is. end_not_reached = (curr != s_end); if (end_not_reached && (*curr == '+' || *curr == '-')) { exp_sign = *curr; curr++; } else if (IS_DIGIT(*curr)) { /* Pass through. */ } else { // Empty E is not allowed. goto fail; } read = 0; end_not_reached = (curr != s_end); while (end_not_reached && IS_DIGIT(*curr)) { // To avoid annoying MSVC's min/max macro definiton, // Use hardcoded int max value if (exponent > (2147483647 / 10)) { // 2147483647 = std::numeric_limits::max() // Integer overflow goto fail; } exponent *= 10; exponent += static_cast(*curr - 0x30); curr++; read++; end_not_reached = (curr != s_end); } exponent *= (exp_sign == '+' ? 1 : -1); if (read == 0) goto fail; } assemble: *result = (sign == '+' ? 1 : -1) * (exponent ? std::ldexp(mantissa * std::pow(5.0, exponent), exponent) : mantissa); return true; fail: return false; } static inline real_t parseReal(const char **token, double default_value = 0.0) { (*token) += strspn((*token), " \t"); const char *end = (*token) + strcspn((*token), " \t\r"); double val = default_value; tryParseDouble((*token), end, &val); real_t f = static_cast(val); (*token) = end; return f; } static inline bool parseReal(const char **token, real_t *out) { (*token) += strspn((*token), " \t"); const char *end = (*token) + strcspn((*token), " \t\r"); double val; bool ret = tryParseDouble((*token), end, &val); if (ret) { real_t f = static_cast(val); (*out) = f; } (*token) = end; return ret; } static inline void parseReal2(real_t *x, real_t *y, const char **token, const double default_x = 0.0, const double default_y = 0.0) { (*x) = parseReal(token, default_x); (*y) = parseReal(token, default_y); } static inline void parseReal3(real_t *x, real_t *y, real_t *z, const char **token, const double default_x = 0.0, const double default_y = 0.0, const double default_z = 0.0) { (*x) = parseReal(token, default_x); (*y) = parseReal(token, default_y); (*z) = parseReal(token, default_z); } #if 0 // not used static inline void parseV(real_t *x, real_t *y, real_t *z, real_t *w, const char **token, const double default_x = 0.0, const double default_y = 0.0, const double default_z = 0.0, const double default_w = 1.0) { (*x) = parseReal(token, default_x); (*y) = parseReal(token, default_y); (*z) = parseReal(token, default_z); (*w) = parseReal(token, default_w); } #endif // Extension: parse vertex with colors(6 items) // Return 3: xyz, 4: xyzw, 6: xyzrgb // `r`: red(case 6) or [w](case 4) static inline int parseVertexWithColor(real_t *x, real_t *y, real_t *z, real_t *r, real_t *g, real_t *b, const char **token, const double default_x = 0.0, const double default_y = 0.0, const double default_z = 0.0) { // TODO: Check error (*x) = parseReal(token, default_x); (*y) = parseReal(token, default_y); (*z) = parseReal(token, default_z); // - 4 components(x, y, z, w) ot 6 components bool has_r = parseReal(token, r); if (!has_r) { (*r) = (*g) = (*b) = 1.0; return 3; } bool has_g = parseReal(token, g); if (!has_g) { (*g) = (*b) = 1.0; return 4; } bool has_b = parseReal(token, b); if (!has_b) { (*r) = (*g) = (*b) = 1.0; return 3; // treated as xyz } return 6; } static inline bool parseOnOff(const char **token, bool default_value = true) { (*token) += strspn((*token), " \t"); const char *end = (*token) + strcspn((*token), " \t\r"); bool ret = default_value; if ((0 == strncmp((*token), "on", 2))) { ret = true; } else if ((0 == strncmp((*token), "off", 3))) { ret = false; } (*token) = end; return ret; } static inline texture_type_t parseTextureType( const char **token, texture_type_t default_value = TEXTURE_TYPE_NONE) { (*token) += strspn((*token), " \t"); const char *end = (*token) + strcspn((*token), " \t\r"); texture_type_t ty = default_value; if ((0 == strncmp((*token), "cube_top", strlen("cube_top")))) { ty = TEXTURE_TYPE_CUBE_TOP; } else if ((0 == strncmp((*token), "cube_bottom", strlen("cube_bottom")))) { ty = TEXTURE_TYPE_CUBE_BOTTOM; } else if ((0 == strncmp((*token), "cube_left", strlen("cube_left")))) { ty = TEXTURE_TYPE_CUBE_LEFT; } else if ((0 == strncmp((*token), "cube_right", strlen("cube_right")))) { ty = TEXTURE_TYPE_CUBE_RIGHT; } else if ((0 == strncmp((*token), "cube_front", strlen("cube_front")))) { ty = TEXTURE_TYPE_CUBE_FRONT; } else if ((0 == strncmp((*token), "cube_back", strlen("cube_back")))) { ty = TEXTURE_TYPE_CUBE_BACK; } else if ((0 == strncmp((*token), "sphere", strlen("sphere")))) { ty = TEXTURE_TYPE_SPHERE; } (*token) = end; return ty; } static tag_sizes parseTagTriple(const char **token) { tag_sizes ts; (*token) += strspn((*token), " \t"); ts.num_ints = atoi((*token)); (*token) += strcspn((*token), "/ \t\r"); if ((*token)[0] != '/') { return ts; } (*token)++; // Skip '/' (*token) += strspn((*token), " \t"); ts.num_reals = atoi((*token)); (*token) += strcspn((*token), "/ \t\r"); if ((*token)[0] != '/') { return ts; } (*token)++; // Skip '/' ts.num_strings = parseInt(token); return ts; } // Parse triples with index offsets: i, i/j/k, i//k, i/j static bool parseTriple(const char **token, int vsize, int vnsize, int vtsize, vertex_index_t *ret, const warning_context &context) { if (!ret) { return false; } vertex_index_t vi(-1); if (!fixIndex(atoi((*token)), vsize, &vi.v_idx, false, context)) { return false; } (*token) += strcspn((*token), "/ \t\r"); if ((*token)[0] != '/') { (*ret) = vi; return true; } (*token)++; // i//k if ((*token)[0] == '/') { (*token)++; if (!fixIndex(atoi((*token)), vnsize, &vi.vn_idx, true, context)) { return false; } (*token) += strcspn((*token), "/ \t\r"); (*ret) = vi; return true; } // i/j/k or i/j if (!fixIndex(atoi((*token)), vtsize, &vi.vt_idx, true, context)) { return false; } (*token) += strcspn((*token), "/ \t\r"); if ((*token)[0] != '/') { (*ret) = vi; return true; } // i/j/k (*token)++; // skip '/' if (!fixIndex(atoi((*token)), vnsize, &vi.vn_idx, true, context)) { return false; } (*token) += strcspn((*token), "/ \t\r"); (*ret) = vi; return true; } // Parse raw triples: i, i/j/k, i//k, i/j static vertex_index_t parseRawTriple(const char **token) { vertex_index_t vi(static_cast(0)); // 0 is an invalid index in OBJ vi.v_idx = atoi((*token)); (*token) += strcspn((*token), "/ \t\r"); if ((*token)[0] != '/') { return vi; } (*token)++; // i//k if ((*token)[0] == '/') { (*token)++; vi.vn_idx = atoi((*token)); (*token) += strcspn((*token), "/ \t\r"); return vi; } // i/j/k or i/j vi.vt_idx = atoi((*token)); (*token) += strcspn((*token), "/ \t\r"); if ((*token)[0] != '/') { return vi; } // i/j/k (*token)++; // skip '/' vi.vn_idx = atoi((*token)); (*token) += strcspn((*token), "/ \t\r"); return vi; } bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, const char *linebuf) { // @todo { write more robust lexer and parser. } bool found_texname = false; std::string texture_name; const char *token = linebuf; // Assume line ends with NULL while (!IS_NEW_LINE((*token))) { token += strspn(token, " \t"); // skip space if ((0 == strncmp(token, "-blendu", 7)) && IS_SPACE((token[7]))) { token += 8; texopt->blendu = parseOnOff(&token, /* default */ true); } else if ((0 == strncmp(token, "-blendv", 7)) && IS_SPACE((token[7]))) { token += 8; texopt->blendv = parseOnOff(&token, /* default */ true); } else if ((0 == strncmp(token, "-clamp", 6)) && IS_SPACE((token[6]))) { token += 7; texopt->clamp = parseOnOff(&token, /* default */ true); } else if ((0 == strncmp(token, "-boost", 6)) && IS_SPACE((token[6]))) { token += 7; texopt->sharpness = parseReal(&token, 1.0); } else if ((0 == strncmp(token, "-bm", 3)) && IS_SPACE((token[3]))) { token += 4; texopt->bump_multiplier = parseReal(&token, 1.0); } else if ((0 == strncmp(token, "-o", 2)) && IS_SPACE((token[2]))) { token += 3; parseReal3(&(texopt->origin_offset[0]), &(texopt->origin_offset[1]), &(texopt->origin_offset[2]), &token); } else if ((0 == strncmp(token, "-s", 2)) && IS_SPACE((token[2]))) { token += 3; parseReal3(&(texopt->scale[0]), &(texopt->scale[1]), &(texopt->scale[2]), &token, 1.0, 1.0, 1.0); } else if ((0 == strncmp(token, "-t", 2)) && IS_SPACE((token[2]))) { token += 3; parseReal3(&(texopt->turbulence[0]), &(texopt->turbulence[1]), &(texopt->turbulence[2]), &token); } else if ((0 == strncmp(token, "-type", 5)) && IS_SPACE((token[5]))) { token += 5; texopt->type = parseTextureType((&token), TEXTURE_TYPE_NONE); } else if ((0 == strncmp(token, "-texres", 7)) && IS_SPACE((token[7]))) { token += 7; // TODO(syoyo): Check if arg is int type. texopt->texture_resolution = parseInt(&token); } else if ((0 == strncmp(token, "-imfchan", 8)) && IS_SPACE((token[8]))) { token += 9; token += strspn(token, " \t"); const char *end = token + strcspn(token, " \t\r"); if ((end - token) == 1) { // Assume one char for -imfchan texopt->imfchan = (*token); } token = end; } else if ((0 == strncmp(token, "-mm", 3)) && IS_SPACE((token[3]))) { token += 4; parseReal2(&(texopt->brightness), &(texopt->contrast), &token, 0.0, 1.0); } else if ((0 == strncmp(token, "-colorspace", 11)) && IS_SPACE((token[11]))) { token += 12; texopt->colorspace = parseString(&token); } else { // Assume texture filename #if 0 size_t len = strcspn(token, " \t\r"); // untile next space texture_name = std::string(token, token + len); token += len; token += strspn(token, " \t"); // skip space #else // Read filename until line end to parse filename containing whitespace // TODO(syoyo): Support parsing texture option flag after the filename. texture_name = std::string(token); token += texture_name.length(); #endif found_texname = true; } } if (found_texname) { (*texname) = texture_name; return true; } else { return false; } } static void InitTexOpt(texture_option_t *texopt, const bool is_bump) { if (is_bump) { texopt->imfchan = 'l'; } else { texopt->imfchan = 'm'; } texopt->bump_multiplier = static_cast(1.0); texopt->clamp = false; texopt->blendu = true; texopt->blendv = true; texopt->sharpness = static_cast(1.0); texopt->brightness = static_cast(0.0); texopt->contrast = static_cast(1.0); texopt->origin_offset[0] = static_cast(0.0); texopt->origin_offset[1] = static_cast(0.0); texopt->origin_offset[2] = static_cast(0.0); texopt->scale[0] = static_cast(1.0); texopt->scale[1] = static_cast(1.0); texopt->scale[2] = static_cast(1.0); texopt->turbulence[0] = static_cast(0.0); texopt->turbulence[1] = static_cast(0.0); texopt->turbulence[2] = static_cast(0.0); texopt->texture_resolution = -1; texopt->type = TEXTURE_TYPE_NONE; } static void InitMaterial(material_t *material) { InitTexOpt(&material->ambient_texopt, /* is_bump */ false); InitTexOpt(&material->diffuse_texopt, /* is_bump */ false); InitTexOpt(&material->specular_texopt, /* is_bump */ false); InitTexOpt(&material->specular_highlight_texopt, /* is_bump */ false); InitTexOpt(&material->bump_texopt, /* is_bump */ true); InitTexOpt(&material->displacement_texopt, /* is_bump */ false); InitTexOpt(&material->alpha_texopt, /* is_bump */ false); InitTexOpt(&material->reflection_texopt, /* is_bump */ false); InitTexOpt(&material->roughness_texopt, /* is_bump */ false); InitTexOpt(&material->metallic_texopt, /* is_bump */ false); InitTexOpt(&material->sheen_texopt, /* is_bump */ false); InitTexOpt(&material->emissive_texopt, /* is_bump */ false); InitTexOpt(&material->normal_texopt, /* is_bump */ false); // @fixme { is_bump will be true? } material->name = ""; material->ambient_texname = ""; material->diffuse_texname = ""; material->specular_texname = ""; material->specular_highlight_texname = ""; material->bump_texname = ""; material->displacement_texname = ""; material->reflection_texname = ""; material->alpha_texname = ""; for (int i = 0; i < 3; i++) { material->ambient[i] = static_cast(0.0); material->diffuse[i] = static_cast(0.0); material->specular[i] = static_cast(0.0); material->transmittance[i] = static_cast(0.0); material->emission[i] = static_cast(0.0); } material->illum = 0; material->dissolve = static_cast(1.0); material->shininess = static_cast(1.0); material->ior = static_cast(1.0); material->roughness = static_cast(0.0); material->metallic = static_cast(0.0); material->sheen = static_cast(0.0); material->clearcoat_thickness = static_cast(0.0); material->clearcoat_roughness = static_cast(0.0); material->anisotropy_rotation = static_cast(0.0); material->anisotropy = static_cast(0.0); material->roughness_texname = ""; material->metallic_texname = ""; material->sheen_texname = ""; material->emissive_texname = ""; material->normal_texname = ""; material->unknown_parameter.clear(); } // code from https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html template static int pnpoly(int nvert, T *vertx, T *verty, T testx, T testy) { int i, j, c = 0; for (i = 0, j = nvert - 1; i < nvert; j = i++) { if (((verty[i] > testy) != (verty[j] > testy)) && (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) c = !c; } return c; } struct TinyObjPoint { real_t x, y, z; TinyObjPoint() : x(0), y(0), z(0) {} TinyObjPoint(real_t x_, real_t y_, real_t z_) : x(x_), y(y_), z(z_) {} }; inline TinyObjPoint cross(const TinyObjPoint &v1, const TinyObjPoint &v2) { return TinyObjPoint(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x); } inline real_t dot(const TinyObjPoint &v1, const TinyObjPoint &v2) { return (v1.x * v2.x + v1.y * v2.y + v1.z * v2.z); } inline real_t GetLength(TinyObjPoint &e) { return std::sqrt(e.x * e.x + e.y * e.y + e.z * e.z); } inline TinyObjPoint Normalize(TinyObjPoint e) { real_t inv_length = real_t(1) / GetLength(e); return TinyObjPoint(e.x * inv_length, e.y * inv_length, e.z * inv_length); } inline TinyObjPoint WorldToLocal(const TinyObjPoint &a, const TinyObjPoint &u, const TinyObjPoint &v, const TinyObjPoint &w) { return TinyObjPoint(dot(a, u), dot(a, v), dot(a, w)); } // TODO(syoyo): refactor function. static bool exportGroupsToShape(shape_t *shape, const PrimGroup &prim_group, const std::vector &tags, const int material_id, const std::string &name, bool triangulate, const std::vector &v, std::string *warn) { if (prim_group.IsEmpty()) { return false; } shape->name = name; // polygon if (!prim_group.faceGroup.empty()) { // Flatten vertices and indices for (size_t i = 0; i < prim_group.faceGroup.size(); i++) { const face_t &face = prim_group.faceGroup[i]; size_t npolys = face.vertex_indices.size(); if (npolys < 3) { // Face must have 3+ vertices. if (warn) { (*warn) += "Degenerated face found\n."; } continue; } if (triangulate && npolys != 3) { if (npolys == 4) { vertex_index_t i0 = face.vertex_indices[0]; vertex_index_t i1 = face.vertex_indices[1]; vertex_index_t i2 = face.vertex_indices[2]; vertex_index_t i3 = face.vertex_indices[3]; size_t vi0 = size_t(i0.v_idx); size_t vi1 = size_t(i1.v_idx); size_t vi2 = size_t(i2.v_idx); size_t vi3 = size_t(i3.v_idx); if (((3 * vi0 + 2) >= v.size()) || ((3 * vi1 + 2) >= v.size()) || ((3 * vi2 + 2) >= v.size()) || ((3 * vi3 + 2) >= v.size())) { // Invalid triangle. // FIXME(syoyo): Is it ok to simply skip this invalid triangle? if (warn) { (*warn) += "Face with invalid vertex index found.\n"; } continue; } real_t v0x = v[vi0 * 3 + 0]; real_t v0y = v[vi0 * 3 + 1]; real_t v0z = v[vi0 * 3 + 2]; real_t v1x = v[vi1 * 3 + 0]; real_t v1y = v[vi1 * 3 + 1]; real_t v1z = v[vi1 * 3 + 2]; real_t v2x = v[vi2 * 3 + 0]; real_t v2y = v[vi2 * 3 + 1]; real_t v2z = v[vi2 * 3 + 2]; real_t v3x = v[vi3 * 3 + 0]; real_t v3y = v[vi3 * 3 + 1]; real_t v3z = v[vi3 * 3 + 2]; // There are two candidates to split the quad into two triangles. // // Choose the shortest edge. // TODO: Is it better to determine the edge to split by calculating // the area of each triangle? // // +---+ // |\ | // | \ | // | \| // +---+ // // +---+ // | /| // | / | // |/ | // +---+ real_t e02x = v2x - v0x; real_t e02y = v2y - v0y; real_t e02z = v2z - v0z; real_t e13x = v3x - v1x; real_t e13y = v3y - v1y; real_t e13z = v3z - v1z; real_t sqr02 = e02x * e02x + e02y * e02y + e02z * e02z; real_t sqr13 = e13x * e13x + e13y * e13y + e13z * e13z; index_t idx0, idx1, idx2, idx3; idx0.vertex_index = i0.v_idx; idx0.normal_index = i0.vn_idx; idx0.texcoord_index = i0.vt_idx; idx1.vertex_index = i1.v_idx; idx1.normal_index = i1.vn_idx; idx1.texcoord_index = i1.vt_idx; idx2.vertex_index = i2.v_idx; idx2.normal_index = i2.vn_idx; idx2.texcoord_index = i2.vt_idx; idx3.vertex_index = i3.v_idx; idx3.normal_index = i3.vn_idx; idx3.texcoord_index = i3.vt_idx; if (sqr02 < sqr13) { // [0, 1, 2], [0, 2, 3] shape->mesh.indices.push_back(idx0); shape->mesh.indices.push_back(idx1); shape->mesh.indices.push_back(idx2); shape->mesh.indices.push_back(idx0); shape->mesh.indices.push_back(idx2); shape->mesh.indices.push_back(idx3); } else { // [0, 1, 3], [1, 2, 3] shape->mesh.indices.push_back(idx0); shape->mesh.indices.push_back(idx1); shape->mesh.indices.push_back(idx3); shape->mesh.indices.push_back(idx1); shape->mesh.indices.push_back(idx2); shape->mesh.indices.push_back(idx3); } // Two triangle faces shape->mesh.num_face_vertices.push_back(3); shape->mesh.num_face_vertices.push_back(3); shape->mesh.material_ids.push_back(material_id); shape->mesh.material_ids.push_back(material_id); shape->mesh.smoothing_group_ids.push_back(face.smoothing_group_id); shape->mesh.smoothing_group_ids.push_back(face.smoothing_group_id); } else { #ifdef TINYOBJLOADER_USE_MAPBOX_EARCUT vertex_index_t i0 = face.vertex_indices[0]; vertex_index_t i0_2 = i0; // TMW change: Find the normal axis of the polygon using Newell's // method TinyObjPoint n; for (size_t k = 0; k < npolys; ++k) { i0 = face.vertex_indices[k % npolys]; size_t vi0 = size_t(i0.v_idx); size_t j = (k + 1) % npolys; i0_2 = face.vertex_indices[j]; size_t vi0_2 = size_t(i0_2.v_idx); real_t v0x = v[vi0 * 3 + 0]; real_t v0y = v[vi0 * 3 + 1]; real_t v0z = v[vi0 * 3 + 2]; real_t v0x_2 = v[vi0_2 * 3 + 0]; real_t v0y_2 = v[vi0_2 * 3 + 1]; real_t v0z_2 = v[vi0_2 * 3 + 2]; const TinyObjPoint point1(v0x, v0y, v0z); const TinyObjPoint point2(v0x_2, v0y_2, v0z_2); TinyObjPoint a(point1.x - point2.x, point1.y - point2.y, point1.z - point2.z); TinyObjPoint b(point1.x + point2.x, point1.y + point2.y, point1.z + point2.z); n.x += (a.y * b.z); n.y += (a.z * b.x); n.z += (a.x * b.y); } real_t length_n = GetLength(n); // Check if zero length normal if (length_n <= 0) { continue; } // Negative is to flip the normal to the correct direction real_t inv_length = -real_t(1.0) / length_n; n.x *= inv_length; n.y *= inv_length; n.z *= inv_length; TinyObjPoint axis_w, axis_v, axis_u; axis_w = n; TinyObjPoint a; if (std::fabs(axis_w.x) > real_t(0.9999999)) { a = TinyObjPoint(0, 1, 0); } else { a = TinyObjPoint(1, 0, 0); } axis_v = Normalize(cross(axis_w, a)); axis_u = cross(axis_w, axis_v); using Point = std::array; // first polyline define the main polygon. // following polylines define holes(not used in tinyobj). std::vector > polygon; std::vector polyline; // TMW change: Find best normal and project v0x and v0y to those // coordinates, instead of picking a plane aligned with an axis (which // can flip polygons). // Fill polygon data(facevarying vertices). for (size_t k = 0; k < npolys; k++) { i0 = face.vertex_indices[k]; size_t vi0 = size_t(i0.v_idx); assert(((3 * vi0 + 2) < v.size())); real_t v0x = v[vi0 * 3 + 0]; real_t v0y = v[vi0 * 3 + 1]; real_t v0z = v[vi0 * 3 + 2]; TinyObjPoint polypoint(v0x, v0y, v0z); TinyObjPoint loc = WorldToLocal(polypoint, axis_u, axis_v, axis_w); polyline.push_back({loc.x, loc.y}); } polygon.push_back(polyline); std::vector indices = mapbox::earcut(polygon); // => result = 3 * faces, clockwise assert(indices.size() % 3 == 0); // Reconstruct vertex_index_t for (size_t k = 0; k < indices.size() / 3; k++) { { index_t idx0, idx1, idx2; idx0.vertex_index = face.vertex_indices[indices[3 * k + 0]].v_idx; idx0.normal_index = face.vertex_indices[indices[3 * k + 0]].vn_idx; idx0.texcoord_index = face.vertex_indices[indices[3 * k + 0]].vt_idx; idx1.vertex_index = face.vertex_indices[indices[3 * k + 1]].v_idx; idx1.normal_index = face.vertex_indices[indices[3 * k + 1]].vn_idx; idx1.texcoord_index = face.vertex_indices[indices[3 * k + 1]].vt_idx; idx2.vertex_index = face.vertex_indices[indices[3 * k + 2]].v_idx; idx2.normal_index = face.vertex_indices[indices[3 * k + 2]].vn_idx; idx2.texcoord_index = face.vertex_indices[indices[3 * k + 2]].vt_idx; shape->mesh.indices.push_back(idx0); shape->mesh.indices.push_back(idx1); shape->mesh.indices.push_back(idx2); shape->mesh.num_face_vertices.push_back(3); shape->mesh.material_ids.push_back(material_id); shape->mesh.smoothing_group_ids.push_back( face.smoothing_group_id); } } #else // Built-in ear clipping triangulation vertex_index_t i0 = face.vertex_indices[0]; vertex_index_t i1(-1); vertex_index_t i2 = face.vertex_indices[1]; // find the two axes to work in size_t axes[2] = {1, 2}; for (size_t k = 0; k < npolys; ++k) { i0 = face.vertex_indices[(k + 0) % npolys]; i1 = face.vertex_indices[(k + 1) % npolys]; i2 = face.vertex_indices[(k + 2) % npolys]; size_t vi0 = size_t(i0.v_idx); size_t vi1 = size_t(i1.v_idx); size_t vi2 = size_t(i2.v_idx); if (((3 * vi0 + 2) >= v.size()) || ((3 * vi1 + 2) >= v.size()) || ((3 * vi2 + 2) >= v.size())) { // Invalid triangle. // FIXME(syoyo): Is it ok to simply skip this invalid triangle? continue; } real_t v0x = v[vi0 * 3 + 0]; real_t v0y = v[vi0 * 3 + 1]; real_t v0z = v[vi0 * 3 + 2]; real_t v1x = v[vi1 * 3 + 0]; real_t v1y = v[vi1 * 3 + 1]; real_t v1z = v[vi1 * 3 + 2]; real_t v2x = v[vi2 * 3 + 0]; real_t v2y = v[vi2 * 3 + 1]; real_t v2z = v[vi2 * 3 + 2]; real_t e0x = v1x - v0x; real_t e0y = v1y - v0y; real_t e0z = v1z - v0z; real_t e1x = v2x - v1x; real_t e1y = v2y - v1y; real_t e1z = v2z - v1z; real_t cx = std::fabs(e0y * e1z - e0z * e1y); real_t cy = std::fabs(e0z * e1x - e0x * e1z); real_t cz = std::fabs(e0x * e1y - e0y * e1x); const real_t epsilon = std::numeric_limits::epsilon(); // std::cout << "cx " << cx << ", cy " << cy << ", cz " << cz << // "\n"; if (cx > epsilon || cy > epsilon || cz > epsilon) { // std::cout << "corner\n"; // found a corner if (cx > cy && cx > cz) { // std::cout << "pattern0\n"; } else { // std::cout << "axes[0] = 0\n"; axes[0] = 0; if (cz > cx && cz > cy) { // std::cout << "axes[1] = 1\n"; axes[1] = 1; } } break; } } face_t remainingFace = face; // copy size_t guess_vert = 0; vertex_index_t ind[3]; real_t vx[3]; real_t vy[3]; // How many iterations can we do without decreasing the remaining // vertices. size_t remainingIterations = face.vertex_indices.size(); size_t previousRemainingVertices = remainingFace.vertex_indices.size(); while (remainingFace.vertex_indices.size() > 3 && remainingIterations > 0) { // std::cout << "remainingIterations " << remainingIterations << // "\n"; npolys = remainingFace.vertex_indices.size(); if (guess_vert >= npolys) { guess_vert -= npolys; } if (previousRemainingVertices != npolys) { // The number of remaining vertices decreased. Reset counters. previousRemainingVertices = npolys; remainingIterations = npolys; } else { // We didn't consume a vertex on previous iteration, reduce the // available iterations. remainingIterations--; } for (size_t k = 0; k < 3; k++) { ind[k] = remainingFace.vertex_indices[(guess_vert + k) % npolys]; size_t vi = size_t(ind[k].v_idx); if (((vi * 3 + axes[0]) >= v.size()) || ((vi * 3 + axes[1]) >= v.size())) { // ??? vx[k] = static_cast(0.0); vy[k] = static_cast(0.0); } else { vx[k] = v[vi * 3 + axes[0]]; vy[k] = v[vi * 3 + axes[1]]; } } // // area is calculated per face // real_t e0x = vx[1] - vx[0]; real_t e0y = vy[1] - vy[0]; real_t e1x = vx[2] - vx[1]; real_t e1y = vy[2] - vy[1]; real_t cross = e0x * e1y - e0y * e1x; // std::cout << "axes = " << axes[0] << ", " << axes[1] << "\n"; // std::cout << "e0x, e0y, e1x, e1y " << e0x << ", " << e0y << ", " // << e1x << ", " << e1y << "\n"; real_t area = (vx[0] * vy[1] - vy[0] * vx[1]) * static_cast(0.5); // std::cout << "cross " << cross << ", area " << area << "\n"; // if an internal angle if (cross * area < static_cast(0.0)) { // std::cout << "internal \n"; guess_vert += 1; // std::cout << "guess vert : " << guess_vert << "\n"; continue; } // check all other verts in case they are inside this triangle bool overlap = false; for (size_t otherVert = 3; otherVert < npolys; ++otherVert) { size_t idx = (guess_vert + otherVert) % npolys; if (idx >= remainingFace.vertex_indices.size()) { // std::cout << "???0\n"; // ??? continue; } size_t ovi = size_t(remainingFace.vertex_indices[idx].v_idx); if (((ovi * 3 + axes[0]) >= v.size()) || ((ovi * 3 + axes[1]) >= v.size())) { // std::cout << "???1\n"; // ??? continue; } real_t tx = v[ovi * 3 + axes[0]]; real_t ty = v[ovi * 3 + axes[1]]; if (pnpoly(3, vx, vy, tx, ty)) { // std::cout << "overlap\n"; overlap = true; break; } } if (overlap) { // std::cout << "overlap2\n"; guess_vert += 1; continue; } // this triangle is an ear { index_t idx0, idx1, idx2; idx0.vertex_index = ind[0].v_idx; idx0.normal_index = ind[0].vn_idx; idx0.texcoord_index = ind[0].vt_idx; idx1.vertex_index = ind[1].v_idx; idx1.normal_index = ind[1].vn_idx; idx1.texcoord_index = ind[1].vt_idx; idx2.vertex_index = ind[2].v_idx; idx2.normal_index = ind[2].vn_idx; idx2.texcoord_index = ind[2].vt_idx; shape->mesh.indices.push_back(idx0); shape->mesh.indices.push_back(idx1); shape->mesh.indices.push_back(idx2); shape->mesh.num_face_vertices.push_back(3); shape->mesh.material_ids.push_back(material_id); shape->mesh.smoothing_group_ids.push_back( face.smoothing_group_id); } // remove v1 from the list size_t removed_vert_index = (guess_vert + 1) % npolys; while (removed_vert_index + 1 < npolys) { remainingFace.vertex_indices[removed_vert_index] = remainingFace.vertex_indices[removed_vert_index + 1]; removed_vert_index += 1; } remainingFace.vertex_indices.pop_back(); } // std::cout << "remainingFace.vi.size = " << // remainingFace.vertex_indices.size() << "\n"; if (remainingFace.vertex_indices.size() == 3) { i0 = remainingFace.vertex_indices[0]; i1 = remainingFace.vertex_indices[1]; i2 = remainingFace.vertex_indices[2]; { index_t idx0, idx1, idx2; idx0.vertex_index = i0.v_idx; idx0.normal_index = i0.vn_idx; idx0.texcoord_index = i0.vt_idx; idx1.vertex_index = i1.v_idx; idx1.normal_index = i1.vn_idx; idx1.texcoord_index = i1.vt_idx; idx2.vertex_index = i2.v_idx; idx2.normal_index = i2.vn_idx; idx2.texcoord_index = i2.vt_idx; shape->mesh.indices.push_back(idx0); shape->mesh.indices.push_back(idx1); shape->mesh.indices.push_back(idx2); shape->mesh.num_face_vertices.push_back(3); shape->mesh.material_ids.push_back(material_id); shape->mesh.smoothing_group_ids.push_back( face.smoothing_group_id); } } #endif } // npolys } else { for (size_t k = 0; k < npolys; k++) { index_t idx; idx.vertex_index = face.vertex_indices[k].v_idx; idx.normal_index = face.vertex_indices[k].vn_idx; idx.texcoord_index = face.vertex_indices[k].vt_idx; shape->mesh.indices.push_back(idx); } shape->mesh.num_face_vertices.push_back( static_cast(npolys)); shape->mesh.material_ids.push_back(material_id); // per face shape->mesh.smoothing_group_ids.push_back( face.smoothing_group_id); // per face } } shape->mesh.tags = tags; } // line if (!prim_group.lineGroup.empty()) { // Flatten indices for (size_t i = 0; i < prim_group.lineGroup.size(); i++) { for (size_t j = 0; j < prim_group.lineGroup[i].vertex_indices.size(); j++) { const vertex_index_t &vi = prim_group.lineGroup[i].vertex_indices[j]; index_t idx; idx.vertex_index = vi.v_idx; idx.normal_index = vi.vn_idx; idx.texcoord_index = vi.vt_idx; shape->lines.indices.push_back(idx); } shape->lines.num_line_vertices.push_back( int(prim_group.lineGroup[i].vertex_indices.size())); } } // points if (!prim_group.pointsGroup.empty()) { // Flatten & convert indices for (size_t i = 0; i < prim_group.pointsGroup.size(); i++) { for (size_t j = 0; j < prim_group.pointsGroup[i].vertex_indices.size(); j++) { const vertex_index_t &vi = prim_group.pointsGroup[i].vertex_indices[j]; index_t idx; idx.vertex_index = vi.v_idx; idx.normal_index = vi.vn_idx; idx.texcoord_index = vi.vt_idx; shape->points.indices.push_back(idx); } } } return true; } // Split a string with specified delimiter character and escape character. // https://rosettacode.org/wiki/Tokenize_a_string_with_escaping#C.2B.2B static void SplitString(const std::string &s, char delim, char escape, std::vector &elems) { std::string token; bool escaping = false; for (size_t i = 0; i < s.size(); ++i) { char ch = s[i]; if (escaping) { escaping = false; } else if (ch == escape) { escaping = true; continue; } else if (ch == delim) { if (!token.empty()) { elems.push_back(token); } token.clear(); continue; } token += ch; } elems.push_back(token); } static std::string JoinPath(const std::string &dir, const std::string &filename) { if (dir.empty()) { return filename; } else { // check '/' char lastChar = *dir.rbegin(); if (lastChar != '/') { return dir + std::string("/") + filename; } else { return dir + filename; } } } void LoadMtl(std::map *material_map, std::vector *materials, std::istream *inStream, std::string *warning, std::string *err) { (void)err; // Create a default material anyway. material_t material; InitMaterial(&material); // Issue 43. `d` wins against `Tr` since `Tr` is not in the MTL specification. bool has_d = false; bool has_tr = false; // has_kd is used to set a default diffuse value when map_Kd is present // and Kd is not. bool has_kd = false; std::stringstream warn_ss; size_t line_no = 0; std::string linebuf; while (inStream->peek() != -1) { safeGetline(*inStream, linebuf); line_no++; // Trim trailing whitespace. if (linebuf.size() > 0) { linebuf = linebuf.substr(0, linebuf.find_last_not_of(" \t") + 1); } // Trim newline '\r\n' or '\n' if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\n') linebuf.erase(linebuf.size() - 1); } if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\r') linebuf.erase(linebuf.size() - 1); } // Skip if empty line. if (linebuf.empty()) { continue; } // Skip leading space. const char *token = linebuf.c_str(); token += strspn(token, " \t"); assert(token); if (token[0] == '\0') continue; // empty line if (token[0] == '#') continue; // comment line // new mtl if ((0 == strncmp(token, "newmtl", 6)) && IS_SPACE((token[6]))) { // flush previous material. if (!material.name.empty()) { material_map->insert(std::pair( material.name, static_cast(materials->size()))); materials->push_back(material); } // initial temporary material InitMaterial(&material); has_d = false; has_tr = false; has_kd = false; // set new mtl name token += 7; { std::string namebuf = parseString(&token); // TODO: empty name check? if (namebuf.empty()) { if (warning) { (*warning) += "empty material name in `newmtl`\n"; } } material.name = namebuf; } continue; } // ambient if (token[0] == 'K' && token[1] == 'a' && IS_SPACE((token[2]))) { token += 2; real_t r, g, b; parseReal3(&r, &g, &b, &token); material.ambient[0] = r; material.ambient[1] = g; material.ambient[2] = b; continue; } // diffuse if (token[0] == 'K' && token[1] == 'd' && IS_SPACE((token[2]))) { token += 2; real_t r, g, b; parseReal3(&r, &g, &b, &token); material.diffuse[0] = r; material.diffuse[1] = g; material.diffuse[2] = b; has_kd = true; continue; } // specular if (token[0] == 'K' && token[1] == 's' && IS_SPACE((token[2]))) { token += 2; real_t r, g, b; parseReal3(&r, &g, &b, &token); material.specular[0] = r; material.specular[1] = g; material.specular[2] = b; continue; } // transmittance if ((token[0] == 'K' && token[1] == 't' && IS_SPACE((token[2]))) || (token[0] == 'T' && token[1] == 'f' && IS_SPACE((token[2])))) { token += 2; real_t r, g, b; parseReal3(&r, &g, &b, &token); material.transmittance[0] = r; material.transmittance[1] = g; material.transmittance[2] = b; continue; } // ior(index of refraction) if (token[0] == 'N' && token[1] == 'i' && IS_SPACE((token[2]))) { token += 2; material.ior = parseReal(&token); continue; } // emission if (token[0] == 'K' && token[1] == 'e' && IS_SPACE(token[2])) { token += 2; real_t r, g, b; parseReal3(&r, &g, &b, &token); material.emission[0] = r; material.emission[1] = g; material.emission[2] = b; continue; } // shininess if (token[0] == 'N' && token[1] == 's' && IS_SPACE(token[2])) { token += 2; material.shininess = parseReal(&token); continue; } // illum model if (0 == strncmp(token, "illum", 5) && IS_SPACE(token[5])) { token += 6; material.illum = parseInt(&token); continue; } // dissolve if ((token[0] == 'd' && IS_SPACE(token[1]))) { token += 1; material.dissolve = parseReal(&token); if (has_tr) { warn_ss << "Both `d` and `Tr` parameters defined for \"" << material.name << "\". Use the value of `d` for dissolve (line " << line_no << " in .mtl.)\n"; } has_d = true; continue; } if (token[0] == 'T' && token[1] == 'r' && IS_SPACE(token[2])) { token += 2; if (has_d) { // `d` wins. Ignore `Tr` value. warn_ss << "Both `d` and `Tr` parameters defined for \"" << material.name << "\". Use the value of `d` for dissolve (line " << line_no << " in .mtl.)\n"; } else { // We invert value of Tr(assume Tr is in range [0, 1]) // NOTE: Interpretation of Tr is application(exporter) dependent. For // some application(e.g. 3ds max obj exporter), Tr = d(Issue 43) material.dissolve = static_cast(1.0) - parseReal(&token); } has_tr = true; continue; } // PBR: roughness if (token[0] == 'P' && token[1] == 'r' && IS_SPACE(token[2])) { token += 2; material.roughness = parseReal(&token); continue; } // PBR: metallic if (token[0] == 'P' && token[1] == 'm' && IS_SPACE(token[2])) { token += 2; material.metallic = parseReal(&token); continue; } // PBR: sheen if (token[0] == 'P' && token[1] == 's' && IS_SPACE(token[2])) { token += 2; material.sheen = parseReal(&token); continue; } // PBR: clearcoat thickness if (token[0] == 'P' && token[1] == 'c' && IS_SPACE(token[2])) { token += 2; material.clearcoat_thickness = parseReal(&token); continue; } // PBR: clearcoat roughness if ((0 == strncmp(token, "Pcr", 3)) && IS_SPACE(token[3])) { token += 4; material.clearcoat_roughness = parseReal(&token); continue; } // PBR: anisotropy if ((0 == strncmp(token, "aniso", 5)) && IS_SPACE(token[5])) { token += 6; material.anisotropy = parseReal(&token); continue; } // PBR: anisotropy rotation if ((0 == strncmp(token, "anisor", 6)) && IS_SPACE(token[6])) { token += 7; material.anisotropy_rotation = parseReal(&token); continue; } // ambient or ambient occlusion texture if ((0 == strncmp(token, "map_Ka", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.ambient_texname), &(material.ambient_texopt), token); continue; } // diffuse texture if ((0 == strncmp(token, "map_Kd", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.diffuse_texname), &(material.diffuse_texopt), token); // Set a decent diffuse default value if a diffuse texture is specified // without a matching Kd value. if (!has_kd) { material.diffuse[0] = static_cast(0.6); material.diffuse[1] = static_cast(0.6); material.diffuse[2] = static_cast(0.6); } continue; } // specular texture if ((0 == strncmp(token, "map_Ks", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.specular_texname), &(material.specular_texopt), token); continue; } // specular highlight texture if ((0 == strncmp(token, "map_Ns", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.specular_highlight_texname), &(material.specular_highlight_texopt), token); continue; } // bump texture if (((0 == strncmp(token, "map_bump", 8)) || (0 == strncmp(token, "map_Bump", 8))) && IS_SPACE(token[8])) { token += 9; ParseTextureNameAndOption(&(material.bump_texname), &(material.bump_texopt), token); continue; } // bump texture if ((0 == strncmp(token, "bump", 4)) && IS_SPACE(token[4])) { token += 5; ParseTextureNameAndOption(&(material.bump_texname), &(material.bump_texopt), token); continue; } // alpha texture if ((0 == strncmp(token, "map_d", 5)) && IS_SPACE(token[5])) { token += 6; material.alpha_texname = token; ParseTextureNameAndOption(&(material.alpha_texname), &(material.alpha_texopt), token); continue; } // displacement texture if (((0 == strncmp(token, "map_disp", 8)) || (0 == strncmp(token, "map_Disp", 8))) && IS_SPACE(token[8])) { token += 9; ParseTextureNameAndOption(&(material.displacement_texname), &(material.displacement_texopt), token); continue; } // displacement texture if ((0 == strncmp(token, "disp", 4)) && IS_SPACE(token[4])) { token += 5; ParseTextureNameAndOption(&(material.displacement_texname), &(material.displacement_texopt), token); continue; } // reflection map if ((0 == strncmp(token, "refl", 4)) && IS_SPACE(token[4])) { token += 5; ParseTextureNameAndOption(&(material.reflection_texname), &(material.reflection_texopt), token); continue; } // PBR: roughness texture if ((0 == strncmp(token, "map_Pr", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.roughness_texname), &(material.roughness_texopt), token); continue; } // PBR: metallic texture if ((0 == strncmp(token, "map_Pm", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.metallic_texname), &(material.metallic_texopt), token); continue; } // PBR: sheen texture if ((0 == strncmp(token, "map_Ps", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.sheen_texname), &(material.sheen_texopt), token); continue; } // PBR: emissive texture if ((0 == strncmp(token, "map_Ke", 6)) && IS_SPACE(token[6])) { token += 7; ParseTextureNameAndOption(&(material.emissive_texname), &(material.emissive_texopt), token); continue; } // PBR: normal map texture if ((0 == strncmp(token, "norm", 4)) && IS_SPACE(token[4])) { token += 5; ParseTextureNameAndOption(&(material.normal_texname), &(material.normal_texopt), token); continue; } // unknown parameter const char *_space = strchr(token, ' '); if (!_space) { _space = strchr(token, '\t'); } if (_space) { std::ptrdiff_t len = _space - token; std::string key(token, static_cast(len)); std::string value = _space + 1; material.unknown_parameter.insert( std::pair(key, value)); } } // flush last material. material_map->insert(std::pair( material.name, static_cast(materials->size()))); materials->push_back(material); if (warning) { (*warning) = warn_ss.str(); } } bool MaterialFileReader::operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, std::string *err) { if (!m_mtlBaseDir.empty()) { #ifdef _WIN32 char sep = ';'; #else char sep = ':'; #endif // https://stackoverflow.com/questions/5167625/splitting-a-c-stdstring-using-tokens-e-g std::vector paths; std::istringstream f(m_mtlBaseDir); std::string s; while (getline(f, s, sep)) { paths.push_back(s); } for (size_t i = 0; i < paths.size(); i++) { std::string filepath = JoinPath(paths[i], matId); std::ifstream matIStream(filepath.c_str()); if (matIStream) { LoadMtl(matMap, materials, &matIStream, warn, err); return true; } } std::stringstream ss; ss << "Material file [ " << matId << " ] not found in a path : " << m_mtlBaseDir << "\n"; if (warn) { (*warn) += ss.str(); } return false; } else { std::string filepath = matId; std::ifstream matIStream(filepath.c_str()); if (matIStream) { LoadMtl(matMap, materials, &matIStream, warn, err); return true; } std::stringstream ss; ss << "Material file [ " << filepath << " ] not found in a path : " << m_mtlBaseDir << "\n"; if (warn) { (*warn) += ss.str(); } return false; } } bool MaterialStreamReader::operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, std::string *err) { (void)err; (void)matId; if (!m_inStream) { std::stringstream ss; ss << "Material stream in error state. \n"; if (warn) { (*warn) += ss.str(); } return false; } LoadMtl(matMap, materials, &m_inStream, warn, err); return true; } bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector *materials, std::string *warn, std::string *err, const char *filename, const char *mtl_basedir, bool triangulate, bool default_vcols_fallback) { attrib->vertices.clear(); attrib->normals.clear(); attrib->texcoords.clear(); attrib->colors.clear(); shapes->clear(); std::stringstream errss; std::ifstream ifs(filename); if (!ifs) { errss << "Cannot open file [" << filename << "]\n"; if (err) { (*err) = errss.str(); } return false; } std::string baseDir = mtl_basedir ? mtl_basedir : ""; if (!baseDir.empty()) { #ifndef _WIN32 const char dirsep = '/'; #else const char dirsep = '\\'; #endif if (baseDir[baseDir.length() - 1] != dirsep) baseDir += dirsep; } MaterialFileReader matFileReader(baseDir); return LoadObj(attrib, shapes, materials, warn, err, &ifs, &matFileReader, triangulate, default_vcols_fallback); } bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector *materials, std::string *warn, std::string *err, std::istream *inStream, MaterialReader *readMatFn /*= NULL*/, bool triangulate, bool default_vcols_fallback) { std::stringstream errss; std::vector v; std::vector vertex_weights; // optional [w] component in `v` std::vector vn; std::vector vt; std::vector vc; std::vector vw; // tinyobj extension: vertex skin weights std::vector tags; PrimGroup prim_group; std::string name; // material std::set material_filenames; std::map material_map; int material = -1; // smoothing group id unsigned int current_smoothing_id = 0; // Initial value. 0 means no smoothing. int greatest_v_idx = -1; int greatest_vn_idx = -1; int greatest_vt_idx = -1; shape_t shape; bool found_all_colors = true; // check if all 'v' line has color info size_t line_num = 0; std::string linebuf; while (inStream->peek() != -1) { safeGetline(*inStream, linebuf); line_num++; // Trim newline '\r\n' or '\n' if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\n') linebuf.erase(linebuf.size() - 1); } if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\r') linebuf.erase(linebuf.size() - 1); } // Skip if empty line. if (linebuf.empty()) { continue; } // Skip leading space. const char *token = linebuf.c_str(); token += strspn(token, " \t"); assert(token); if (token[0] == '\0') continue; // empty line if (token[0] == '#') continue; // comment line // vertex if (token[0] == 'v' && IS_SPACE((token[1]))) { token += 2; real_t x, y, z; real_t r, g, b; int num_components = parseVertexWithColor(&x, &y, &z, &r, &g, &b, &token); found_all_colors &= (num_components == 6); v.push_back(x); v.push_back(y); v.push_back(z); vertex_weights.push_back( r); // r = w, and initialized to 1.0 when `w` component is not found. if ((num_components == 6) || default_vcols_fallback) { vc.push_back(r); vc.push_back(g); vc.push_back(b); } continue; } // normal if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { token += 3; real_t x, y, z; parseReal3(&x, &y, &z, &token); vn.push_back(x); vn.push_back(y); vn.push_back(z); continue; } // texcoord if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { token += 3; real_t x, y; parseReal2(&x, &y, &token); vt.push_back(x); vt.push_back(y); continue; } // skin weight. tinyobj extension if (token[0] == 'v' && token[1] == 'w' && IS_SPACE((token[2]))) { token += 3; // vw ... // example: // vw 0 0 0.25 1 0.25 2 0.5 // TODO(syoyo): Add syntax check int vid = 0; vid = parseInt(&token); skin_weight_t sw; sw.vertex_id = vid; while (!IS_NEW_LINE(token[0]) && token[0] != '#') { real_t j, w; // joint_id should not be negative, weight may be negative // TODO(syoyo): # of elements check parseReal2(&j, &w, &token, -1.0); if (j < static_cast(0)) { if (err) { std::stringstream ss; ss << "Failed parse `vw' line. joint_id is negative. " "line " << line_num << ".)\n"; (*err) += ss.str(); } return false; } joint_and_weight_t jw; jw.joint_id = int(j); jw.weight = w; sw.weightValues.push_back(jw); size_t n = strspn(token, " \t\r"); token += n; } vw.push_back(sw); } warning_context context; context.warn = warn; context.line_number = line_num; // line if (token[0] == 'l' && IS_SPACE((token[1]))) { token += 2; __line_t line; while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi; if (!parseTriple(&token, static_cast(v.size() / 3), static_cast(vn.size() / 3), static_cast(vt.size() / 2), &vi, context)) { if (err) { (*err) += "Failed to parse `l' line (e.g. a zero value for vertex index. " "Line " + toString(line_num) + ").\n"; } return false; } line.vertex_indices.push_back(vi); size_t n = strspn(token, " \t\r"); token += n; } prim_group.lineGroup.push_back(line); continue; } // points if (token[0] == 'p' && IS_SPACE((token[1]))) { token += 2; __points_t pts; while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi; if (!parseTriple(&token, static_cast(v.size() / 3), static_cast(vn.size() / 3), static_cast(vt.size() / 2), &vi, context)) { if (err) { (*err) += "Failed to parse `p' line (e.g. a zero value for vertex index. " "Line " + toString(line_num) + ").\n"; } return false; } pts.vertex_indices.push_back(vi); size_t n = strspn(token, " \t\r"); token += n; } prim_group.pointsGroup.push_back(pts); continue; } // face if (token[0] == 'f' && IS_SPACE((token[1]))) { token += 2; token += strspn(token, " \t"); face_t face; face.smoothing_group_id = current_smoothing_id; face.vertex_indices.reserve(3); while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi; if (!parseTriple(&token, static_cast(v.size() / 3), static_cast(vn.size() / 3), static_cast(vt.size() / 2), &vi, context)) { if (err) { (*err) += "Failed to parse `f' line (e.g. a zero value for vertex index " "or invalid relative vertex index). Line " + toString(line_num) + ").\n"; } return false; } greatest_v_idx = greatest_v_idx > vi.v_idx ? greatest_v_idx : vi.v_idx; greatest_vn_idx = greatest_vn_idx > vi.vn_idx ? greatest_vn_idx : vi.vn_idx; greatest_vt_idx = greatest_vt_idx > vi.vt_idx ? greatest_vt_idx : vi.vt_idx; face.vertex_indices.push_back(vi); size_t n = strspn(token, " \t\r"); token += n; } // replace with emplace_back + std::move on C++11 prim_group.faceGroup.push_back(face); continue; } // use mtl if ((0 == strncmp(token, "usemtl", 6))) { token += 6; std::string namebuf = parseString(&token); int newMaterialId = -1; std::map::const_iterator it = material_map.find(namebuf); if (it != material_map.end()) { newMaterialId = it->second; } else { // { error!! material not found } if (warn) { (*warn) += "material [ '" + namebuf + "' ] not found in .mtl\n"; } } if (newMaterialId != material) { // Create per-face material. Thus we don't add `shape` to `shapes` at // this time. // just clear `faceGroup` after `exportGroupsToShape()` call. exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); prim_group.faceGroup.clear(); material = newMaterialId; } continue; } // load mtl if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { if (readMatFn) { token += 7; std::vector filenames; SplitString(std::string(token), ' ', '\\', filenames); if (filenames.empty()) { if (warn) { std::stringstream ss; ss << "Looks like empty filename for mtllib. Use default " "material (line " << line_num << ".)\n"; (*warn) += ss.str(); } } else { bool found = false; for (size_t s = 0; s < filenames.size(); s++) { if (material_filenames.count(filenames[s]) > 0) { found = true; continue; } std::string warn_mtl; std::string err_mtl; bool ok = (*readMatFn)(filenames[s].c_str(), materials, &material_map, &warn_mtl, &err_mtl); if (warn && (!warn_mtl.empty())) { (*warn) += warn_mtl; } if (err && (!err_mtl.empty())) { (*err) += err_mtl; } if (ok) { found = true; material_filenames.insert(filenames[s]); break; } } if (!found) { if (warn) { (*warn) += "Failed to load material file(s). Use default " "material.\n"; } } } } continue; } // group name if (token[0] == 'g' && IS_SPACE((token[1]))) { // flush previous face group. bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); (void)ret; // return value not used. if (shape.mesh.indices.size() > 0) { shapes->push_back(shape); } shape = shape_t(); // material = -1; prim_group.clear(); std::vector names; while (!IS_NEW_LINE(token[0]) && token[0] != '#') { std::string str = parseString(&token); names.push_back(str); token += strspn(token, " \t\r"); // skip tag } // names[0] must be 'g' if (names.size() < 2) { // 'g' with empty names if (warn) { std::stringstream ss; ss << "Empty group name. line: " << line_num << "\n"; (*warn) += ss.str(); name = ""; } } else { std::stringstream ss; ss << names[1]; // tinyobjloader does not support multiple groups for a primitive. // Currently we concatinate multiple group names with a space to get // single group name. for (size_t i = 2; i < names.size(); i++) { ss << " " << names[i]; } name = ss.str(); } continue; } // object name if (token[0] == 'o' && IS_SPACE((token[1]))) { // flush previous face group. bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); (void)ret; // return value not used. if (shape.mesh.indices.size() > 0 || shape.lines.indices.size() > 0 || shape.points.indices.size() > 0) { shapes->push_back(shape); } // material = -1; prim_group.clear(); shape = shape_t(); // @todo { multiple object name? } token += 2; std::stringstream ss; ss << token; name = ss.str(); continue; } if (token[0] == 't' && IS_SPACE(token[1])) { const int max_tag_nums = 8192; // FIXME(syoyo): Parameterize. tag_t tag; token += 2; tag.name = parseString(&token); tag_sizes ts = parseTagTriple(&token); if (ts.num_ints < 0) { ts.num_ints = 0; } if (ts.num_ints > max_tag_nums) { ts.num_ints = max_tag_nums; } if (ts.num_reals < 0) { ts.num_reals = 0; } if (ts.num_reals > max_tag_nums) { ts.num_reals = max_tag_nums; } if (ts.num_strings < 0) { ts.num_strings = 0; } if (ts.num_strings > max_tag_nums) { ts.num_strings = max_tag_nums; } tag.intValues.resize(static_cast(ts.num_ints)); for (size_t i = 0; i < static_cast(ts.num_ints); ++i) { tag.intValues[i] = parseInt(&token); } tag.floatValues.resize(static_cast(ts.num_reals)); for (size_t i = 0; i < static_cast(ts.num_reals); ++i) { tag.floatValues[i] = parseReal(&token); } tag.stringValues.resize(static_cast(ts.num_strings)); for (size_t i = 0; i < static_cast(ts.num_strings); ++i) { tag.stringValues[i] = parseString(&token); } tags.push_back(tag); continue; } if (token[0] == 's' && IS_SPACE(token[1])) { // smoothing group id token += 2; // skip space. token += strspn(token, " \t"); // skip space if (token[0] == '\0') { continue; } if (token[0] == '\r' || token[1] == '\n') { continue; } if (strlen(token) >= 3 && token[0] == 'o' && token[1] == 'f' && token[2] == 'f') { current_smoothing_id = 0; } else { // assume number int smGroupId = parseInt(&token); if (smGroupId < 0) { // parse error. force set to 0. // FIXME(syoyo): Report warning. current_smoothing_id = 0; } else { current_smoothing_id = static_cast(smGroupId); } } continue; } // smoothing group id // Ignore unknown command. } // not all vertices have colors, no default colors desired? -> clear colors if (!found_all_colors && !default_vcols_fallback) { vc.clear(); } if (greatest_v_idx >= static_cast(v.size() / 3)) { if (warn) { std::stringstream ss; ss << "Vertex indices out of bounds (line " << line_num << ".)\n\n"; (*warn) += ss.str(); } } if (greatest_vn_idx >= static_cast(vn.size() / 3)) { if (warn) { std::stringstream ss; ss << "Vertex normal indices out of bounds (line " << line_num << ".)\n\n"; (*warn) += ss.str(); } } if (greatest_vt_idx >= static_cast(vt.size() / 2)) { if (warn) { std::stringstream ss; ss << "Vertex texcoord indices out of bounds (line " << line_num << ".)\n\n"; (*warn) += ss.str(); } } bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); // exportGroupsToShape return false when `usemtl` is called in the last // line. // we also add `shape` to `shapes` when `shape.mesh` has already some // faces(indices) if (ret || shape.mesh.indices .size()) { // FIXME(syoyo): Support other prims(e.g. lines) shapes->push_back(shape); } prim_group.clear(); // for safety if (err) { (*err) += errss.str(); } attrib->vertices.swap(v); attrib->vertex_weights.swap(vertex_weights); attrib->normals.swap(vn); attrib->texcoords.swap(vt); attrib->texcoord_ws.swap(vt); attrib->colors.swap(vc); attrib->skin_weights.swap(vw); return true; } bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, void *user_data /*= NULL*/, MaterialReader *readMatFn /*= NULL*/, std::string *warn, /* = NULL*/ std::string *err /*= NULL*/) { std::stringstream errss; // material std::set material_filenames; std::map material_map; int material_id = -1; // -1 = invalid std::vector indices; std::vector materials; std::vector names; names.reserve(2); std::vector names_out; std::string linebuf; while (inStream.peek() != -1) { safeGetline(inStream, linebuf); // Trim newline '\r\n' or '\n' if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\n') linebuf.erase(linebuf.size() - 1); } if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\r') linebuf.erase(linebuf.size() - 1); } // Skip if empty line. if (linebuf.empty()) { continue; } // Skip leading space. const char *token = linebuf.c_str(); token += strspn(token, " \t"); assert(token); if (token[0] == '\0') continue; // empty line if (token[0] == '#') continue; // comment line // vertex if (token[0] == 'v' && IS_SPACE((token[1]))) { token += 2; real_t x, y, z; real_t r, g, b; int num_components = parseVertexWithColor(&x, &y, &z, &r, &g, &b, &token); if (callback.vertex_cb) { callback.vertex_cb(user_data, x, y, z, r); // r=w is optional } if (callback.vertex_color_cb) { bool found_color = (num_components == 6); callback.vertex_color_cb(user_data, x, y, z, r, g, b, found_color); } continue; } // normal if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { token += 3; real_t x, y, z; parseReal3(&x, &y, &z, &token); if (callback.normal_cb) { callback.normal_cb(user_data, x, y, z); } continue; } // texcoord if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { token += 3; real_t x, y, z; // y and z are optional. default = 0.0 parseReal3(&x, &y, &z, &token); if (callback.texcoord_cb) { callback.texcoord_cb(user_data, x, y, z); } continue; } // face if (token[0] == 'f' && IS_SPACE((token[1]))) { token += 2; token += strspn(token, " \t"); indices.clear(); while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi = parseRawTriple(&token); index_t idx; idx.vertex_index = vi.v_idx; idx.normal_index = vi.vn_idx; idx.texcoord_index = vi.vt_idx; indices.push_back(idx); size_t n = strspn(token, " \t\r"); token += n; } if (callback.index_cb && indices.size() > 0) { callback.index_cb(user_data, &indices.at(0), static_cast(indices.size())); } continue; } // use mtl if ((0 == strncmp(token, "usemtl", 6)) && IS_SPACE((token[6]))) { token += 7; std::stringstream ss; ss << token; std::string namebuf = ss.str(); int newMaterialId = -1; std::map::const_iterator it = material_map.find(namebuf); if (it != material_map.end()) { newMaterialId = it->second; } else { // { warn!! material not found } if (warn && (!callback.usemtl_cb)) { (*warn) += "material [ " + namebuf + " ] not found in .mtl\n"; } } if (newMaterialId != material_id) { material_id = newMaterialId; } if (callback.usemtl_cb) { callback.usemtl_cb(user_data, namebuf.c_str(), material_id); } continue; } // load mtl if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { if (readMatFn) { token += 7; std::vector filenames; SplitString(std::string(token), ' ', '\\', filenames); if (filenames.empty()) { if (warn) { (*warn) += "Looks like empty filename for mtllib. Use default " "material. \n"; } } else { bool found = false; for (size_t s = 0; s < filenames.size(); s++) { if (material_filenames.count(filenames[s]) > 0) { found = true; continue; } std::string warn_mtl; std::string err_mtl; bool ok = (*readMatFn)(filenames[s].c_str(), &materials, &material_map, &warn_mtl, &err_mtl); if (warn && (!warn_mtl.empty())) { (*warn) += warn_mtl; // This should be warn message. } if (err && (!err_mtl.empty())) { (*err) += err_mtl; } if (ok) { found = true; material_filenames.insert(filenames[s]); break; } } if (!found) { if (warn) { (*warn) += "Failed to load material file(s). Use default " "material.\n"; } } else { if (callback.mtllib_cb) { callback.mtllib_cb(user_data, &materials.at(0), static_cast(materials.size())); } } } } continue; } // group name if (token[0] == 'g' && IS_SPACE((token[1]))) { names.clear(); while (!IS_NEW_LINE(token[0]) && token[0] != '#') { std::string str = parseString(&token); names.push_back(str); token += strspn(token, " \t\r"); // skip tag } assert(names.size() > 0); if (callback.group_cb) { if (names.size() > 1) { // create const char* array. names_out.resize(names.size() - 1); for (size_t j = 0; j < names_out.size(); j++) { names_out[j] = names[j + 1].c_str(); } callback.group_cb(user_data, &names_out.at(0), static_cast(names_out.size())); } else { callback.group_cb(user_data, NULL, 0); } } continue; } // object name if (token[0] == 'o' && IS_SPACE((token[1]))) { // @todo { multiple object name? } token += 2; std::stringstream ss; ss << token; std::string object_name = ss.str(); if (callback.object_cb) { callback.object_cb(user_data, object_name.c_str()); } continue; } #if 0 // @todo if (token[0] == 't' && IS_SPACE(token[1])) { tag_t tag; token += 2; std::stringstream ss; ss << token; tag.name = ss.str(); token += tag.name.size() + 1; tag_sizes ts = parseTagTriple(&token); tag.intValues.resize(static_cast(ts.num_ints)); for (size_t i = 0; i < static_cast(ts.num_ints); ++i) { tag.intValues[i] = atoi(token); token += strcspn(token, "/ \t\r") + 1; } tag.floatValues.resize(static_cast(ts.num_reals)); for (size_t i = 0; i < static_cast(ts.num_reals); ++i) { tag.floatValues[i] = parseReal(&token); token += strcspn(token, "/ \t\r") + 1; } tag.stringValues.resize(static_cast(ts.num_strings)); for (size_t i = 0; i < static_cast(ts.num_strings); ++i) { std::stringstream ss; ss << token; tag.stringValues[i] = ss.str(); token += tag.stringValues[i].size() + 1; } tags.push_back(tag); } #endif // Ignore unknown command. } if (err) { (*err) += errss.str(); } return true; } bool ObjReader::ParseFromFile(const std::string &filename, const ObjReaderConfig &config) { std::string mtl_search_path; if (config.mtl_search_path.empty()) { // // split at last '/'(for unixish system) or '\\'(for windows) to get // the base directory of .obj file // size_t pos = filename.find_last_of("/\\"); if (pos != std::string::npos) { mtl_search_path = filename.substr(0, pos); } } else { mtl_search_path = config.mtl_search_path; } valid_ = LoadObj(&attrib_, &shapes_, &materials_, &warning_, &error_, filename.c_str(), mtl_search_path.c_str(), config.triangulate, config.vertex_color); return valid_; } bool ObjReader::ParseFromString(const std::string &obj_text, const std::string &mtl_text, const ObjReaderConfig &config) { std::stringbuf obj_buf(obj_text); std::stringbuf mtl_buf(mtl_text); std::istream obj_ifs(&obj_buf); std::istream mtl_ifs(&mtl_buf); MaterialStreamReader mtl_ss(mtl_ifs); valid_ = LoadObj(&attrib_, &shapes_, &materials_, &warning_, &error_, &obj_ifs, &mtl_ss, config.triangulate, config.vertex_color); return valid_; } #ifdef __clang__ #pragma clang diagnostic pop #endif } // namespace tinyobj #endif bino-2.5/src/tools.cpp000066400000000000000000000036761475415313200147630ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #include #include #include #include #include "tools.hpp" bool IsOpenGLES; void initializeIsOpenGLES(const QSurfaceFormat& format) { IsOpenGLES = (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES || format.renderableType() == QSurfaceFormat::OpenGLES); } QString readFile(const char* fileName) { QFile f(fileName); f.open(QIODevice::ReadOnly); QTextStream in(&f); return in.readAll(); } bool checkTextureAnisotropicFilterAvailability() { return QOpenGLContext::currentContext()->hasExtension("GL_ARB_texture_filter_anisotropic") || QOpenGLContext::currentContext()->hasExtension("GL_EXT_texture_filter_anisotropic"); } const char* getOpenGLString(QOpenGLExtraFunctions* gl, GLenum p) { return reinterpret_cast(gl->glGetString(p)); } QString getExtension(const QString& fileName) { QString extension; int lastDot = fileName.lastIndexOf('.'); if (lastDot > 0) extension = (fileName.right(fileName.length() - lastDot - 1)).toLower(); return extension; } QString getExtension(const QUrl& url) { return getExtension(url.fileName()); } bino-2.5/src/tools.hpp000066400000000000000000000043141475415313200147560ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #pragma once #include #include #include #include // Global boolean variable that tells if the OpenGL flavor is OpenGL ES or desktop GL extern bool IsOpenGLES; void initializeIsOpenGLES(const QSurfaceFormat& format); // Read a complete file into a QString (without error checking; // intended to be used for resource files) QString readFile(const char* fileName); // Check for OpenGL errors #define CHECK_GL() \ { \ GLenum err = glGetError(); \ if (err != GL_NO_ERROR) { \ LOG_FATAL("%s:%d: OpenGL error 0x%04X in function %s", __FILE__, __LINE__, err, Q_FUNC_INFO); \ std::exit(1); \ } \ } // Check for existence of GL_ARB_texture_filter_anisotropic // or GL_EXT_texture_filter_anisotropic (which does the same) #ifndef GL_TEXTURE_MAX_ANISOTROPY # define GL_TEXTURE_MAX_ANISOTROPY 0x84FE #endif bool checkTextureAnisotropicFilterAvailability(); // Mipmap generation does not work on MacOS OpenGL 4.1, see https://github.com/marlam/bino/issues/25 // Simply disable all use of mipmaps as a crude workaround. #if __APPLE__ #undef GL_LINEAR_MIPMAP_LINEAR #define GL_LINEAR_MIPMAP_LINEAR GL_LINEAR #endif // Shortcut to get a string from OpenGL const char* getOpenGLString(QOpenGLExtraFunctions* gl, GLenum p); // Shortcut to get an extension from a file name QString getExtension(const QString& fileName); QString getExtension(const QUrl& url); bino-2.5/src/urlloader.cpp000066400000000000000000000025251475415313200156040ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2024 * Martin Lambers * * 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 . */ #include #include #include "urlloader.hpp" UrlLoader::UrlLoader(const QUrl& url) : _url(url), _done(false) { } UrlLoader::~UrlLoader() { } void UrlLoader::urlLoaded(QNetworkReply* reply) { _data = reply->readAll(); reply->deleteLater(); _done = true; } const QByteArray& UrlLoader::load() { connect(&_netAccMgr, SIGNAL(finished(QNetworkReply*)), this, SLOT(urlLoaded(QNetworkReply*))); QNetworkRequest request(_url); _netAccMgr.get(request); while (!_done) { QGuiApplication::processEvents(); } return _data; } bino-2.5/src/urlloader.hpp000066400000000000000000000022441475415313200156070ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2024 * Martin Lambers * * 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 . */ #pragma once #include #include #include #include class UrlLoader : public QObject { Q_OBJECT private: QUrl _url; QNetworkAccessManager _netAccMgr; QByteArray _data; bool _done; private slots: void urlLoaded(QNetworkReply* reply); public: UrlLoader(const QUrl& url); virtual ~UrlLoader(); const QByteArray& load(); }; bino-2.5/src/version.hpp000066400000000000000000000000331475415313200152750ustar00rootroot00000000000000#define BINO_VERSION "2.5" bino-2.5/src/videoframe.cpp000066400000000000000000000316721475415313200157410ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include "videoframe.hpp" #include "log.hpp" VideoFrame::VideoFrame() { update(Input_Unknown, Surround_Unknown, QVideoFrame(), false); } bool VideoFrame::isValid() const { return (qframe.isValid() && qframe.pixelFormat() != QVideoFrameFormat::Format_Invalid); } // from qtmultimedia/src/multimedia/shaders/qvideotexturehelper.cpp static float linearToPQ(float sig) { const float m1 = 1305.0f / 8192.0f; const float m2 = 2523.0f / 32.0f; const float c1 = 107.0f / 128.0f; const float c2 = 2413.0f / 128.0f; const float c3 = 2392.0f / 128.0f; const float SDR_LEVEL = 100.f; sig *= SDR_LEVEL / 10000.0f; float psig = powf(sig, m1); float num = c1 + c2 * psig; float den = 1.0f + c3 * psig; return powf(num / den, m2); } // from qtmultimedia/src/multimedia/shaders/qvideotexturehelper.cpp static float linearToHLG(float sig) { const float a = 0.17883277f; const float b = 0.28466892f; // = 1 - 4a const float c = 0.55991073f; // = 0.5 - a ln(4a) if (sig < 1.0f / 12.0f) return sqrtf(3.0f * sig); return a * logf(12.0f * sig - b) + c; } void VideoFrame::update(InputMode im, SurroundMode sm, const QVideoFrame& frame, bool newSrc) { if (qframe.isMapped()) qframe.unmap(); qframe = frame; if (isValid()) { subtitle = qframe.subtitleText(); width = qframe.width(); height = qframe.height(); aspectRatio = float(width) / height; LOG_FIREHOSE("videoframe receives new %dx%d frame with pixel format %s", width, height, qPrintable(QVideoFrameFormat::pixelFormatToString(qframe.pixelFormat()))); if (im == Input_Unknown) { if (aspectRatio >= 3.0f) im = Input_Left_Right; else if (aspectRatio < 1.0f) im = Input_Top_Bottom; else im = Input_Mono; LOG_FIREHOSE("videoframe guesses input mode from aspect ratio %g: %s", aspectRatio, inputModeToString(im)); } inputMode = im; if (sm == Surround_Unknown) { if (width == height && (inputMode == Input_Top_Bottom || inputMode == Input_Bottom_Top)) sm = Surround_360; else if (width == 2 * height && inputMode == Input_Mono) sm = Surround_360; else if (width == 2 * height && (inputMode == Input_Top_Bottom_Half || inputMode == Input_Bottom_Top_Half)) sm = Surround_360; else if (width == 2 * height && (inputMode == Input_Left_Right_Half || inputMode == Input_Right_Left_Half)) sm = Surround_360; else if (width == 4 * height && (inputMode == Input_Left_Right || inputMode == Input_Right_Left)) sm = Surround_360; else if (2 * width == height && (inputMode == Input_Top_Bottom || inputMode == Input_Bottom_Top)) sm = Surround_180; else if (width == height && inputMode == Input_Mono) sm = Surround_180; else if (width == height && (inputMode == Input_Top_Bottom_Half || inputMode == Input_Bottom_Top_Half)) sm = Surround_180; else if (width == height && (inputMode == Input_Left_Right_Half || inputMode == Input_Right_Left_Half)) sm = Surround_180; else if (width == 2 * height && (inputMode == Input_Left_Right || inputMode == Input_Right_Left)) sm = Surround_180; else sm = Surround_Off; LOG_FIREHOSE("videoframe guesses surround mode %s from frame size", surroundModeToString(sm)); } surroundMode = sm; bool fallbackToImage = true; if ( qframe.pixelFormat() == QVideoFrameFormat::Format_ARGB8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_ARGB8888_Premultiplied || qframe.pixelFormat() == QVideoFrameFormat::Format_XRGB8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_BGRA8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_BGRA8888_Premultiplied || qframe.pixelFormat() == QVideoFrameFormat::Format_BGRX8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_ABGR8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_XBGR8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_RGBA8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_RGBX8888 || qframe.pixelFormat() == QVideoFrameFormat::Format_YUV420P || qframe.pixelFormat() == QVideoFrameFormat::Format_YUV422P || qframe.pixelFormat() == QVideoFrameFormat::Format_YV12 || qframe.pixelFormat() == QVideoFrameFormat::Format_NV12 || qframe.pixelFormat() == QVideoFrameFormat::Format_P010 || qframe.pixelFormat() == QVideoFrameFormat::Format_P016 || qframe.pixelFormat() == QVideoFrameFormat::Format_Y8 || qframe.pixelFormat() == QVideoFrameFormat::Format_Y16) { fallbackToImage = false; } if (fallbackToImage) { if (newSrc) { LOG_WARNING("%s", qPrintable(tr("Pixel format %1 is not hardware accelerated!") .arg(QVideoFrameFormat::pixelFormatToString(qframe.pixelFormat())))); } storage = Storage_Image; pixelFormat = QVideoFrameFormat::pixelFormatFromImageFormat(QImage::Format_RGB32); colorRangeSmall = false; colorSpace = CS_AdobeRgb; image = qframe.toImage(); image.convertTo(QImage::Format_RGB32); } else { storage = Storage_Mapped; pixelFormat = qframe.pixelFormat(); // Heuristic used in qtmultimedia/src/multimedia/video/qvideotexturehelper.cpp: colorSpace = (qframe.surfaceFormat().frameHeight() > 576 ? CS_BT709 : CS_BT601); switch (qframe.surfaceFormat().colorSpace()) { case QVideoFrameFormat::ColorSpace_Undefined: // nothing to do: default was already guessed break; case QVideoFrameFormat::ColorSpace_BT601: colorSpace = CS_BT601; break; case QVideoFrameFormat::ColorSpace_BT709: colorSpace = CS_BT709; break; case QVideoFrameFormat::ColorSpace_AdobeRgb: colorSpace = CS_AdobeRgb; break; case QVideoFrameFormat::ColorSpace_BT2020: colorSpace = CS_BT2020; break; } colorRangeSmall = true; switch (qframe.surfaceFormat().colorRange()) { case QVideoFrameFormat::ColorRange_Unknown: case QVideoFrameFormat::ColorRange_Video: // nothing to do: default value already set break; case QVideoFrameFormat::ColorRange_Full: colorRangeSmall = false; break; } colorTransfer = CT_NOOP; // color was already transferred by color space conversion masteringWhite = 1.0f; switch (qframe.surfaceFormat().colorTransfer()) { case QVideoFrameFormat::ColorTransfer_Unknown: case QVideoFrameFormat::ColorTransfer_BT709: case QVideoFrameFormat::ColorTransfer_BT601: case QVideoFrameFormat::ColorTransfer_Linear: case QVideoFrameFormat::ColorTransfer_Gamma22: case QVideoFrameFormat::ColorTransfer_Gamma28: // nothing to do: default was already set break; case QVideoFrameFormat::ColorTransfer_ST2084: colorTransfer = CT_ST2084; masteringWhite = linearToPQ(qframe.surfaceFormat().maxLuminance() / 100.0f); break; case QVideoFrameFormat::ColorTransfer_STD_B67: colorTransfer = CT_STD_B67; masteringWhite = linearToHLG(qframe.surfaceFormat().maxLuminance() / 100.0f); break; } qframe.map(QVideoFrame::ReadOnly); planeCount = qframe.planeCount(); for (int p = 0; p < planeCount; p++) { bytesPerLine[p] = qframe.bytesPerLine(p); bytesPerPlane[p] = qframe.mappedBytes(p); mappedBits[p] = qframe.bits(p); } } subtitle = qframe.subtitleText(); subtitle.replace(QLatin1Char('\n'), QChar::LineSeparator); // qvideoframe.cpp does this } else { // Synthesize a black frame inputMode = Input_Mono; surroundMode = Surround_Off; width = 1; height = 1; storage = Storage_Image; image = QImage(width, height, QImage::Format_RGB32); image.fill(0); aspectRatio = 1.0f; subtitle = QString(); } } void VideoFrame::reUpdate() { update(inputMode, surroundMode, qframe, false); } void VideoFrame::invalidate() { if (isValid()) update(Input_Unknown, Surround_Unknown, QVideoFrame(), false); } QDataStream &operator<<(QDataStream& ds, const VideoFrame& f) { ds << static_cast(f.inputMode); ds << static_cast(f.surroundMode); ds << f.subtitle; ds << f.width; ds << f.height; ds << f.aspectRatio; switch (f.storage) { case VideoFrame::Storage_Mapped: case VideoFrame::Storage_Copied: ds << static_cast(VideoFrame::Storage_Copied); ds << static_cast(f.pixelFormat); ds << f.colorRangeSmall; ds << static_cast(f.colorSpace); ds << static_cast(f.colorTransfer); ds << f.masteringWhite; ds << f.planeCount; for (int p = 0; p < f.planeCount; p++) { ds << f.bytesPerLine[p]; ds << f.bytesPerPlane[p]; if (f.storage == VideoFrame::Storage_Mapped) ds.writeRawData(reinterpret_cast(f.mappedBits[p]), f.bytesPerPlane[p]); else ds.writeRawData(reinterpret_cast(f.bits[p].data()), f.bytesPerPlane[p]); } break; case VideoFrame::Storage_Image: ds.writeRawData(reinterpret_cast(f.image.bits()), f.image.sizeInBytes()); break; } return ds; } QDataStream &operator>>(QDataStream& ds, VideoFrame& f) { int tmp; ds >> tmp; f.inputMode = static_cast(tmp); ds >> tmp; f.surroundMode = static_cast(tmp); ds >> f.subtitle; ds >> f.width; ds >> f.height; ds >> f.aspectRatio; ds >> tmp; f.storage = static_cast(tmp); switch (f.storage) { case VideoFrame::Storage_Mapped: // cannot happen, see above case VideoFrame::Storage_Copied: f.image = QImage(); ds >> tmp; f.pixelFormat = static_cast(tmp); ds >> f.colorRangeSmall; ds >> tmp; f.colorSpace = static_cast(tmp); ds >> tmp; f.colorTransfer = static_cast(tmp); ds >> f.masteringWhite; ds >> f.planeCount; for (int p = 0; p < 3; p++) { if (p < f.planeCount) { ds >> f.bytesPerLine[p]; ds >> f.bytesPerPlane[p]; f.bits[p].resize(f.bytesPerPlane[p]); ds.readRawData(reinterpret_cast(f.bits[p].data()), f.bytesPerPlane[p]); } else { f.bytesPerLine[p] = 0; f.bytesPerPlane[p] = 0; f.bits[p].clear(); } f.mappedBits[p] = nullptr; } break; case VideoFrame::Storage_Image: f.pixelFormat = QVideoFrameFormat::pixelFormatFromImageFormat(QImage::Format_RGB32); f.colorRangeSmall = false; f.colorSpace = VideoFrame::CS_AdobeRgb; f.planeCount = 0; for (int p = 0; p < 3; p++) { f.bytesPerLine[p] = 0; f.bytesPerPlane[p] = 0; f.mappedBits[p] = nullptr; f.bits[p].clear(); } f.image = QImage(f.width, f.height, QImage::Format_RGB32); ds.readRawData(reinterpret_cast(f.image.bits()), f.image.sizeInBytes()); break; } return ds; } bino-2.5/src/videoframe.hpp000066400000000000000000000063711475415313200157440ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #pragma once #include #include #include #include "modes.hpp" class VideoFrame { Q_DECLARE_TR_FUNCTIONS(VideoFrame) public: // The pixel data can be represented in one of three ways: // 1. mapped data, via bytesPerLine and mappedBits // 2. copied data, via bytesPerLine and bits // 3. as a fallback: QImage // The QImage fallback is used when the frame pixel format or some other // of its properties cannot be handled in our texture upload / color conversion // pipeline. In that case, we let the frame convert itself to QImage and convert // that to a fallback pixel format that can always be handled. // The mapped data is the preferred option since it is the fastest. On single-process // instances, it is all we need. // The copied data is used to represent mapped data after it has been serialized on the // main process and deserialized on a child process. enum Storage { Storage_Mapped, // mapped data Storage_Copied, // copied data Storage_Image // QImage }; enum ColorSpace { // see shader-color.frag.glsl CS_BT601 = 1, CS_BT709 = 2, CS_AdobeRgb = 3, CS_BT2020 = 4 }; enum ColorTransfer { // see shader-color.frag.glsl CT_NOOP = 1, CT_ST2084 = 2, CT_STD_B67 = 3 }; /* This is a shallow copy of the original QVideoFrame: */ QVideoFrame qframe; /* The input mode of this frame: */ InputMode inputMode; /* The surround mode of this frame: */ SurroundMode surroundMode; /* The subtitle: */ QString subtitle; /* The following can mirror the data of QVideoFrame: */ int width; int height; float aspectRatio; enum Storage storage; // for mapped and copied data: QVideoFrameFormat::PixelFormat pixelFormat; bool colorRangeSmall; enum ColorSpace colorSpace; enum ColorTransfer colorTransfer; float masteringWhite; int planeCount; // 1-3 int bytesPerLine[3]; int bytesPerPlane[3]; // for mapped data: uchar* mappedBits[3]; // for copied data: std::vector bits[3]; // for QImage data: QImage image; VideoFrame(); bool isValid() const; void update(InputMode im, SurroundMode ts, const QVideoFrame& frame, bool newSrc); void reUpdate(); void invalidate(); }; QDataStream &operator<<(QDataStream& ds, const VideoFrame& frame); QDataStream &operator>>(QDataStream& ds, VideoFrame& frame); bino-2.5/src/videosink.cpp000066400000000000000000000072501475415313200156060ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #include #include "videosink.hpp" #include "log.hpp" VideoSink::VideoSink(VideoFrame* frame, VideoFrame* extFrame, bool* frameIsNew) : frameCounter(0), frame(frame), extFrame(extFrame), frameIsNew(frameIsNew), needExtFrame(false), inputMode(Input_Unknown), surroundMode(Surround_Unknown), lastFrameWasValid(false) { connect(this, SIGNAL(videoFrameChanged(const QVideoFrame&)), this, SLOT(processNewFrame(const QVideoFrame&))); } // called whenever new media is played: void VideoSink::newPlaylistEntry(const PlaylistEntry& entry, const MetaData& metaData) { frameCounter = 0; lastFrameWasValid = false; int vt = entry.videoTrack; if (vt < 0) vt = 0; inputMode = entry.inputMode; if (inputMode == Input_Unknown) { if (vt < metaData.inputModes.size()) inputMode = metaData.inputModes[vt]; } LOG_DEBUG("input mode for %s: %s", qPrintable(entry.url.toString()), inputModeToString(inputMode)); surroundMode = entry.surroundMode; if (surroundMode == Surround_Unknown) { if (vt < metaData.surroundModes.size()) surroundMode = metaData.surroundModes[vt]; } LOG_DEBUG("surround mode for %s: %s", qPrintable(entry.url.toString()), surroundModeToString(surroundMode)); } void VideoSink::processNewFrame(const QVideoFrame& frame) { if (!frame.isValid() && lastFrameWasValid) { // keep showing the last frame of the current media; ignore that QtMultiMedia // with the FFmpeg backend gives us an invalid frame as soon as the media stops // (it does not do this with the GStreamer backend) LOG_DEBUG("video sink gets invalid frame and ignores it since last frame of current media was valid"); return; } if (frame.isValid()) { LOG_FIREHOSE("video sink gets a valid frame"); lastFrameWasValid = true; } bool updateExtFrame; if (inputMode == Input_Alternating_LR || inputMode == Input_Alternating_RL) { if (needExtFrame) { LOG_FIREHOSE("video sink updates extended frame for alternating mode"); updateExtFrame = true; needExtFrame = false; } else { LOG_FIREHOSE("video sink updates standard frame for alternating mode"); updateExtFrame = false; needExtFrame = true; } } else { LOG_FIREHOSE("video sink updates standard frame for non-alternating mode"); updateExtFrame = false; needExtFrame = false; } if (updateExtFrame) { this->extFrame->update(inputMode, surroundMode, frame, frameCounter == 0); } else { this->frame->update(inputMode, surroundMode, frame, frameCounter == 0); this->extFrame->invalidate(); } if (!needExtFrame) { LOG_FIREHOSE("video sink signals that new frame is complete"); *frameIsNew = true; emit newVideoFrame(); } frameCounter++; } bino-2.5/src/videosink.hpp000066400000000000000000000034411475415313200156110ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024, 2025 * Martin Lambers * * 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 . */ #pragma once #include #include "modes.hpp" #include "videoframe.hpp" #include "metadata.hpp" #include "playlist.hpp" class VideoSink : public QVideoSink { Q_OBJECT public: unsigned long long frameCounter; // number of frames seen for this URL VideoFrame* frame; // target video frame VideoFrame* extFrame; // extension to target video frame, for alternating stereo bool *frameIsNew; // flag to set when the target frame represents a new frame bool needExtFrame; // flag to set in alternating stereo when extFrame is not filled yet InputMode inputMode; // input mode of current media SurroundMode surroundMode; // surround mode of the current media bool lastFrameWasValid; // last frame of current media was valid VideoSink(VideoFrame* frame, VideoFrame* extFrame, bool* frameIsNew); void newPlaylistEntry(const PlaylistEntry& entry, const MetaData& metaData); public Q_SLOTS: void processNewFrame(const QVideoFrame& frame); signals: void newVideoFrame(); }; bino-2.5/src/widget.cpp000066400000000000000000000430751475415313200151030ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #include #include #include #include #include "widget.hpp" #include "playlist.hpp" #include "tools.hpp" #include "log.hpp" /* These might not be defined in OpenGL ES environments. * Define them here to fix compilation. */ #ifndef GL_BACK_LEFT # define GL_BACK_LEFT 0x0402 #endif #ifndef GL_BACK_RIGHT # define GL_BACK_RIGHT 0x0403 #endif static const QSize SizeBase(16, 9); Widget::Widget(OutputMode outputMode, float surroundVerticalFOV, QWidget* parent) : QOpenGLWidget(parent), _sizeHint(0.5f * SizeBase), _outputMode(outputMode), _openGLStereo(QSurfaceFormat::defaultFormat().stereo()), _alternatingLastView(1), _inSurroundMovement(false), _surroundHorizontalAngleBase(0.0f), _surroundVerticalAngleBase(0.0f), _surroundHorizontalAngleCurrent(0.0f), _surroundVerticalAngleCurrent(0.0f) { setSurroundVerticalFieldOfView(surroundVerticalFOV); _surroundVerticalFOVDefault = _surroundVerticalFOV; // to make sure clamping was applied setUpdateBehavior(QOpenGLWidget::PartialUpdate); setMouseTracking(true); setMinimumSize(8, 8); QSize screenSize = QGuiApplication::primaryScreen()->availableSize(); QSize maxSize = 0.75f * screenSize; _sizeHint = SizeBase.scaled(maxSize, Qt::KeepAspectRatio); connect(Bino::instance(), &Bino::newVideoFrame, [=]() { update(); }); connect(Bino::instance(), &Bino::toggleFullscreen, [=]() { emit toggleFullscreen(); }); connect(Playlist::instance(), SIGNAL(mediaChanged(PlaylistEntry)), this, SLOT(mediaChanged(PlaylistEntry))); setFocus(); } bool Widget::isOpenGLStereo() const { return _openGLStereo; } OutputMode Widget::outputMode() const { return _outputMode; } void Widget::setOutputMode(enum OutputMode mode) { _outputMode = mode; } void Widget::setSurroundVerticalFieldOfView(float vfov) { _surroundVerticalFOV = qBound(5.0f, vfov, 115.0f); } void Widget::resetSurroundView() { _surroundVerticalFOV = _surroundVerticalFOVDefault; _surroundHorizontalAngleBase = 0.0f; _surroundVerticalAngleBase = 0.0f; _surroundHorizontalAngleCurrent = 0.0f; _surroundVerticalAngleCurrent = 0.0f; } QSize Widget::sizeHint() const { return _sizeHint; } void Widget::initializeGL() { bool contextIsOk = (context()->isValid() && context()->format().majorVersion() >= 3); if (!contextIsOk) { LOG_FATAL("%s", qPrintable(tr("Insufficient OpenGL capabilities."))); QMessageBox::critical(this, tr("Error"), tr("Insufficient OpenGL capabilities.")); std::exit(1); } if (QSurfaceFormat::defaultFormat().stereo() && !context()->format().stereo()) { LOG_FATAL("%s", qPrintable(tr("OpenGL stereo mode is not available on this system."))); QMessageBox::critical(this, tr("Error"), tr("OpenGL stereo mode is not available on this system.")); std::exit(1); } bool haveAnisotropicFiltering = checkTextureAnisotropicFilterAvailability(); initializeOpenGLFunctions(); bool isCoreProfile = (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile); QString variantString = IsOpenGLES ? "OpenGL ES" : "OpenGL"; if (!IsOpenGLES) variantString += isCoreProfile ? " core profile" : " compatibility profile"; GLint maxTexSize, maxFBWidth, maxFBHeight; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize); glGetIntegerv(GL_MAX_FRAMEBUFFER_WIDTH, &maxFBWidth); glGetIntegerv(GL_MAX_FRAMEBUFFER_HEIGHT, &maxFBHeight); LOG_INFO("OpenGL Variant: %s", qPrintable(variantString)); LOG_INFO("OpenGL Version: %s", getOpenGLString(this, GL_VERSION)); LOG_INFO("OpenGL GLSL Version: %s", getOpenGLString(this, GL_SHADING_LANGUAGE_VERSION)); LOG_INFO("OpenGL Vendor: %s", getOpenGLString(this, GL_VENDOR)); LOG_INFO("OpenGL Renderer: %s", getOpenGLString(this, GL_RENDERER)); LOG_INFO("OpenGL AnisoTexFilt: %s", haveAnisotropicFiltering ? "yes" : "no"); LOG_INFO("OpenGL Max Tex Size: %d", maxTexSize); LOG_INFO("OpenGL Max FB Size: %dx%d", maxFBWidth, maxFBHeight); // View textures glGenTextures(2, _viewTex); for (int i = 0; i < 2; i++) { glBindTexture(GL_TEXTURE_2D, _viewTex[i]); unsigned char nullBytes[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; if (IsOpenGLES) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, 1, 1, 0, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, nullBytes); else glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16, 1, 1, 0, GL_RGBA, GL_UNSIGNED_SHORT, nullBytes); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); if (haveAnisotropicFiltering) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 4.0f); _viewTexWidth[i] = 1; _viewTexHeight[i] = 1; } CHECK_GL(); // Quad geometry const float quadPositions[] = { -1.0f, +1.0f, 0.0f, +1.0f, +1.0f, 0.0f, +1.0f, -1.0f, 0.0f, -1.0f, -1.0f, 0.0f }; const float quadTexCoords[] = { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f }; static const unsigned short quadIndices[] = { 0, 3, 1, 1, 3, 2 }; glGenVertexArrays(1, &_quadVao); glBindVertexArray(_quadVao); GLuint quadPositionBuf; glGenBuffers(1, &quadPositionBuf); glBindBuffer(GL_ARRAY_BUFFER, quadPositionBuf); glBufferData(GL_ARRAY_BUFFER, sizeof(quadPositions), quadPositions, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); GLuint quadTexCoordBuf; glGenBuffers(1, &quadTexCoordBuf); glBindBuffer(GL_ARRAY_BUFFER, quadTexCoordBuf); glBufferData(GL_ARRAY_BUFFER, sizeof(quadTexCoords), quadTexCoords, GL_STATIC_DRAW); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); GLuint quadIndexBuf; glGenBuffers(1, &quadIndexBuf); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, quadIndexBuf); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(quadIndices), quadIndices, GL_STATIC_DRAW); CHECK_GL(); // Initialize Bino Bino::instance()->initProcess(); } void Widget::rebuildDisplayPrgIfNecessary(OutputMode outputMode) { if (outputMode == Output_Right) outputMode = Output_Left; // these are handled specially; see shader if (_displayPrg.isLinked() && _displayPrgOutputMode == outputMode) return; LOG_DEBUG("rebuilding display program for output mode %s", outputModeToString(outputMode)); QString vertexShaderSource = readFile(":src/shader-display.vert.glsl"); QString fragmentShaderSource = readFile(":src/shader-display.frag.glsl"); fragmentShaderSource.replace("$OUTPUT_MODE", QString::number(int(outputMode))); if (IsOpenGLES) { vertexShaderSource.prepend("#version 300 es\n"); fragmentShaderSource.prepend("#version 300 es\n" "precision mediump float;\n"); } else { vertexShaderSource.prepend("#version 330\n"); fragmentShaderSource.prepend("#version 330\n"); } _displayPrg.removeAllShaders(); _displayPrg.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource); _displayPrg.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource); _displayPrg.link(); _displayPrgOutputMode = outputMode; } void Widget::paintGL() { // Support for HighDPI output int width = _width * devicePixelRatioF(); int height = _height * devicePixelRatioF(); // Find out about the views we have int viewCount, viewWidth, viewHeight; float frameDisplayAspectRatio; bool surround; Bino::instance()->preRenderProcess(width, height, &viewCount, &viewWidth, &viewHeight, &frameDisplayAspectRatio, &surround); // Adjust the stereo mode if necessary bool frameIsStereo = (viewCount == 2); OutputMode outputMode = _outputMode; if (!frameIsStereo) outputMode = Output_Left; if (outputMode == Output_Left_Right || outputMode == Output_Right_Left) frameDisplayAspectRatio *= 2.0f; else if (outputMode == Output_Top_Bottom || outputMode == Output_Bottom_Top || outputMode == Output_HDMI_Frame_Pack) frameDisplayAspectRatio *= 0.5f; LOG_FIREHOSE("%s: %d views, %dx%d, %g, surround %s", Q_FUNC_INFO, viewCount, viewWidth, viewHeight, frameDisplayAspectRatio, surround ? "on" : "off"); // Fill the view texture(s) as needed for (int v = 0; v <= 1; v++) { bool needThisView = true; switch (outputMode) { case Output_Left: needThisView = (v == 0); break; case Output_Right: needThisView = (v == 1); break; case Output_Alternating: needThisView = (v != _alternatingLastView); break; case Output_HDMI_Frame_Pack: case Output_OpenGL_Stereo: case Output_Left_Right: case Output_Left_Right_Half: case Output_Right_Left: case Output_Right_Left_Half: case Output_Top_Bottom: case Output_Top_Bottom_Half: case Output_Bottom_Top: case Output_Bottom_Top_Half: case Output_Even_Odd_Rows: case Output_Even_Odd_Columns: case Output_Checkerboard: case Output_Red_Cyan_Dubois: case Output_Red_Cyan_FullColor: case Output_Red_Cyan_HalfColor: case Output_Red_Cyan_Monochrome: case Output_Green_Magenta_Dubois: case Output_Green_Magenta_FullColor: case Output_Green_Magenta_HalfColor: case Output_Green_Magenta_Monochrome: case Output_Amber_Blue_Dubois: case Output_Amber_Blue_FullColor: case Output_Amber_Blue_HalfColor: case Output_Amber_Blue_Monochrome: case Output_Red_Green_Monochrome: case Output_Red_Blue_Monochrome: break; } if (!needThisView) continue; // prepare view texture glBindTexture(GL_TEXTURE_2D, _viewTex[v]); if (_viewTexWidth[v] != viewWidth || _viewTexHeight[v] != viewHeight) { if (IsOpenGLES) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, viewWidth, viewHeight, 0, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, nullptr); else glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16, viewWidth, viewHeight, 0, GL_RGBA, GL_UNSIGNED_SHORT, nullptr); _viewTexWidth[v] = viewWidth; _viewTexHeight[v] = viewHeight; } // render view into view texture LOG_FIREHOSE("%s: getting view %d for stereo mode %s", Q_FUNC_INFO, v, outputModeToString(outputMode)); QMatrix4x4 projectionMatrix; QMatrix4x4 orientationMatrix; QMatrix4x4 viewMatrix; if (Bino::instance()->assumeSurroundMode() != Surround_Off) { float verticalFieldOfView = qDegreesToRadians(_surroundVerticalFOV); float aspectRatio = 2.0f; // always 2:1 for surround video! float top = qTan(verticalFieldOfView * 0.5f); float bottom = -top; float right = top * aspectRatio; float left = -right; projectionMatrix.frustum(left, right, bottom, top, 1.0f, 100.0f); QQuaternion orientation = QQuaternion::fromEulerAngles( (_surroundVerticalAngleBase + _surroundVerticalAngleCurrent), (_surroundHorizontalAngleBase + _surroundHorizontalAngleCurrent), 0.0f); orientationMatrix.rotate(orientation.inverted()); } Bino::instance()->render( QVector3D(), QVector3D(), QVector3D(), QVector3D(), QVector3D(), QVector3D(), projectionMatrix, orientationMatrix, viewMatrix, v, viewWidth, viewHeight, _viewTex[v]); // generate mipmaps for the view texture glBindTexture(GL_TEXTURE_2D, _viewTex[v]); glGenerateMipmap(GL_TEXTURE_2D); } // Put the views on screen in the current mode glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject()); glViewport(0, 0, width, height); glDisable(GL_DEPTH_TEST); float relWidth = 1.0f; float relHeight = 1.0f; float screenAspectRatio = width / float(height); if (outputMode == Output_HDMI_Frame_Pack) screenAspectRatio = width / (height - height / 49.0f); if (screenAspectRatio < frameDisplayAspectRatio) relHeight = screenAspectRatio / frameDisplayAspectRatio; else relWidth = frameDisplayAspectRatio / screenAspectRatio; rebuildDisplayPrgIfNecessary((outputMode == Output_OpenGL_Stereo || outputMode == Output_Alternating) ? Output_Left /* also covers Output_Right */ : outputMode); glUseProgram(_displayPrg.programId()); _displayPrg.setUniformValue("view0", 0); _displayPrg.setUniformValue("view1", 1); _displayPrg.setUniformValue("relativeWidth", relWidth); _displayPrg.setUniformValue("relativeHeight", relHeight); QPoint globalLowerLeft = mapToGlobal(QPoint(0, height - 1)); _displayPrg.setUniformValue("fragOffsetX", float(globalLowerLeft.x())); _displayPrg.setUniformValue("fragOffsetY", float(screen()->geometry().height() - 1 - globalLowerLeft.y())); LOG_FIREHOSE("lower left widget corner in screen coordinates: x=%d y=%d", globalLowerLeft.x(), screen()->geometry().height() - 1 - globalLowerLeft.y()); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _viewTex[0]); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, _viewTex[1]); glBindVertexArray(_quadVao); if (_openGLStereo) { LOG_FIREHOSE("widget draw mode: opengl stereo"); GLenum bufferBackLeft = GL_BACK_LEFT; GLenum bufferBackRight = GL_BACK_RIGHT; if (outputMode == Output_OpenGL_Stereo) { glDrawBuffers(1, &bufferBackLeft); _displayPrg.setUniformValue("outputModeLeftRightView", 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); glDrawBuffers(1, &bufferBackRight); _displayPrg.setUniformValue("outputModeLeftRightView", 1); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); } else { if (outputMode == Output_Alternating) outputMode = (_alternatingLastView == 0 ? Output_Right : Output_Left); _displayPrg.setUniformValue("outputModeLeftRightView", outputMode == Output_Left ? 0 : 1); glDrawBuffers(1, &bufferBackLeft); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); glDrawBuffers(1, &bufferBackRight); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); } } else { LOG_FIREHOSE("widget draw mode: normal"); if (outputMode == Output_Alternating) outputMode = (_alternatingLastView == 0 ? Output_Right : Output_Left); _displayPrg.setUniformValue("outputModeLeftRightView", outputMode == Output_Left ? 0 : 1); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); } // Update Output_Alternating if (_outputMode == Output_Alternating && frameIsStereo) { _alternatingLastView = (_alternatingLastView == 0 ? 1 : 0); update(); } } void Widget::resizeGL(int w, int h) { _width = w; _height = h; } void Widget::keyPressEvent(QKeyEvent* e) { Bino::instance()->keyPressEvent(e); } void Widget::mousePressEvent(QMouseEvent* e) { _inSurroundMovement = true; _surroundMovementStart = e->position(); _surroundHorizontalAngleCurrent = 0.0f; _surroundVerticalAngleCurrent = 0.0f; } void Widget::mouseReleaseEvent(QMouseEvent*) { _inSurroundMovement = false; _surroundHorizontalAngleBase += _surroundHorizontalAngleCurrent; _surroundVerticalAngleBase += _surroundVerticalAngleCurrent; _surroundHorizontalAngleCurrent = 0.0f; _surroundVerticalAngleCurrent = 0.0f; } void Widget::mouseMoveEvent(QMouseEvent* e) { // Support for HighDPI output int width = _width * devicePixelRatioF(); int height = _height * devicePixelRatioF(); if (_inSurroundMovement) { // position delta QPointF posDelta = e->position() - _surroundMovementStart; // horizontal angle delta float dx = posDelta.x(); float xf = dx / width; // in [-1,+1] _surroundHorizontalAngleCurrent = xf * 180.0f; // vertical angle float dy = posDelta.y(); float yf = dy / height; // in [-1,+1] _surroundVerticalAngleCurrent = yf * 90.0f; update(); } } void Widget::wheelEvent(QWheelEvent* e) { setSurroundVerticalFieldOfView(_surroundVerticalFOV - e->angleDelta().y() / 120.0f); update(); } void Widget::mediaChanged(PlaylistEntry) { _inSurroundMovement = false; _surroundHorizontalAngleBase = 0.0f; _surroundVerticalAngleBase = 0.0f; _surroundHorizontalAngleCurrent = 0.0f; _surroundVerticalAngleCurrent = 0.0f; } bino-2.5/src/widget.hpp000066400000000000000000000050631475415313200151030ustar00rootroot00000000000000/* * This file is part of Bino, a 3D video player. * * Copyright (C) 2022, 2023, 2024 * Martin Lambers * * 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 . */ #pragma once #include #include #include "modes.hpp" #include "bino.hpp" class Widget : public QOpenGLWidget, protected QOpenGLExtraFunctions { Q_OBJECT private: QSize _sizeHint; int _width, _height; OutputMode _outputMode; bool _openGLStereo; // is this widget in quad-buffered stereo mode? int _alternatingLastView; // last view displayed in Mode_Alternating (0 or 1) float _surroundVerticalFOVDefault; float _surroundVerticalFOV; bool _inSurroundMovement; QPointF _surroundMovementStart; float _surroundHorizontalAngleBase; float _surroundVerticalAngleBase; float _surroundHorizontalAngleCurrent; float _surroundVerticalAngleCurrent; unsigned int _viewTex[2]; int _viewTexWidth[2], _viewTexHeight[2]; unsigned int _quadVao; QOpenGLShaderProgram _displayPrg; int _displayPrgOutputMode; void rebuildDisplayPrgIfNecessary(OutputMode outputMode); public: Widget(OutputMode outputMode, float surroundVerticalFOV, QWidget* parent = nullptr); bool isOpenGLStereo() const; OutputMode outputMode() const; void setOutputMode(OutputMode mode); void setSurroundVerticalFieldOfView(float vfov); void resetSurroundView(); virtual QSize sizeHint() const override; virtual void initializeGL() override; virtual void paintGL() override; virtual void resizeGL(int w, int h) override; virtual void keyPressEvent(QKeyEvent* e) override; virtual void mousePressEvent(QMouseEvent* e) override; virtual void mouseReleaseEvent(QMouseEvent* e) override; virtual void mouseMoveEvent(QMouseEvent* e) override; virtual void wheelEvent(QWheelEvent* e) override; public slots: void mediaChanged(PlaylistEntry entry); signals: void toggleFullscreen(); };