commit ef90c77e8a47616a62e19df51fffe04d06610684 Author: zii Date: Tue Aug 6 08:28:43 2024 -0400 v1.0 -first ccommit for stable version of JustVideo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d92f52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +/build +/app_dir +/release +/debug +/installers + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +# VSCode +/.vscode diff --git a/JustVideo.pro b/JustVideo.pro new file mode 100644 index 0000000..669fd0f --- /dev/null +++ b/JustVideo.pro @@ -0,0 +1,56 @@ +QT += core gui +QT += multimedia +QT += multimediawidgets + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++11 + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +TARGET = build/jvideo +OBJECTS_DIR = build +MOC_DIR = build +RCC_DIR = build + +DEFINES += "QT_AVPLAYER_MULTIMEDIA" \ + #"QT_AVPLAYER_VA_X11" \ #uncomment to enable libva-x11 + #"QT_AVPLAYER_VA_DRM" \ #uncomment to enable libva-drm + "QT_AVPLAYER_VDPAU" + +INCLUDEPATH += QtAVPlayer/src + +include(QtAVPlayer/src/QtAVPlayer/QtAVPlayer.pri) + +SOURCES += \ + src/actions.cpp \ + src/folder_dialog.cpp \ + src/img_loader.cpp \ + src/main.cpp \ + src/common.cpp \ + src/main_widget.cpp \ + src/play_control.cpp \ + src/player.cpp \ + src/playlist_backend.cpp \ + src/playlist_widget.cpp \ + src/pref_dialog.cpp \ + src/recents.cpp \ + src/vid_grid.cpp \ + src/vid_widget.cpp + +HEADERS += \ + src/actions.h \ + src/common.h \ + src/folder_dialog.h \ + src/img_loader.h \ + src/main_widget.h \ + src/play_control.h \ + src/player.h \ + src/playlist_backend.h \ + src/playlist_widget.h \ + src/pref_dialog.h \ + src/recents.h \ + src/vid_grid.h \ + src/vid_widget.h diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f718b3e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,621 @@ + 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 \ No newline at end of file diff --git a/QtAVPlayer/LICENSE b/QtAVPlayer/LICENSE new file mode 100644 index 0000000..2af75f0 --- /dev/null +++ b/QtAVPlayer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Val Doroshchuk + +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. diff --git a/QtAVPlayer/README.md b/QtAVPlayer/README.md new file mode 100644 index 0000000..3e22e8d --- /dev/null +++ b/QtAVPlayer/README.md @@ -0,0 +1,218 @@ +# Qt AVPlayer +![example workflow](https://github.com/valbok/QtAVPlayer/actions/workflows/main.yaml/badge.svg) + +Free and open-source Qt Media Player library based on FFmpeg. +- Designed to decode _video_/_audio_/_subtitle_ frames. +- Supports [FFmpeg Bitstream Filters](https://ffmpeg.org/ffmpeg-bitstream-filters.html) and [FFmpeg Filters](https://ffmpeg.org/ffmpeg-filters.html) including `filter_complex`. +- Supports multiple parallel filters for one input (one input frame and multiple output ones). +- Supports decoding all available streams at the same time. +- Based on Qt platform the video frames are sent using specific hardware context: + * `VA-API` for Linux: DRM with EGL or X11 with GLX. + * `VDPAU` for Linux. + * `Video Toolbox` for macOS and iOS. + * `D3D11` for Windows. + * `MediaCodec` for Android. + + Note: Not all ffmpeg decoders or filters support HW acceleration. In this case software decoders are used. +- It is up to an application to decide how to process the frames. + * But there is _experimental_ support of converting the video frames to QtMultimedia's [QVideoFrame](https://doc.qt.io/qt-5/qvideoframe.html) for copy-free rendering if possible. + Note: Not all Qt's renders support copy-free rendering. Also QtMultimedia does not always provide public API to render the video frames. And, of course, for best performance both decoding and rendering should be accelerated. + * Audio frames could be played by `QAVAudioOutput` which is a wrapper of QtMultimedia's [QAudioSink](https://doc-snapshots.qt.io/qt6-dev/qaudiosink.html) +- Supports accurate seek, it starts playing the closest frame. No weird jumps on pts anymore. +- It is bundled directly into an app using qmake pri. +- Designed to be as simple and understandable as possible, to share knowledge about creating efficient FFmpeg applications. +- Might be used for media analytics software like [qctools](https://github.com/bavc/qctools) or [dvrescue](https://github.com/mipops/dvrescue). +- Strange to say this in 21st century, but each feature is covered by integration tests. +- Implements and replaces a combination of FFmpeg and FFplay: + + ffmpeg -i we-miss-gst-pipeline-in-qt6mm.mkv -filter_complex "qt,nev:er,wanted;[ffmpeg];what:happened" - | ffplay - + + but using QML or Qt Widgets: + + ./qml_video :/valbok "if:you:like[cats];remove[this]" + +# Features + +1. QAVPlayer supports playing from an url or QIODevice or from avdevice: + + player.setSource("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"); + player.setSource("/home/lana/The Matrix Resurrections.mov"); + + // Playing from qrc + QSharedPointer file(new QFile(":/alarm.wav")); + file->open(QIODevice::ReadOnly); + QSharedPointer dev(new QAVIODevice(file)); + player.setSource("alarm", dev); + + // Getting frames from the camera in Linux + player.setSource("/dev/video0"); + // Or Windows + player.setInputFormat("dshow"); + player.setSource("video=Integrated Camera"); + // Or MacOS + player.setInputFormat("avfoundation"); + player.setSource("default"); + // Or Android + player.setInputFormat("android_camera"); + player.setSource("0:0"); + + player.setInputOptions({{"user_agent", "QAVPlayer"}}); + + // Using various protocols + player.setSource("subfile,,start,0,end,0,,:/root/Downloads/why-qtmm-must-die.mkv"); + +3. Easy getting video and audio frames: + + QObject::connect(&player, &QAVPlayer::videoFrame, [&](const QAVVideoFrame &frame) { + // QAVVideoFrame is comppatible with QVideoFrame + QVideoFrame videoFrame = frame; + + // QAVVideoFrame can be converted to various pixel formats + auto convertedFrame = frame.convert(AV_PIX_FMT_YUV420P); + + // Easy getting data from video frame + auto mapped = videoFrame.map(); // downloads data if it is in GPU + qDebug() << mapped.format << mapped.size; + + // The frame might contain OpenGL or MTL textures, for copy-free rendering + qDebug() << frame.handleType() << frame.handle(); + }, Qt::DirectConnection); + + // Audio frames could be played using QAVAudioOutput + QAVAudioOutput audioOutput; + QObject::connect(&player, &QAVPlayer::audioFrame, [&](const QAVAudioFrame &frame) { + // Access to the data + qDebug() << autioFrame.format() << autioFrame.data().size(); + audioOutput.play(frame); + }, Qt::DirectConnection); + + QObject::connect(&p, &QAVPlayer::subtitleFrame, &p, [](const QAVSubtitleFrame &frame) { + for (unsigned i = 0; i < frame.subtitle()->num_rects; ++i) { + if (frame.subtitle()->rects[i]->type == SUBTITLE_TEXT) + qDebug() << "text:" << frame.subtitle()->rects[i]->text; + else + qDebug() << "ass:" << frame.subtitle()->rects[i]->ass; + } + }, Qt::DirectConnection); + + +4. Each action is confirmed by a signal: + + // All signals are added to a queue and guaranteed to be emitted in proper order. + QObject::connect(&player, &QAVPlayer::played, [&](qint64 pos) { qDebug() << "Playing started from pos" << pos; }); + QObject::connect(&player, &QAVPlayer::paused, [&](qint64 pos) { qDebug() << "Paused at pos" << pos; }); + QObject::connect(&player, &QAVPlayer::stopped, [&](qint64 pos) { qDebug() << "Stopped at pos" << pos; }); + QObject::connect(&player, &QAVPlayer::seeked, [&](qint64 pos) { qDebug() << "Seeked to pos" << pos; }); + QObject::connect(&player, &QAVPlayer::stepped, [&](qint64 pos) { qDebug() << "Made a step to pos" << pos; }); + QObject::connect(&player, &QAVPlayer::mediaStatusChanged, [&](QAVPlayer::MediaStatus status) { + switch (status) { + case QAVplayer::EndOfMedia: + qDebug() << "Finished to play, no frames in queue"; + break; + case QAVplayer::NoMedia: + qDebug() << "Demuxer threads are finished"; + break; + default: + break; + } + }); + +5. Accurate seek: + + QObject::connect(&p, &QAVPlayer::seeked, &p, [&](qint64 pos) { seekPosition = pos; }); + QObject::connect(&player, &QAVPlayer::videoFrame, [&](const QAVVideoFrame &frame) { seekFrame = frame; }); + player.seek(5000) + QTRY_COMPARE(seekPosition, 5000); + QTRY_COMPARE(seekFrame.pts(), 5.0); + + If there is a frame with needed pts, it will be returned as first frame. + +6. FFmpeg filters: + + player.setFilter("crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left][right] hstack"); + // Render bundled subtitles + player.setFilter("subtitles=file.mkv"); + // Render subtitles from srt file + player.setFilter("subtitles=file.srt"); + // Multiple filters + player.setFilters({ + "drawtext=text=%{pts\\\\:hms}:x=(w-text_w)/2:y=(h-text_h)*(4/5):box=1:boxcolor=gray@0.5:fontsize=36[drawtext]", + "negate[negate]", + "[0:v]split=3[in1][in2][in3];[in1]boxblur[out1];[in2]negate[out2];[in3]drawtext=text=%{pts\\\\:hms}:x=(w-text_w)/2:y=(h-text_h)*(4/5):box=1:boxcolor=gray@0.5:fontsize=36[out3]" + }); // Return frames from 3 filters with 5 outputs + +7. Step by step: + + // Pausing will always emit one frame + QObject::connect(&player, &QAVPlayer::videoFrame, [&](const QAVVideoFrame &frame) { receivedFrame = frame; }); + if (player.state() != QAVPlayer::PausedState) { // No frames if it is already paused + player.pause(); + QTRY_VERIFY(receivedFrame); + } + + // Always makes a step forward and emits only one frame + player.stepForward(); + // the same here but backward + player.stepBackward(); + +8. Multiple streams: + + qDebug() << "Audio streams" << player.availableAudioStreams().size(); + qDebug() << "Current audio stream" << player.currentAudioStreams().first().index() << player.currentAudioStreams().first().metadata(); + player.setAudioStreams(player.availableAudioStreams()); // Return all frames for all available audio streams + // Reports progress of playing per stream, like current pts, fps, frame rate, num of frames etc + for (const auto &s : p.availableVideoStreams()) + qDebug() << s << p.progress(s); + +9. HW accelerations: + + QT_AVPLAYER_NO_HWDEVICE can be used to force using software decoding. The video codec is negotiated automatically. + + * `VA-API` and `VDPAU` for Linux: the frames are returned with OpenGL textures. + * `Video Toolbox` for macOS and iOS: the frames are returned with Metal Textures. + * `D3D11` for Windows: the frames are returned with D3D11Texture2D textures. + * `MediaCodec` for Android: the frames are returned with OpenGL textures. + +10. QtMultimedia could be used to render video frames to QML or Widgets. See [examples](examples) +11. Qt 5.12 - **6**.x is supported + +# How to build + +QtAVPlayer should be directly bundled into an app using QMake and [QtAVPlayer.pri](https://github.com/valbok/QtAVPlayer/blob/master/src/QtAVPlayer/QtAVPlayer.pri). +Some defines should be provided to opt some features. +* `QT_AVPLAYER_MULTIMEDIA` - enables support of `QtMultimedia` which requires `QtGUI`, `QtQuick` etc. +* `QT_AVPLAYER_VA_X11` - enables support of `libva-x11` for HW acceleration. For linux only. +* `QT_AVPLAYER_VA_DRM` - enables support of `libva-drm` for HW acceleration. For linux only. +* `QT_AVPLAYER_VDPAU` - enables support of `libvdpau` for HW acceleration. For linux only. + +CMake is not supported. + +Include QtAVPlayer.pri in your pro file: + + INCLUDEPATH += ../../src/ + include(../../src/QtAVPlayer/QtAVPlayer.pri) + +And then for your app: + + $ qmake DEFINES+="QT_AVPLAYER_MULTIMEDIA" + +FFmpeg on custom path: + + $ qmake DEFINES+="QT_AVPLAYER_MULTIMEDIA" INCLUDEPATH+="/usr/local/Cellar/ffmpeg/6.0/include" LIBS="-L/usr/local/Cellar/ffmpeg/6.0/lib" + +## Android: + +Some exports could be also used: vars that point to libraries in armeabi-v7a, arm64-v8a, x86 and x86_64 target archs. + + $ export AVPLAYER_ANDROID_LIB_ARMEABI_V7A=/opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib + $ export AVPLAYER_ANDROID_LIB_ARMEABI_V8A=/opt/mobile-ffmpeg/prebuilt/android-arm64/ffmpeg/lib + $ export AVPLAYER_ANDROID_LIB_X86=/opt/mobile-ffmpeg/prebuilt/android-x86/ffmpeg/lib + $ export AVPLAYER_ANDROID_LIB_X86_64=/opt/mobile-ffmpeg/prebuilt/android-x86_64/ffmpeg/lib + $ export CPLUS_INCLUDE_PATH=/opt/mobile-ffmpeg/prebuilt/android-arm64/ffmpeg/include:$CPLUS_INCLUDE_PATH + $ qmake DEFINES+="QT_AVPLAYER_MULTIMEDIA" + +Don't forget to set extra libs in _pro_ file for your app: + + ANDROID_EXTRA_LIBS += /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavdevice.so /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavformat.so /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavutil.so /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavcodec.so /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavfilter.so /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libswscale.so /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libswresample.so + + diff --git a/QtAVPlayer/examples/extract_frames/extract_frames.pro b/QtAVPlayer/examples/extract_frames/extract_frames.pro new file mode 100644 index 0000000..a2451d8 --- /dev/null +++ b/QtAVPlayer/examples/extract_frames/extract_frames.pro @@ -0,0 +1,13 @@ +TEMPLATE = app +TARGET = extract_frames +INCLUDEPATH += . + +INCLUDEPATH += . ../../src +include(../../src/QtAVPlayer/QtAVPlayer.pri) + +QT -= gui +CONFIG += c++1z +SOURCES += main.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/$$TARGET +INSTALLS += target diff --git a/QtAVPlayer/examples/extract_frames/main.cpp b/QtAVPlayer/examples/extract_frames/main.cpp new file mode 100644 index 0000000..362f0e7 --- /dev/null +++ b/QtAVPlayer/examples/extract_frames/main.cpp @@ -0,0 +1,30 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + QAVPlayer p; + QObject::connect(&p, &QAVPlayer::audioFrame, [&](const QAVAudioFrame &frame) { qDebug() << "audio:" << frame.pts(); }); + QObject::connect(&p, &QAVPlayer::videoFrame, [&](const QAVVideoFrame &frame) { qDebug() << "video:" << frame.pts(); }); + p.setSource(QLatin1String("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")); + p.play(); + + QObject::connect(&p, &QAVPlayer::stateChanged, [&](auto s) { qDebug() << "stateChanged" << s << p.mediaStatus(); }); + QObject::connect(&p, &QAVPlayer::mediaStatusChanged, [&](auto s){ qDebug() << "mediaStatusChanged"<< s << p.state(); }); + QObject::connect(&p, &QAVPlayer::durationChanged, [&](auto d) { qDebug() << "durationChanged" << d; }); + + return app.exec(); +} + diff --git a/QtAVPlayer/examples/qml_video/main.cpp b/QtAVPlayer/examples/qml_video/main.cpp new file mode 100644 index 0000000..bbed2ec --- /dev/null +++ b/QtAVPlayer/examples/qml_video/main.cpp @@ -0,0 +1,204 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include +#include +#include +#include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#include +#else +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) +class Source : public QObject +{ + Q_OBJECT + Q_PROPERTY(QAbstractVideoSurface *videoSurface READ videoSurface WRITE setVideoSurface) +public: + explicit Source(QObject *parent = 0) : QObject(parent) { } + virtual ~Source() { } + + QAbstractVideoSurface* videoSurface() const { return m_surface; } + void setVideoSurface(QAbstractVideoSurface *surface) + { + m_surface = surface; + } + + QAbstractVideoSurface *m_surface = nullptr; +}; +#endif + +static bool isStreamCurrent(int index, const QList &streams) +{ + for (const auto &stream: streams) { + if (stream.index() == index) + return true; + } + return false; +} + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQuickView viewer; + viewer.setSource(QUrl(QString::fromLatin1("qrc:///main.qml"))); + viewer.setResizeMode(QQuickView::SizeRootObjectToView); + QObject::connect(viewer.engine(), SIGNAL(quit()), &viewer, SLOT(close())); + + QQuickItem *rootObject = viewer.rootObject(); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + using VideoOutput = QDeclarativeVideoOutput; +#else + using VideoOutput = QQuickVideoOutput; +#endif + + auto vo = rootObject->findChild(QString::fromLatin1("videoOutput")); + + QAVAudioOutput audioOutput; + QAVPlayer p; + +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + Source src; + vo->setSource(&src); + auto videoSurface = src.m_surface; +#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto videoSurface = vo->videoSurface(); + // Make sure that render geometry has been updated after a frame + QObject::connect(vo, &QDeclarativeVideoOutput::sourceRectChanged, &p, [&] { + vo->update(); + }); +#else + auto videoSurface = vo->videoSink(); +#endif + + QObject::connect(&p, &QAVPlayer::videoFrame, &p, [&](const QAVVideoFrame &frame) { + rootObject->setProperty("frame_fps", p.progress(frame.stream()).fps()); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + // Might download and convert data + QVideoFrame videoFrame = frame; + if (!videoSurface->isActive()) + videoSurface->start({videoFrame.size(), videoFrame.pixelFormat(), videoFrame.handleType()}); + if (videoSurface->isActive()) + videoSurface->present(videoFrame); +#endif + }); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QObject::connect(&p, &QAVPlayer::videoFrame, &p, [&](const QAVVideoFrame &frame) { + // Might download and convert data + QVideoFrame videoFrame = frame; + videoSurface->setVideoFrame(videoFrame); + }, Qt::DirectConnection); +#endif + +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + //audioOutput.setChannelConfig(QAudioFormat::channelConfig(QAudioFormat::FrontLeft)); +#endif + + QObject::connect(&p, &QAVPlayer::audioFrame, &p, [&audioOutput](const QAVAudioFrame &frame) { audioOutput.play(frame); }, Qt::DirectConnection); + QString file = argc > 1 ? QString::fromUtf8(argv[1]) : QString::fromLatin1("http://archive.org/download/big-bunny-sample-video/SampleVideo.ia.mp4"); + QString filter = argc > 2 ? QString::fromUtf8(argv[2]) : QString(); + + QObject::connect(&p, &QAVPlayer::stateChanged, [&](auto s) { qDebug() << "stateChanged" << s << p.mediaStatus(); }); + QObject::connect(&p, &QAVPlayer::mediaStatusChanged, [&](auto status) { + qDebug() << "mediaStatusChanged"<< status << p.state(); + if (status == QAVPlayer::LoadedMedia) { + auto availableVideoStreams = p.availableVideoStreams(); + //p.setVideoStreams({}); + auto videoStreams = p.currentVideoStreams(); + qDebug() << "Video streams:" << availableVideoStreams.size(); + for (auto &s : p.availableVideoStreams()) + qDebug() << "[" << s.index() << "]" << s.metadata() << s.framesCount() << "frames," << s.frameRate() << "frame rate" << (isStreamCurrent(s.index(), videoStreams) ? "---current" : ""); + + auto availableAudioStreams = p.availableAudioStreams(); + auto audioStreams = p.currentAudioStreams(); + qDebug() << "Audio streams:" << availableAudioStreams.size(); + + for (auto &s : availableAudioStreams) + qDebug() << "[" << s.index() << "]" << s.metadata() << s.framesCount() << "frames," << s.frameRate() << "frame rate" << (isStreamCurrent(s.index(), audioStreams) ? "---current" : ""); + + auto availableSubtitleStreams = p.availableSubtitleStreams(); + qDebug() << "Subtitle streams:" << availableSubtitleStreams.size(); + for (auto &s : availableSubtitleStreams) { + if (s.metadata()[QString::fromLatin1("language")] == QString::fromLatin1("eng")) { + p.setSubtitleStream(s); + break; + } + } + + auto subtitleStreams = p.currentSubtitleStreams(); + for (auto &s : availableSubtitleStreams) { + qDebug() << "[" << s.index() << "]" << s.metadata() << s.framesCount() << "frames," << s.frameRate() << "frame rate" << (isStreamCurrent(s.index(), subtitleStreams) ? "---current" : ""); + } + p.play(); + } else if (status == QAVPlayer::EndOfMedia) { + for (const auto &s : p.availableVideoStreams()) + qDebug() << s << p.progress(s); + for (const auto &s : p.availableAudioStreams()) + qDebug() << s << p.progress(s); + for (const auto &s : p.availableSubtitleStreams()) + qDebug() << s << p.progress(s); + } + + }); + QObject::connect(&p, &QAVPlayer::durationChanged, [&](auto d) { qDebug() << "durationChanged" << d; }); + + QObject::connect(&p, &QAVPlayer::subtitleFrame, &p, [](const QAVSubtitleFrame &frame) { + for (unsigned i = 0; i < frame.subtitle()->num_rects; ++i) { + if (frame.subtitle()->rects[i]->type == SUBTITLE_TEXT) + qDebug() << "text:" << frame.subtitle()->rects[i]->text; + else + qDebug() << "ass:" << frame.subtitle()->rects[i]->ass; + } + }); + + QSharedPointer qrc; + if (file.startsWith(QString::fromLatin1(":/"))) { + QSharedPointer io(new QFile(file)); + if (io->open(QIODevice::ReadOnly)) + qrc.reset(new QAVIODevice(io)); + } + p.setSource(file, qrc); + p.setFilter(filter); + //p.setSynced(false); + + viewer.setMinimumSize(QSize(300, 360)); + viewer.resize(1960, 1086); + viewer.show(); + + QElapsedTimer qmlElapsed; + qmlElapsed.start(); + int qmlCount = 0; + + QObject::connect(&viewer, &QQuickView::afterRendering, &viewer, [&] { + const int fps = qmlCount++ * 1000 / qmlElapsed.elapsed(); + rootObject->setProperty("qml_fps", fps); + }); + + return app.exec(); +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) +#include "main.moc" +#endif diff --git a/QtAVPlayer/examples/qml_video/main.qml b/QtAVPlayer/examples/qml_video/main.qml new file mode 100644 index 0000000..42181f6 --- /dev/null +++ b/QtAVPlayer/examples/qml_video/main.qml @@ -0,0 +1,80 @@ +import QtQuick 2.5 +import QtMultimedia 5.12 + +Item { + id: root + property alias frame_fps: fpsTextVideo.text + property alias qml_fps: fpsTextQML.text + + VideoOutput { + anchors.fill: parent + objectName: "videoOutput" + } + + Rectangle { + id: fps + property color textColor: "white" + property int textSize: 30 + + border.width: 1 + border.color: "black" + width: 5.5 * fps.textSize + height: 2.5 * fps.textSize + color: "black" + opacity: 0.5 + radius: 0 + + // This should ensure that the monitor is on top of all other content + z: 999 + + Text { + id: labelText + anchors { + top: parent.top + left: parent.left + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + text: "Video FPS" + width: fps.width - 2*anchors.margins + elide: Text.ElideRight + } + + Text { + id: labelTextQML + anchors { + bottom: parent.bottom + left: parent.left + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + text: "QML FPS" + width: fps.width - 2*anchors.margins + elide: Text.ElideRight + } + + Text { + id: fpsTextVideo + anchors { + top: parent.top + right: parent.right + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + } + + Text { + id: fpsTextQML + anchors { + bottom: parent.bottom + right: parent.right + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + } + } +} diff --git a/QtAVPlayer/examples/qml_video/main_qt6.qml b/QtAVPlayer/examples/qml_video/main_qt6.qml new file mode 100644 index 0000000..bdbf7ec --- /dev/null +++ b/QtAVPlayer/examples/qml_video/main_qt6.qml @@ -0,0 +1,80 @@ +import QtQuick 2.5 +import QtMultimedia 6.2 + +Item { + id: root + property alias frame_fps: fpsTextVideo.text + property alias qml_fps: fpsTextQML.text + + VideoOutput { + anchors.fill: parent + objectName: "videoOutput" + } + + Rectangle { + id: fps + property color textColor: "white" + property int textSize: 30 + + border.width: 1 + border.color: "black" + width: 5.5 * fps.textSize + height: 2.5 * fps.textSize + color: "black" + opacity: 0.5 + radius: 0 + + // This should ensure that the monitor is on top of all other content + z: 999 + + Text { + id: labelText + anchors { + top: parent.top + left: parent.left + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + text: "Video FPS" + width: fps.width - 2*anchors.margins + elide: Text.ElideRight + } + + Text { + id: labelTextQML + anchors { + bottom: parent.bottom + left: parent.left + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + text: "QML FPS" + width: fps.width - 2*anchors.margins + elide: Text.ElideRight + } + + Text { + id: fpsTextVideo + anchors { + top: parent.top + right: parent.right + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + } + + Text { + id: fpsTextQML + anchors { + bottom: parent.bottom + right: parent.right + margins: 10 + } + color: fps.textColor + font.pixelSize: 0.6 * fps.textSize + } + } +} diff --git a/QtAVPlayer/examples/qml_video/qml.qrc b/QtAVPlayer/examples/qml_video/qml.qrc new file mode 100644 index 0000000..f85f45d --- /dev/null +++ b/QtAVPlayer/examples/qml_video/qml.qrc @@ -0,0 +1,6 @@ + + + main.qml + ../../tests/auto/integration/testdata/20190821_075842.jpg + + diff --git a/QtAVPlayer/examples/qml_video/qml_qt6.qrc b/QtAVPlayer/examples/qml_video/qml_qt6.qrc new file mode 100644 index 0000000..49ce550 --- /dev/null +++ b/QtAVPlayer/examples/qml_video/qml_qt6.qrc @@ -0,0 +1,6 @@ + + + main_qt6.qml + ../../tests/auto/integration/testdata/20190821_075842.jpg + + diff --git a/QtAVPlayer/examples/qml_video/qml_video.pro b/QtAVPlayer/examples/qml_video/qml_video.pro new file mode 100644 index 0000000..953a679 --- /dev/null +++ b/QtAVPlayer/examples/qml_video/qml_video.pro @@ -0,0 +1,25 @@ +TEMPLATE = app +TARGET = qml_video +DEFINES += "QT_AVPLAYER_MULTIMEDIA" +DEFINES += "QT_NO_CAST_FROM_ASCII" +INCLUDEPATH += . ../../src +include(../../src/QtAVPlayer/QtAVPlayer.pri) +# Example +#ANDROID_EXTRA_LIBS += /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavdevice.so \ +# /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavformat.so \ +# /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavutil.so \ +# /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavcodec.so \ +# /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libavfilter.so \ +# /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libswscale.so \ +# /opt/mobile-ffmpeg/prebuilt/android-arm/ffmpeg/lib/libswresample.so +CONFIG += c++1z +QT += gui multimedia +lessThan(QT_MAJOR_VERSION, 6): QT += qtmultimediaquicktools-private +equals(QT_MAJOR_VERSION, 6): QT += multimediaquick-private + +SOURCES += main.cpp +lessThan(QT_MAJOR_VERSION, 6): RESOURCES += qml.qrc +equals(QT_MAJOR_VERSION, 6): RESOURCES += qml_qt6.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/$$TARGET +INSTALLS += target diff --git a/QtAVPlayer/examples/widget_video/main.cpp b/QtAVPlayer/examples/widget_video/main.cpp new file mode 100644 index 0000000..0245634 --- /dev/null +++ b/QtAVPlayer/examples/widget_video/main.cpp @@ -0,0 +1,136 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#include +#include +#include +#include + +class VideoRenderer : public QVideoRendererControl +{ +public: + QAbstractVideoSurface *surface() const override + { + return m_surface; + } + + void setSurface(QAbstractVideoSurface *surface) override + { + m_surface = surface; + } + + QAbstractVideoSurface *m_surface = nullptr; +}; + +class MediaService : public QMediaService +{ +public: + MediaService(VideoRenderer *vr, QObject* parent = nullptr) + : QMediaService(parent) + , m_renderer(vr) + { + } + + QMediaControl* requestControl(const char *name) override + { + if (qstrcmp(name, QVideoRendererControl_iid) == 0) + return m_renderer; + + return nullptr; + } + + void releaseControl(QMediaControl *) override + { + } + + VideoRenderer *m_renderer = nullptr; +}; + +class MediaObject : public QMediaObject +{ +public: + explicit MediaObject(VideoRenderer *vr, QObject* parent = nullptr) + : QMediaObject(parent, new MediaService(vr, parent)) + { + } +}; + +class VideoWidget : public QVideoWidget +{ +public: + bool setMediaObject(QMediaObject *object) override + { + return QVideoWidget::setMediaObject(object); + } +}; +#else +#include +#endif + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + VideoRenderer vr; + + VideoWidget w; + w.show(); + + MediaObject mo(&vr); + w.setMediaObject(&mo); +#else + QVideoWidget w; + w.show(); +#endif + + QAVPlayer p; + QString file = argc > 1 ? QString::fromUtf8(argv[1]) : QString::fromLatin1("http://archive.org/download/big-bunny-sample-video/SampleVideo.ia.mp4"); + p.setSource(file); + p.play(); + + QAVAudioOutput audioOutput; + QObject::connect(&p, &QAVPlayer::audioFrame, &p, [&audioOutput](const QAVAudioFrame &frame) { audioOutput.play(frame); }, Qt::DirectConnection); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QObject::connect(&p, &QAVPlayer::videoFrame, &p, [&](const QAVVideoFrame &frame) { + if (vr.m_surface == nullptr) + return; + QVideoFrame videoFrame = frame.convertTo(AV_PIX_FMT_RGB32); + if (!vr.m_surface->isActive() || vr.m_surface->surfaceFormat().frameSize() != videoFrame.size()) { + QVideoSurfaceFormat f(videoFrame.size(), videoFrame.pixelFormat(), videoFrame.handleType()); + vr.m_surface->start(f); + } + if (vr.m_surface->isActive()) + vr.m_surface->present(videoFrame); + }, Qt::DirectConnection); +#else + QObject::connect(&p, &QAVPlayer::videoFrame, &p, [&](const QAVVideoFrame &frame) { + QVideoFrame videoFrame = frame; + w.videoSink()->setVideoFrame(videoFrame); + }, Qt::DirectConnection); +#endif + + QObject::connect(&p, &QAVPlayer::mediaStatusChanged, [&](auto status) { + qDebug() << "mediaStatusChanged"<< status << p.state(); + if (status == QAVPlayer::LoadedMedia) { + qDebug() << "Video streams:" << p.currentVideoStreams().size(); + for (const auto &s: p.currentVideoStreams()) + qDebug() << "[" << s.index() << "]" << s.metadata() << s.framesCount() << "frames," << s.frameRate() << "frame rate"; + } + }); + return app.exec(); +} + diff --git a/QtAVPlayer/examples/widget_video/widget_video.pro b/QtAVPlayer/examples/widget_video/widget_video.pro new file mode 100644 index 0000000..b30097c --- /dev/null +++ b/QtAVPlayer/examples/widget_video/widget_video.pro @@ -0,0 +1,14 @@ +TEMPLATE = app +TARGET = widget_video +DEFINES += "QT_AVPLAYER_MULTIMEDIA" +DEFINES += "QT_NO_CAST_FROM_ASCII" +INCLUDEPATH += . ../../src +include(../../src/QtAVPlayer/QtAVPlayer.pri) + +CONFIG += c++1z +QT += gui multimedia multimediawidgets + +SOURCES += main.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/$$TARGET +INSTALLS += target diff --git a/QtAVPlayer/src/QtAVPlayer/QtAVPlayer.pri b/QtAVPlayer/src/QtAVPlayer/QtAVPlayer.pri new file mode 100644 index 0000000..4382be6 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/QtAVPlayer.pri @@ -0,0 +1,131 @@ +QT += concurrent +CONFIG += C++1z +LIBS += -lavcodec -lavformat -lswscale -lavutil -lswresample -lswscale -lavfilter -lavdevice + +PRIVATE_HEADERS += \ + $$PWD/qavcodec_p.h \ + $$PWD/qavcodec_p_p.h \ + $$PWD/qavframecodec_p.h \ + $$PWD/qavaudiocodec_p.h \ + $$PWD/qavvideocodec_p.h \ + $$PWD/qavsubtitlecodec_p.h \ + $$PWD/qavhwdevice_p.h \ + $$PWD/qavdemuxer_p.h \ + $$PWD/qavpacket_p.h \ + $$PWD/qavstreamframe_p.h \ + $$PWD/qavframe_p.h \ + $$PWD/qavpacketqueue_p.h \ + $$PWD/qavvideobuffer_p.h \ + $$PWD/qavvideobuffer_cpu_p.h \ + $$PWD/qavvideobuffer_gpu_p.h \ + $$PWD/qavfilter_p.h \ + $$PWD/qavfilter_p_p.h \ + $$PWD/qavvideofilter_p.h \ + $$PWD/qavaudiofilter_p.h \ + $$PWD/qavfiltergraph_p.h \ + $$PWD/qavinoutfilter_p.h \ + $$PWD/qavinoutfilter_p_p.h \ + $$PWD/qavvideoinputfilter_p.h \ + $$PWD/qavaudioinputfilter_p.h \ + $$PWD/qavvideooutputfilter_p.h \ + $$PWD/qavaudiooutputfilter_p.h \ + $$PWD/qavfilters_p.h + +PUBLIC_HEADERS += \ + $$PWD/qaviodevice.h \ + $$PWD/qavaudioformat.h \ + $$PWD/qavstreamframe.h \ + $$PWD/qavframe.h \ + $$PWD/qavvideoframe.h \ + $$PWD/qavaudioframe.h \ + $$PWD/qavsubtitleframe.h \ + $$PWD/qtavplayerglobal.h \ + $$PWD/qavstream.h \ + $$PWD/qavplayer.h \ + +SOURCES += \ + $$PWD/qavplayer.cpp \ + $$PWD/qavcodec.cpp \ + $$PWD/qavframecodec.cpp \ + $$PWD/qavaudiocodec.cpp \ + $$PWD/qavvideocodec.cpp \ + $$PWD/qavsubtitlecodec.cpp \ + $$PWD/qavdemuxer.cpp \ + $$PWD/qavpacket.cpp \ + $$PWD/qavframe.cpp \ + $$PWD/qavstreamframe.cpp \ + $$PWD/qavvideoframe.cpp \ + $$PWD/qavaudioframe.cpp \ + $$PWD/qavsubtitleframe.cpp \ + $$PWD/qavvideobuffer_cpu.cpp \ + $$PWD/qavvideobuffer_gpu.cpp \ + $$PWD/qavfilter.cpp \ + $$PWD/qavvideofilter.cpp \ + $$PWD/qavaudiofilter.cpp \ + $$PWD/qavfiltergraph.cpp \ + $$PWD/qavinoutfilter.cpp \ + $$PWD/qavvideoinputfilter.cpp \ + $$PWD/qavaudioinputfilter.cpp \ + $$PWD/qavvideooutputfilter.cpp \ + $$PWD/qavaudiooutputfilter.cpp \ + $$PWD/qaviodevice.cpp \ + $$PWD/qavstream.cpp \ + $$PWD/qavfilters.cpp + +contains(DEFINES, QT_AVPLAYER_MULTIMEDIA) { + QT += multimedia + # Needed for QAbstractVideoBuffer + equals(QT_MAJOR_VERSION, 6): QT += multimedia-private + HEADERS += $$PWD/qavaudiooutput.h + SOURCES += $$PWD/qavaudiooutput.cpp +} + +contains(DEFINES, QT_AVPLAYER_VA_X11):qtConfig(opengl) { + QMAKE_USE += x11 opengl + LIBS += -lva-x11 -lva + PRIVATE_HEADERS += $$PWD/qavhwdevice_vaapi_x11_glx_p.h + SOURCES += $$PWD/qavhwdevice_vaapi_x11_glx.cpp +} + +contains(DEFINES, QT_AVPLAYER_VA_DRM):qtConfig(egl) { + QMAKE_USE += egl opengl + LIBS += -lva-drm -lva + PRIVATE_HEADERS += $$PWD/qavhwdevice_vaapi_drm_egl_p.h + SOURCES += $$PWD/qavhwdevice_vaapi_drm_egl.cpp +} + +contains(DEFINES, QT_AVPLAYER_VDPAU) { + PRIVATE_HEADERS += $$PWD/qavhwdevice_vdpau_p.h + SOURCES += $$PWD/qavhwdevice_vdpau.cpp +} + +macos|darwin { + PRIVATE_HEADERS += $$PWD/qavhwdevice_videotoolbox_p.h + SOURCES += $$PWD/qavhwdevice_videotoolbox.mm + LIBS += -framework CoreVideo -framework Metal -framework CoreMedia -framework QuartzCore -framework IOSurface +} + +win32 { + PRIVATE_HEADERS += $$PWD/qavhwdevice_d3d11_p.h + SOURCES += $$PWD/qavhwdevice_d3d11.cpp +} + +android { + QT += core-private + PRIVATE_HEADERS += $$PWD/qavhwdevice_mediacodec_p.h + SOURCES += $$PWD/qavhwdevice_mediacodec.cpp $$PWD/qavandroidsurfacetexture.cpp + + equals(ANDROID_TARGET_ARCH, armeabi-v7a): \ + LIBS += -L$$(AVPLAYER_ANDROID_LIB_ARMEABI_V7A) + + equals(ANDROID_TARGET_ARCH, arm64-v8a): \ + LIBS += -L$$(AVPLAYER_ANDROID_LIB_ARMEABI_V8A) + + equals(ANDROID_TARGET_ARCH, x86): \ + LIBS += -L$$(AVPLAYER_ANDROID_LIB_X86) + + equals(ANDROID_TARGET_ARCH, x86_64): \ + LIBS += -L$$(AVPLAYER_ANDROID_LIB_X86_64) +} + +HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS diff --git a/QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture.cpp b/QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture.cpp new file mode 100644 index 0000000..d6c9d74 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture.cpp @@ -0,0 +1,74 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavandroidsurfacetexture_p.h" +#include +#include + +QT_BEGIN_NAMESPACE + +QAVAndroidSurfaceTexture::QAVAndroidSurfaceTexture(quint32 texName) +{ + Q_STATIC_ASSERT(sizeof (jlong) >= sizeof (void *)); + m_surfaceTexture = JniObject("android/graphics/SurfaceTexture", "(I)V", jint(texName)); +} + +QAVAndroidSurfaceTexture::~QAVAndroidSurfaceTexture() +{ + if (m_surface.isValid()) + m_surface.callMethod("release"); + + if (m_surfaceTexture.isValid()) + release(); +} + +void QAVAndroidSurfaceTexture::release() +{ + m_surfaceTexture.callMethod("release"); +} + +void QAVAndroidSurfaceTexture::updateTexImage() +{ + if (!m_surfaceTexture.isValid()) + return; + + m_surfaceTexture.callMethod("updateTexImage"); +} + +jobject QAVAndroidSurfaceTexture::surfaceTexture() +{ + return m_surfaceTexture.object(); +} + +jobject QAVAndroidSurfaceTexture::surface() +{ + if (!m_surface.isValid()) { + m_surface = JniObject("android/view/Surface", + "(Landroid/graphics/SurfaceTexture;)V", + m_surfaceTexture.object()); + } + + return m_surface.object(); +} + +void QAVAndroidSurfaceTexture::attachToGLContext(quint32 texName) +{ + if (!m_surfaceTexture.isValid()) + return; + + m_surfaceTexture.callMethod("attachToGLContext", "(I)V", texName); +} + +void QAVAndroidSurfaceTexture::detachFromGLContext() +{ + if (!m_surfaceTexture.isValid()) + return; + + m_surfaceTexture.callMethod("detachFromGLContext"); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture_p.h b/QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture_p.h new file mode 100644 index 0000000..1b0c094 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture_p.h @@ -0,0 +1,61 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVANDROIDSURFACETEXTURE_H +#define QAVANDROIDSURFACETEXTURE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#include +using JniObject = QJNIObjectPrivate; +using JniEnvironment = QJNIEnvironmentPrivate; +#else +#include +using JniObject = QJniObject; +using JniEnvironment = QJniEnvironment; +#endif + +#include + +QT_BEGIN_NAMESPACE + +class QAVAndroidSurfaceTexture +{ +public: + explicit QAVAndroidSurfaceTexture(quint32 texName = 0); + ~QAVAndroidSurfaceTexture(); + + jobject surfaceTexture(); + jobject surface(); + inline bool isValid() const { return m_surfaceTexture.isValid(); } + + void release(); // API level 14 + void updateTexImage(); + + void attachToGLContext(quint32 texName); // API level 16 + void detachFromGLContext(); // API level 16 + +private: + JniObject m_surfaceTexture; + JniObject m_surface; +}; + +QT_END_NAMESPACE + +#endif // QAVANDROIDSURFACETEXTURE_H diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiocodec.cpp b/QtAVPlayer/src/QtAVPlayer/qavaudiocodec.cpp new file mode 100644 index 0000000..cab76f0 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiocodec.cpp @@ -0,0 +1,49 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavaudiocodec_p.h" +#include "qavcodec_p_p.h" +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +QAVAudioCodec::QAVAudioCodec() +{ +} + +QAVAudioFormat QAVAudioCodec::audioFormat() const +{ + Q_D(const QAVCodec); + QAVAudioFormat format; + if (!d->avctx) + return format; + + auto fmt = AVSampleFormat(d->avctx->sample_fmt); + if (fmt == AV_SAMPLE_FMT_U8) + format.setSampleFormat(QAVAudioFormat::UInt8); + else if (fmt == AV_SAMPLE_FMT_S16) + format.setSampleFormat(QAVAudioFormat::Int16); + else if (fmt == AV_SAMPLE_FMT_S32) + format.setSampleFormat(QAVAudioFormat::Int32); + else if (fmt == AV_SAMPLE_FMT_FLT) + format.setSampleFormat(QAVAudioFormat::Float); + + format.setSampleRate(d->avctx->sample_rate); +#if LIBAVCODEC_VERSION_INT <= AV_VERSION_INT(59, 23, 0) + format.setChannelCount(d->avctx->channels); +#else + format.setChannelCount(d->avctx->ch_layout.nb_channels); +#endif + + return format; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiocodec_p.h b/QtAVPlayer/src/QtAVPlayer/qavaudiocodec_p.h new file mode 100644 index 0000000..2fdabc9 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiocodec_p.h @@ -0,0 +1,39 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVAUDIOCODEC_P_H +#define QAVAUDIOCODEC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavframecodec_p.h" +#include "qavaudioformat.h" + +QT_BEGIN_NAMESPACE + +class QAVAudioCodec : public QAVFrameCodec +{ +public: + QAVAudioCodec(); + QAVAudioFormat audioFormat() const; + +private: + Q_DISABLE_COPY(QAVAudioCodec) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiofilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavaudiofilter.cpp new file mode 100644 index 0000000..c92c1d5 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiofilter.cpp @@ -0,0 +1,155 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavaudiofilter_p.h" +#include "qavfilter_p_p.h" +#include "qavcodec_p.h" +#include "qavstream.h" +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVAudioFilterPrivate : public QAVFilterPrivate +{ +public: + QAVAudioFilterPrivate(QAVFilter *q, QMutex &mutex) : QAVFilterPrivate(q, mutex) { } + + QList inputs; + QList outputs; + int64_t filter_in_rescale_delta_last = AV_NOPTS_VALUE; +}; + +QAVAudioFilter::QAVAudioFilter( + const QAVStream &stream, + const QString &name, + const QList &inputs, + const QList &outputs, + QMutex &mutex) + : QAVFilter( + stream, + name, + *new QAVAudioFilterPrivate(this, mutex)) +{ + Q_D(QAVAudioFilter); + d->inputs = inputs; + d->outputs = outputs; +} + +int QAVAudioFilter::write(const QAVFrame &frame) +{ + Q_D(QAVAudioFilter); + if (!frame || frame.stream().stream()->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { + qWarning() << "Frame is not audio"; + return AVERROR(EINVAL); + } + if (!d->isEmpty) + return AVERROR(EAGAIN); + + d->sourceFrame = frame; + AVFrame *decoded_frame = d->sourceFrame.frame(); + AVRational decoded_frame_tb = d->sourceFrame.stream().stream()->time_base; + // TODO: clear filter_in_rescale_delta_last + if (!d->inputs.isEmpty() && decoded_frame->pts != AV_NOPTS_VALUE) { + decoded_frame->pts = av_rescale_delta(decoded_frame_tb, decoded_frame->pts, + AVRational{1, decoded_frame->sample_rate}, + decoded_frame->nb_samples, + &d->filter_in_rescale_delta_last, + AVRational{1, decoded_frame->sample_rate}); + } + + for (auto &filter : d->inputs) { + QAVFrame ref = d->sourceFrame; + QMutexLocker locker(&d->graphMutex); + int ret = av_buffersrc_add_frame_flags(filter.ctx(), ref.frame(), AV_BUFFERSRC_FLAG_PUSH); + if (ret < 0) + return ret; + } + d->isEmpty = false; + return 0; +} + +int QAVAudioFilter::read(QAVFrame &frame) +{ + Q_D(QAVAudioFilter); + if (d->outputs.isEmpty() || d->isEmpty) { + int ret = AVERROR(EAGAIN); + if (d->sourceFrame && d->outputs.isEmpty()) { + frame = d->sourceFrame; + ret = 0; + } + d->sourceFrame = {}; + d->isEmpty = true; + return ret; + } + + int ret = 0; + if (d->outputFrames.isEmpty()) { + for (int i = 0; i < d->outputs.size(); ++i) { + const auto &filter = d->outputs[i]; + while (true) { + QAVFrame out = d->sourceFrame; + // av_buffersink_get_frame_flags allocates frame's data + av_frame_unref(out.frame()); + { + QMutexLocker locker(&d->graphMutex); + ret = av_buffersink_get_frame_flags(filter.ctx(), out.frame(), 0); + } + if (ret < 0) + break; + +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 30, 0) + if (!out.frame()->pkt_duration) + out.frame()->pkt_duration = d->sourceFrame.frame()->pkt_duration; +#else + if (out.frame()->duration == AV_NOPTS_VALUE || out.frame()->duration == 0) + out.frame()->duration = d->sourceFrame.frame()->duration; +#endif + out.setFrameRate(av_buffersink_get_frame_rate(filter.ctx())); + out.setTimeBase(av_buffersink_get_time_base(filter.ctx())); + out.setFilterName( + !filter.name().isEmpty() + ? filter.name() + : QString(QLatin1String("%1:%2")).arg(d->name).arg(QString::number(i))); + if (!out.stream()) + out.setStream(d->stream); + d->outputFrames.push_back(out); + } + } + } + + ret = AVERROR(EAGAIN); + if (!d->outputFrames.isEmpty()) { + frame = d->outputFrames.takeFirst(); + ret = 0; + } + if (d->outputFrames.isEmpty()) { + d->sourceFrame = {}; + d->isEmpty = true; + } + return ret; +} + +void QAVAudioFilter::flush() +{ + Q_D(QAVAudioFilter); + for (const auto &filter : d->inputs) { + int ret = av_buffersrc_add_frame(filter.ctx(), nullptr); + if (ret < 0) + qWarning() << "Could not flush:" << ret; + } + d->isEmpty = false; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiofilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavaudiofilter_p.h new file mode 100644 index 0000000..0d95bab --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiofilter_p.h @@ -0,0 +1,53 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVAUDIOFILTER_P_H +#define QAVAUDIOFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavfilter_p.h" +#include "qavaudioinputfilter_p.h" +#include "qavaudiooutputfilter_p.h" +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVAudioFilterPrivate; +class QAVAudioFilter : public QAVFilter +{ +public: + QAVAudioFilter( + const QAVStream &stream, + const QString &name, + const QList &inputs, + const QList &outputs, + QMutex &mutex); + + int write(const QAVFrame &frame) override; + int read(QAVFrame &frame) override; + void flush() override; + +protected: + Q_DECLARE_PRIVATE(QAVAudioFilter) +private: + Q_DISABLE_COPY(QAVAudioFilter) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudioformat.h b/QtAVPlayer/src/QtAVPlayer/qavaudioformat.h new file mode 100644 index 0000000..af64adc --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudioformat.h @@ -0,0 +1,61 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVAUDIOFORMAT_H +#define QAVAUDIOFORMAT_H + +#include + +QT_BEGIN_NAMESPACE + +class QAVAudioFormat +{ +public: + enum SampleFormat + { + Unknown, + UInt8, + Int16, + Int32, + Float + }; + + SampleFormat sampleFormat() const { return m_sampleFormat; } + void setSampleFormat(SampleFormat f) { m_sampleFormat = f; } + + int sampleRate() const { return m_sampleRate; } + void setSampleRate(int sampleRate) { m_sampleRate = sampleRate; } + + int channelCount() const { return m_channelCount; } + void setChannelCount(int channelCount) { m_channelCount = channelCount; } + + operator bool() const + { + return m_sampleFormat != SampleFormat::Unknown && m_sampleRate != 0 && m_channelCount != 0; + } + + friend bool operator==(const QAVAudioFormat &a, const QAVAudioFormat &b) + { + return a.m_sampleRate == b.m_sampleRate && + a.m_channelCount == b.m_channelCount && + a.m_sampleFormat == b.m_sampleFormat; + } + + friend bool operator!=(const QAVAudioFormat &a, const QAVAudioFormat &b) + { + return !(a == b); + } + +private: + SampleFormat m_sampleFormat = Unknown; + int m_sampleRate = 0; + int m_channelCount = 0; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudioframe.cpp b/QtAVPlayer/src/QtAVPlayer/qavaudioframe.cpp new file mode 100644 index 0000000..9e0fe55 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudioframe.cpp @@ -0,0 +1,216 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavaudioframe.h" +#include "qavframe_p.h" +#include "qavaudiocodec_p.h" +#include + +extern "C" { +#include "libswresample/swresample.h" +} + +QT_BEGIN_NAMESPACE + +class QAVAudioFramePrivate : public QAVFramePrivate +{ +public: + QAVAudioFormat outAudioFormat; + int inSampleRate = 0; + SwrContext *swr_ctx = nullptr; + uint8_t *audioBuf = nullptr; + QByteArray data; +}; + +QAVAudioFrame::QAVAudioFrame() + : QAVFrame(*new QAVAudioFramePrivate) +{ +} + +QAVAudioFrame::~QAVAudioFrame() +{ + Q_D(QAVAudioFrame); + swr_free(&d->swr_ctx); + av_freep(&d->audioBuf); +} + +QAVAudioFrame::QAVAudioFrame(const QAVFrame &other) + : QAVFrame(*new QAVAudioFramePrivate) +{ + operator=(other); +} + +QAVAudioFrame::QAVAudioFrame(const QAVAudioFrame &other) + : QAVFrame(*new QAVAudioFramePrivate) +{ + operator=(other); +} + +QAVAudioFrame::QAVAudioFrame(const QAVAudioFormat &format, const QByteArray &data) + : QAVAudioFrame() +{ + Q_D(QAVAudioFrame); + d->outAudioFormat = format; + d->data = data; +} + +QAVAudioFrame &QAVAudioFrame::operator=(const QAVFrame &other) +{ + Q_D(QAVAudioFrame); + QAVFrame::operator=(other); + d->data.clear(); + + return *this; +} + +QAVAudioFrame &QAVAudioFrame::operator=(const QAVAudioFrame &other) +{ + Q_D(QAVAudioFrame); + QAVFrame::operator=(other); + auto rhs = reinterpret_cast(other.d_ptr.get()); + d->outAudioFormat = rhs->outAudioFormat; + d->data = rhs->data; + + return *this; +} + +QAVAudioFrame::operator bool() const +{ + Q_D(const QAVAudioFrame); + return (d->outAudioFormat &&!d->data.isEmpty()) || QAVFrame::operator bool(); +} + +static const QAVAudioCodec *audioCodec(const QAVCodec *c) +{ + return reinterpret_cast(c); +} + +QAVAudioFormat QAVAudioFrame::format() const +{ + Q_D(const QAVAudioFrame); + if (d->outAudioFormat) + return d->outAudioFormat; + + if (!d->stream) + return {}; + + auto c = audioCodec(d->stream.codec().data()); + if (!c) + return {}; + + auto format = c->audioFormat(); + if (format.sampleFormat() != QAVAudioFormat::Int32) + format.setSampleFormat(QAVAudioFormat::Int32); + + return format; +} + +QByteArray QAVAudioFrame::data() const +{ + auto d = const_cast(reinterpret_cast(d_ptr.get())); + const auto frame = d->frame; + if (!frame) + return {}; + + if (d->outAudioFormat && !d->data.isEmpty()) + return d->data; + + const auto fmt = format(); + AVSampleFormat outFormat = AV_SAMPLE_FMT_NONE; +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + int64_t outChannelLayout = av_get_default_channel_layout(fmt.channelCount()); +#else + AVChannelLayout outChannelLayout; + av_channel_layout_default(&outChannelLayout, fmt.channelCount()); +#endif + int outSampleRate = fmt.sampleRate(); + + switch (fmt.sampleFormat()) { + case QAVAudioFormat::UInt8: + outFormat = AV_SAMPLE_FMT_U8; + break; + case QAVAudioFormat::Int16: + outFormat = AV_SAMPLE_FMT_S16; + break; + case QAVAudioFormat::Int32: + outFormat = AV_SAMPLE_FMT_S32; + break; + case QAVAudioFormat::Float: + outFormat = AV_SAMPLE_FMT_FLT; + break; + default: + qWarning() << "Could not negotiate output format:" << fmt.sampleFormat(); + return {}; + } + +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + int64_t channelLayout = (frame->channel_layout && frame->channels == av_get_channel_layout_nb_channels(frame->channel_layout)) + ? frame->channel_layout + : av_get_default_channel_layout(frame->channels); + bool needsConvert = frame->format != outFormat || channelLayout != outChannelLayout || frame->sample_rate != outSampleRate; +#else + AVChannelLayout channelLayout =frame->ch_layout; + bool needsConvert = frame->format != outFormat || av_channel_layout_compare(&channelLayout, &outChannelLayout) || frame->sample_rate != outSampleRate; +#endif + + if (needsConvert && (fmt != d->outAudioFormat || frame->sample_rate != d->inSampleRate || !d->swr_ctx)) { + swr_free(&d->swr_ctx); +#if LIBSWRESAMPLE_VERSION_INT <= AV_VERSION_INT(4, 4, 0) + d->swr_ctx = swr_alloc_set_opts(nullptr, + outChannelLayout, outFormat, outSampleRate, + channelLayout, AVSampleFormat(frame->format), frame->sample_rate, + 0, nullptr); +#else + swr_alloc_set_opts2(&d->swr_ctx, + &outChannelLayout, outFormat, outSampleRate, + &channelLayout, AVSampleFormat(frame->format), frame->sample_rate, + 0, nullptr); +#endif + int ret = swr_init(d->swr_ctx); + if (!d->swr_ctx || ret < 0) { + qWarning() << "Could not init SwrContext:" << ret; + return {}; + } + } + + if (d->swr_ctx) { + const uint8_t **in = (const uint8_t **)frame->extended_data; + int outCount = (int64_t)frame->nb_samples * outSampleRate / frame->sample_rate + 256; + int outSize = av_samples_get_buffer_size(nullptr, fmt.channelCount(), outCount, outFormat, 0); + + av_freep(&d->audioBuf); + uint8_t **out = &d->audioBuf; + unsigned bufSize = 0; + av_fast_malloc(&d->audioBuf, &bufSize, outSize); + + int samples = swr_convert(d->swr_ctx, out, outCount, in, frame->nb_samples); + if (samples < 0) { + qWarning() << "Could not convert audio samples"; + return {}; + } + + int size = samples * fmt.channelCount() * av_get_bytes_per_sample(outFormat); + // Make deep copy + d->data = QByteArray((const char *)d->audioBuf, size); + } else { + int size = av_samples_get_buffer_size(nullptr, +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + frame->channels, +#else + outChannelLayout.nb_channels, +#endif + frame->nb_samples, + AVSampleFormat(frame->format), 1); + d->data = QByteArray::fromRawData((const char *)frame->data[0], size); + } + + d->inSampleRate = frame->sample_rate; + d->outAudioFormat = fmt; + return d->data; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudioframe.h b/QtAVPlayer/src/QtAVPlayer/qavaudioframe.h new file mode 100644 index 0000000..bc36c16 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudioframe.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFAUDIORAME_H +#define QAVFAUDIORAME_H + +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVAudioCodec; +class QAVAudioFramePrivate; +class QAVAudioFrame : public QAVFrame +{ +public: + QAVAudioFrame(); + ~QAVAudioFrame(); + QAVAudioFrame(const QAVFrame &other); + QAVAudioFrame(const QAVAudioFrame &other); + QAVAudioFrame(const QAVAudioFormat &format, const QByteArray &data); + QAVAudioFrame &operator=(const QAVFrame &other); + QAVAudioFrame &operator=(const QAVAudioFrame &other); + operator bool() const; + + QAVAudioFormat format() const; + QByteArray data() const; + +private: + Q_DECLARE_PRIVATE(QAVAudioFrame) +}; + +Q_DECLARE_METATYPE(QAVAudioFrame) + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter.cpp new file mode 100644 index 0000000..02dbb29 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter.cpp @@ -0,0 +1,127 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#if !defined(__STDC_FORMAT_MACROS) +#define __STDC_FORMAT_MACROS 1 +#endif +#include + +#include "qavframe.h" +#include "qavaudioinputfilter_p.h" +#include "qavinoutfilter_p_p.h" +#include "qavdemuxer_p.h" +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVAudioInputFilterPrivate : public QAVInOutFilterPrivate +{ +public: + QAVAudioInputFilterPrivate(QAVInOutFilter *q) + : QAVInOutFilterPrivate(q) + { } + + AVSampleFormat format = AV_SAMPLE_FMT_NONE; + int sample_rate = 0; +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + uint64_t channel_layout = 0; + int channels = 0; +#else + AVChannelLayout ch_layout; +#endif +}; + +QAVAudioInputFilter::QAVAudioInputFilter() + : QAVInOutFilter(*new QAVAudioInputFilterPrivate(this)) +{ +} + +QAVAudioInputFilter::QAVAudioInputFilter(const QAVFrame &frame) + : QAVAudioInputFilter() +{ + Q_D(QAVAudioInputFilter); + const auto & frm = frame.frame(); + const auto & stream = frame.stream().stream(); + d->format = frm->format != AV_SAMPLE_FMT_NONE ? AVSampleFormat(frm->format) : AVSampleFormat(stream->codecpar->format); + d->sample_rate = frm->sample_rate ? frm->sample_rate : stream->codecpar->sample_rate; +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + d->channel_layout = frm->channel_layout ? frm->channel_layout : stream->codecpar->channel_layout; + d->channels = frm->channels ? frm->channels : stream->codecpar->channels; +#else + d->ch_layout = frm->ch_layout.order != AV_CHANNEL_ORDER_UNSPEC ? frm->ch_layout : stream->codecpar->ch_layout; +#endif +} + +QAVAudioInputFilter::QAVAudioInputFilter(const QAVAudioInputFilter &other) + : QAVAudioInputFilter() +{ + *this = other; +} + +QAVAudioInputFilter::~QAVAudioInputFilter() = default; + +QAVAudioInputFilter &QAVAudioInputFilter::operator=(const QAVAudioInputFilter &other) +{ + Q_D(QAVAudioInputFilter); + QAVInOutFilter::operator=(other); + d->format = other.d_func()->format; + d->sample_rate = other.d_func()->sample_rate; +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + d->channel_layout = other.d_func()->channel_layout; + d->channels = other.d_func()->channels; +#else + d->ch_layout = other.d_func()->ch_layout; +#endif + return *this; +} + +int QAVAudioInputFilter::configure(AVFilterGraph *graph, AVFilterInOut *in) +{ + QAVInOutFilter::configure(graph, in); + Q_D(QAVAudioInputFilter); + AVBPrint args; + av_bprint_init(&args, 0, AV_BPRINT_SIZE_AUTOMATIC); + av_bprintf(&args, "time_base=%d/%d:sample_rate=%d:sample_fmt=%s", + 1, d->sample_rate, + d->sample_rate, + av_get_sample_fmt_name(AVSampleFormat(d->format))); + +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 23, 0) + if (d->channel_layout) + av_bprintf(&args, ":channel_layout=0x%" PRIx64, d->channel_layout); + else + av_bprintf(&args, ":channels=%d", d->channels); +#else + if (av_channel_layout_check(&d->ch_layout) && + d->ch_layout.order != AV_CHANNEL_ORDER_UNSPEC) { + av_bprintf(&args, ":channel_layout="); + av_channel_layout_describe_bprint(&d->ch_layout, &args); + } else { + av_bprintf(&args, ":channels=%d", d->ch_layout.nb_channels); + } +#endif + + char name[255]; + static int index = 0; + snprintf(name, sizeof(name), "abuffer_%d", index++); + + int ret = avfilter_graph_create_filter(&d->ctx, + avfilter_get_by_name("abuffer"), + name, args.str, nullptr, graph); + if (ret < 0) + return ret; + + return avfilter_link(d->ctx, 0, in->filter_ctx, in->pad_idx); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter_p.h new file mode 100644 index 0000000..a42a986 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter_p.h @@ -0,0 +1,45 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVAUDIOINPUTFILTER_P_H +#define QAVAUDIOINPUTFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavinoutfilter_p.h" + +QT_BEGIN_NAMESPACE + +class QAVFrame; +class QAVAudioInputFilterPrivate; +class QAVAudioInputFilter : public QAVInOutFilter +{ +public: + QAVAudioInputFilter(const QAVFrame &frame); + QAVAudioInputFilter(const QAVAudioInputFilter &other); + ~QAVAudioInputFilter(); + QAVAudioInputFilter &operator=(const QAVAudioInputFilter &other); + + int configure(AVFilterGraph *graph, AVFilterInOut *in) override; + +protected: + QAVAudioInputFilter(); + Q_DECLARE_PRIVATE(QAVAudioInputFilter) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiooutput.cpp b/QtAVPlayer/src/QtAVPlayer/qavaudiooutput.cpp new file mode 100644 index 0000000..1bff3e8 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiooutput.cpp @@ -0,0 +1,305 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavaudiooutput.h" +#include +#include +#include +#include +#include +#include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#include +#endif + +extern "C" { +#include "libavutil/time.h" +} + +QT_BEGIN_NAMESPACE + +static QAudioFormat format(const QAVAudioFormat &from) +{ + QAudioFormat out; + + out.setSampleRate(from.sampleRate()); + out.setChannelCount(from.channelCount()); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + out.setByteOrder(QAudioFormat::LittleEndian); + out.setCodec(QLatin1String("audio/pcm")); +#endif + switch (from.sampleFormat()) { + case QAVAudioFormat::UInt8: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + out.setSampleSize(8); + out.setSampleType(QAudioFormat::UnSignedInt); +#else + out.setSampleFormat(QAudioFormat::UInt8); +#endif + break; + case QAVAudioFormat::Int16: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + out.setSampleSize(16); + out.setSampleType(QAudioFormat::SignedInt); +#else + out.setSampleFormat(QAudioFormat::Int16); +#endif + break; + case QAVAudioFormat::Int32: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + out.setSampleSize(32); + out.setSampleType(QAudioFormat::SignedInt); +#else + out.setSampleFormat(QAudioFormat::Int32); +#endif + break; + case QAVAudioFormat::Float: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + out.setSampleSize(32); + out.setSampleType(QAudioFormat::Float); +#else + out.setSampleFormat(QAudioFormat::Float); +#endif + break; + default: + qWarning() << "Could not negotiate output format:" << from.sampleFormat(); + return {}; + } + + return out; +} + +class QAVAudioOutputPrivate : public QIODevice +{ +public: + QAVAudioOutputPrivate() + { + open(QIODevice::ReadOnly); + threadPool.setMaxThreadCount(1); + } + + QFuture audioPlayFuture; + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + using AudioOutput = QAudioOutput; + using AudioDevice = QAudioDeviceInfo; +#else + using AudioOutput = QAudioSink; + using AudioDevice = QAudioDevice; +#endif + AudioOutput *audioOutput = nullptr; + qreal volume = 1.0; + int bufferSize = 0; +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + QAudioFormat::ChannelConfig channelConfig = QAudioFormat::ChannelConfigUnknown; +#endif + + QList frames; + qint64 offset = 0; + bool quit = 0; + AudioDevice defaultAudioDevice; + mutable QMutex mutex; + QWaitCondition cond; + QThreadPool threadPool; + + qint64 readData(char *data, qint64 len) override + { + if (!len) + return 0; + + QMutexLocker locker(&mutex); + qint64 bytesWritten = 0; + while (len && !quit) { + if (frames.isEmpty()) { + // Wait for more frames + if (bytesWritten == 0) + cond.wait(&mutex); + if (frames.isEmpty()) + break; + } + + auto frame = frames.front(); + auto sampleData = frame.data(); + const int toWrite = qMin(sampleData.size() - offset, len); + memcpy(data, sampleData.constData() + offset, toWrite); + bytesWritten += toWrite; + data += toWrite; + len -= toWrite; + offset += toWrite; + + if (offset >= sampleData.size()) { + offset = 0; + frames.removeFirst(); + } + } + + return bytesWritten; + } + + qint64 writeData(const char *, qint64) override { return 0; } + qint64 size() const override { return 0; } + qint64 bytesAvailable() const override { return std::numeric_limits::max(); } + bool isSequential() const override { return true; } + bool atEnd() const override { return false; } + + void tryInit(const QAudioFormat &fmt, int bsize, qreal v) + { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto audioDevice = QAudioDeviceInfo::defaultOutputDevice(); +#else + auto audioDevice = QMediaDevices::defaultAudioOutput(); +#endif + + if (!audioOutput + || (fmt.isValid() && audioOutput->format() != fmt) + || audioOutput->state() == QAudio::StoppedState + || defaultAudioDevice != audioDevice) + { + if (audioOutput) { + audioOutput->stop(); + audioOutput->deleteLater(); + } + + audioOutput = new AudioOutput(audioDevice, fmt); + defaultAudioDevice = audioDevice; + + QObject::connect(audioOutput, &AudioOutput::stateChanged, audioOutput, + [&](QAudio::State state) { + switch (state) { + case QAudio::StoppedState: + if (audioOutput->error() != QAudio::NoError) + qWarning() << "QAudioOutput stopped:" << audioOutput->error(); + break; + default: + break; + } + }); + + if (bsize > 0) + audioOutput->setBufferSize(bsize); + audioOutput->setVolume(v); + audioOutput->start(this); + } + } + + void doPlayAudio() + { + while (!quit) { + QMutexLocker locker(&mutex); + cond.wait(&mutex); + auto fmt = !frames.isEmpty() ? format(frames.first().format()) : QAudioFormat(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + fmt.setChannelConfig(channelConfig); +#endif + auto v = volume; + auto bsize = bufferSize; + locker.unlock(); + if (fmt.isValid()) + tryInit(fmt, bsize, v); + if (audioOutput) + audioOutput->setVolume(v); + QCoreApplication::processEvents(); + } + if (audioOutput) { + audioOutput->stop(); + audioOutput->deleteLater(); + } + audioOutput = nullptr; + } + + void startThreadIfNeeded() + { + if (!audioPlayFuture.isRunning()) { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + audioPlayFuture = QtConcurrent::run(&threadPool, this, &QAVAudioOutputPrivate::doPlayAudio); +#else + audioPlayFuture = QtConcurrent::run(&threadPool, &QAVAudioOutputPrivate::doPlayAudio, this); +#endif + } + } +}; + +QAVAudioOutput::QAVAudioOutput(QObject *parent) + : QObject(parent) + , d_ptr(new QAVAudioOutputPrivate) +{ +} + +QAVAudioOutput::~QAVAudioOutput() +{ + Q_D(QAVAudioOutput); + d->quit = true; + d->cond.wakeAll(); + d->audioPlayFuture.waitForFinished(); +} + +void QAVAudioOutput::setVolume(qreal v) +{ + Q_D(QAVAudioOutput); + QMutexLocker locker(&d->mutex); + d->volume = v; + d->cond.wakeAll(); +} + +qreal QAVAudioOutput::volume() const +{ + Q_D(const QAVAudioOutput); + QMutexLocker locker(&d->mutex); + return d->volume; +} + +void QAVAudioOutput::setBufferSize(int bytes) +{ + Q_D(QAVAudioOutput); + QMutexLocker locker(&d->mutex); + d->bufferSize = bytes; + if (d->bufferSize > 0 && d->audioOutput) + d->audioOutput->setBufferSize(d->bufferSize); +} + +int QAVAudioOutput::bufferSize() const +{ + Q_D(const QAVAudioOutput); + QMutexLocker locker(&d->mutex); + return d->bufferSize; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) +void QAVAudioOutput::setChannelConfig(QAudioFormat::ChannelConfig config) +{ + Q_D(QAVAudioOutput); + QMutexLocker locker(&d->mutex); + d->channelConfig = config; +} + +QAudioFormat::ChannelConfig QAVAudioOutput::channelConfig() const +{ + Q_D(const QAVAudioOutput); + QMutexLocker locker(&d->mutex); + return d->channelConfig; +} + +#endif + +bool QAVAudioOutput::play(const QAVAudioFrame &frame) +{ + Q_D(QAVAudioOutput); + if (d->quit || !frame) + return false; + + QMutexLocker locker(&d->mutex); + d->startThreadIfNeeded(); + d->frames.push_back(frame); + d->cond.wakeAll(); + + return true; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiooutput.h b/QtAVPlayer/src/QtAVPlayer/qavaudiooutput.h new file mode 100644 index 0000000..bcdd5a2 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiooutput.h @@ -0,0 +1,47 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVAUDIOOUTPUT_H +#define QAVAUDIOOUTPUT_H + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVAudioOutputPrivate; +class QAVAudioOutput : public QObject +{ +public: + QAVAudioOutput(QObject *parent = nullptr); + ~QAVAudioOutput(); + + void setVolume(qreal v); + qreal volume() const; + void setBufferSize(int bytes); + int bufferSize() const; +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + void setChannelConfig(QAudioFormat::ChannelConfig); + QAudioFormat::ChannelConfig channelConfig() const; +#endif + + bool play(const QAVAudioFrame &frame); + +protected: + std::unique_ptr d_ptr; + +private: + Q_DISABLE_COPY(QAVAudioOutput) + Q_DECLARE_PRIVATE(QAVAudioOutput) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter.cpp new file mode 100644 index 0000000..1fb87d1 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter.cpp @@ -0,0 +1,43 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavaudiooutputfilter_p.h" +#include "qavinoutfilter_p_p.h" +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +QAVAudioOutputFilter::QAVAudioOutputFilter() + : QAVInOutFilter(*new QAVInOutFilterPrivate(this)) +{ +} + +QAVAudioOutputFilter::~QAVAudioOutputFilter() = default; + +int QAVAudioOutputFilter::configure(AVFilterGraph *graph, AVFilterInOut *out) +{ + QAVInOutFilter::configure(graph, out); + Q_D(QAVInOutFilter); + char name[255]; + static int index = 0; + snprintf(name, sizeof(name), "out_%d", index++); + int ret = avfilter_graph_create_filter(&d->ctx, + avfilter_get_by_name("abuffersink"), + name, nullptr, nullptr, graph); + if (ret < 0) + return ret; + + return avfilter_link(out->filter_ctx, out->pad_idx, d->ctx, 0); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter_p.h new file mode 100644 index 0000000..8e7ed29 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter_p.h @@ -0,0 +1,38 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVAUDIOOUTPUTFILTER_P_H +#define QAVAUDIOOUTPUTFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavinoutfilter_p.h" + +QT_BEGIN_NAMESPACE + +class QAVAudioOutputFilterPrivate; +class QAVAudioOutputFilter : public QAVInOutFilter +{ +public: + QAVAudioOutputFilter(); + ~QAVAudioOutputFilter(); + + int configure(AVFilterGraph *graph, AVFilterInOut *out) override; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavcodec.cpp b/QtAVPlayer/src/QtAVPlayer/qavcodec.cpp new file mode 100644 index 0000000..49701c9 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavcodec.cpp @@ -0,0 +1,99 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavcodec_p.h" +#include "qavcodec_p_p.h" + +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +QAVCodec::QAVCodec() + : QAVCodec(*new QAVCodecPrivate) +{ +} + +QAVCodec::QAVCodec(QAVCodecPrivate &d) + : d_ptr(&d) +{ + d_ptr->avctx = avcodec_alloc_context3(nullptr); +} + +QAVCodec::~QAVCodec() +{ + Q_D(QAVCodec); + if (d->avctx) + avcodec_free_context(&d->avctx); +} + +void QAVCodec::setCodec(const AVCodec *c) +{ + d_func()->codec = c; +} + +bool QAVCodec::open(AVStream *stream) +{ + Q_D(QAVCodec); + + if (!stream) + return false; + + int ret = avcodec_parameters_to_context(d->avctx, stream->codecpar); + if (ret < 0) { + qWarning() << "Failed avcodec_parameters_to_context:" << ret; + return false; + } + + d->avctx->pkt_timebase = stream->time_base; + d->avctx->framerate = stream->avg_frame_rate; + if (!d->codec) + d->codec = avcodec_find_decoder(d->avctx->codec_id); + if (!d->codec) { + qWarning() << "No decoder could be found for codec"; + return false; + } + + d->avctx->codec_id = d->codec->id; + + av_opt_set_int(d->avctx, "refcounted_frames", true, 0); + av_opt_set_int(d->avctx, "threads", 1, 0); + ret = avcodec_open2(d->avctx, d->codec, nullptr); + if (ret < 0) { + qWarning() << "Could not open the codec:" << d->codec->name << ret; + return false; + } + + stream->discard = AVDISCARD_DEFAULT; + d->stream = stream; + + return true; +} + +AVCodecContext *QAVCodec::avctx() const +{ + return d_func()->avctx; +} + +const AVCodec *QAVCodec::codec() const +{ + return d_func()->codec; +} + +void QAVCodec::flushBuffers() +{ + Q_D(QAVCodec); + if (!d->avctx) + return; + avcodec_flush_buffers(d->avctx); +} +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavcodec_p.h b/QtAVPlayer/src/QtAVPlayer/qavcodec_p.h new file mode 100644 index 0000000..34ea2c2 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavcodec_p.h @@ -0,0 +1,62 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVCODEC_P_H +#define QAVCODEC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavpacket_p.h" +#include "qavframe.h" +#include +#include + +QT_BEGIN_NAMESPACE + +struct AVCodec; +struct AVCodecContext; +struct AVStream; +class QAVCodecPrivate; +class QAVCodec +{ +public: + virtual ~QAVCodec(); + + bool open(AVStream *stream); + AVCodecContext *avctx() const; + void setCodec(const AVCodec *c); + const AVCodec *codec() const; + + void flushBuffers(); + + // Sends a packet + virtual int write(const QAVPacket &pkt) = 0; + // Receives a frame + // NOTE: There could be multiple frames + virtual int read(QAVStreamFrame &frame) = 0; + +protected: + QAVCodec(); + QAVCodec(QAVCodecPrivate &d); + std::unique_ptr d_ptr; + Q_DECLARE_PRIVATE(QAVCodec) +private: + Q_DISABLE_COPY(QAVCodec) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavcodec_p_p.h b/QtAVPlayer/src/QtAVPlayer/qavcodec_p_p.h new file mode 100644 index 0000000..1ab56b1 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavcodec_p_p.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVCODEC_P_P_H +#define QAVCODEC_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavcodec_p.h" + +QT_BEGIN_NAMESPACE + +struct AVCodec; +struct AVStream; +struct AVCodecContext; +class QAVCodecPrivate +{ +public: + virtual ~QAVCodecPrivate() = default; + + AVCodecContext *avctx = nullptr; + const AVCodec *codec = nullptr; + AVStream *stream = nullptr; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavdemuxer.cpp b/QtAVPlayer/src/QtAVPlayer/qavdemuxer.cpp new file mode 100644 index 0000000..e124c96 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavdemuxer.cpp @@ -0,0 +1,876 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavdemuxer_p.h" +#include "qavvideocodec_p.h" +#include "qavaudiocodec_p.h" +#include "qavsubtitlecodec_p.h" +#include "qavhwdevice_p.h" +#include "qaviodevice.h" +#include + +#if defined(QT_AVPLAYER_VA_X11) && QT_CONFIG(opengl) +#include "qavhwdevice_vaapi_x11_glx_p.h" +#endif + +#if defined(QT_AVPLAYER_VA_DRM) && QT_CONFIG(egl) +#include "qavhwdevice_vaapi_drm_egl_p.h" +#endif + +#if defined(QT_AVPLAYER_VDPAU) +#include "qavhwdevice_vdpau_p.h" +#endif + +#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) +#include "qavhwdevice_videotoolbox_p.h" +#endif + +#if defined(Q_OS_WIN) +#include "qavhwdevice_d3d11_p.h" +#endif + +#if defined(Q_OS_ANDROID) +#include "qavhwdevice_mediacodec_p.h" +#include +extern "C" { +#include "libavcodec/jni.h" +} +#endif + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 0, 0) +#include +#endif +} + +QT_BEGIN_NAMESPACE + +class QAVDemuxerPrivate +{ + Q_DECLARE_PUBLIC(QAVDemuxer) +public: + QAVDemuxerPrivate(QAVDemuxer *q) + : q_ptr(q) + { + } + + QAVDemuxer *q_ptr = nullptr; + AVFormatContext *ctx = nullptr; + AVBSFContext *bsf_ctx = nullptr; + + std::atomic_bool abortRequest = false; + mutable QMutex mutex; + + bool seekable = false; + QList availableStreams; + QList currentVideoStreams; + QList currentAudioStreams; + QList currentSubtitleStreams; + QList progress; + QString inputFormat; + QString inputVideoCodec; + QMap inputOptions; + + bool eof = false; + QList packets; + QString bsfs; +}; + +static int decode_interrupt_cb(void *ctx) +{ + auto d = reinterpret_cast(ctx); + return d ? int(d->abortRequest) : 0; +} + +QAVDemuxer::QAVDemuxer() + : d_ptr(new QAVDemuxerPrivate(this)) +{ + static bool loaded = false; + if (!loaded) { +#if (LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100)) + av_register_all(); + avcodec_register_all(); +#endif + avdevice_register_all(); + loaded = true; + } +} + +QAVDemuxer::~QAVDemuxer() +{ + abort(false); + unload(); +} + +void QAVDemuxer::abort(bool stop) +{ + Q_D(QAVDemuxer); + d->abortRequest = stop; +} + +static int setup_video_codec(const QString &inputVideoCodec, AVStream *stream, QAVVideoCodec &codec) +{ + const AVCodec *videoCodec = nullptr; + if (!inputVideoCodec.isEmpty()) { + qDebug() << "Loading: -vcodec" << inputVideoCodec; + videoCodec = avcodec_find_decoder_by_name(inputVideoCodec.toUtf8().constData()); + if (!videoCodec) { + qWarning() << "Could not find decoder:" << inputVideoCodec; + return AVERROR(EINVAL); + } + } + + if (videoCodec) + codec.setCodec(videoCodec); + + QList> devices; + AVDictionary *opts = NULL; + Q_UNUSED(opts); + +#if defined(QT_AVPLAYER_VA_X11) && QT_CONFIG(opengl) + devices.append(QSharedPointer(new QAVHWDevice_VAAPI_X11_GLX)); + av_dict_set(&opts, "connection_type", "x11", 0); +#endif +#if defined(QT_AVPLAYER_VDPAU) + devices.append(QSharedPointer(new QAVHWDevice_VDPAU)); +#endif +#if defined(QT_AVPLAYER_VA_DRM) && QT_CONFIG(egl) + devices.append(QSharedPointer(new QAVHWDevice_VAAPI_DRM_EGL)); +#endif +#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) + devices.append(QSharedPointer(new QAVHWDevice_VideoToolbox)); +#endif +#if defined(Q_OS_WIN) + devices.append(QSharedPointer(new QAVHWDevice_D3D11)); +#endif +#if defined(Q_OS_ANDROID) + devices.append(QSharedPointer(new QAVHWDevice_MediaCodec)); + if (!codec.codec()) + codec.setCodec(avcodec_find_decoder_by_name("h264_mediacodec")); + auto vm = QtAndroidPrivate::javaVM(); + av_jni_set_java_vm(vm, NULL); +#endif + + const bool ignoreHW = qEnvironmentVariableIsSet("QT_AVPLAYER_NO_HWDEVICE"); + if (!ignoreHW) { + AVBufferRef *hw_device_ctx = nullptr; + for (auto &device : devices) { + auto deviceName = av_hwdevice_get_type_name(device->type()); + qDebug() << "Creating hardware device context:" << deviceName; + if (av_hwdevice_ctx_create(&hw_device_ctx, device->type(), nullptr, opts, 0) >= 0) { + qDebug() << "Using hardware device context:" << deviceName; + codec.avctx()->hw_device_ctx = hw_device_ctx; + codec.avctx()->pix_fmt = device->format(); + codec.setDevice(device); + break; + } + av_buffer_unref(&hw_device_ctx); + } + } + + // Open codec after hwdevices + if (!codec.open(stream)) { + qWarning() << "Could not open video codec for stream"; + return AVERROR(EINVAL); + } + + return 0; +} + +static void log_callback(void *ptr, int level, const char *fmt, va_list vl) +{ + if (level > av_log_get_level()) + return; + + va_list vl2; + char line[1024]; + static int print_prefix = 1; + + va_copy(vl2, vl); + av_log_format_line(ptr, level, fmt, vl2, line, sizeof(line), &print_prefix); + va_end(vl2); + + qDebug() << "FFmpeg:" << line; +} + +QStringList QAVDemuxer::supportedFormats() +{ + static QStringList values; + if (values.isEmpty()) { +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 0, 0) + const AVInputFormat *fmt = nullptr; + void *it = nullptr; + while ((fmt = av_demuxer_iterate(&it))) { + if (fmt->name) + values << QString::fromLatin1(fmt->name).split(QLatin1Char(','), +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + QString::SkipEmptyParts +#else + Qt::SkipEmptyParts +#endif + ); + } +#endif + } + + return values; +} + +QStringList QAVDemuxer::supportedVideoCodecs() +{ + static QStringList values; + if (values.isEmpty()) { + const AVCodec *c = nullptr; + void *it = nullptr; + while ((c = av_codec_iterate(&it))) { + if (!av_codec_is_decoder(c) || c->type != AVMEDIA_TYPE_VIDEO) + continue; + values.append(QString::fromLatin1(c->name)); + } + } + + return values; +} + +QStringList QAVDemuxer::supportedProtocols() +{ + static QStringList values; + if (values.isEmpty()) { + void *opq = 0; + const char *value = nullptr; + while ((value = avio_enum_protocols(&opq, 0))) + values << QString::fromUtf8(value); + } + + return values; +} + +static int init_output_bsfs(AVBSFContext *ctx, AVStream *st) +{ + if (!ctx) + return 0; + + int ret = avcodec_parameters_copy(ctx->par_in, st->codecpar); + if (ret < 0) + return ret; + + ctx->time_base_in = st->time_base; + + ret = av_bsf_init(ctx); + if (ret < 0) { + qWarning() << "Error initializing bitstream filter:" << ctx->filter->name; + return ret; + } + + ret = avcodec_parameters_copy(st->codecpar, ctx->par_out); + if (ret < 0) + return ret; + + st->time_base = ctx->time_base_out; + return 0; +} + +static int apply_bsf(const QString &bsf, AVFormatContext *ctx, AVBSFContext *&bsf_ctx) +{ + int ret = !bsf.isEmpty() ? av_bsf_list_parse_str(bsf.toUtf8().constData(), &bsf_ctx) : 0; + if (ret < 0) { + qWarning() << "Error parsing bitstream filter sequence:" << bsf; + return ret; + } + + for (std::size_t i = 0; i < ctx->nb_streams; ++i) { + switch (ctx->streams[i]->codecpar->codec_type) { + case AVMEDIA_TYPE_VIDEO: + case AVMEDIA_TYPE_AUDIO: + ret = init_output_bsfs(bsf_ctx, ctx->streams[i]); + break; + default: + break; + } + } + + return ret; +} + +int QAVDemuxer::load(const QString &url, QAVIODevice *dev) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + + if (!d->ctx) + d->ctx = avformat_alloc_context(); + + d->ctx->flags |= AVFMT_FLAG_GENPTS; + d->ctx->interrupt_callback.callback = decode_interrupt_cb; + d->ctx->interrupt_callback.opaque = d; + if (dev) { + d->ctx->pb = dev->ctx(); + d->ctx->flags |= AVFMT_FLAG_CUSTOM_IO; + } + +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 0, 0) + const +#endif + AVInputFormat *inputFormat = nullptr; + if (!d->inputFormat.isEmpty()) { + qDebug() << "Loading: -f" << d->inputFormat; + inputFormat = av_find_input_format(d->inputFormat.toUtf8().constData()); + if (!inputFormat) { + qWarning() << "Could not find input format:" << d->inputFormat; + return AVERROR(EINVAL); + } + } + + AVDictionary *opts = nullptr; + for (const auto & key: d->inputOptions.keys()) + av_dict_set(&opts, key.toUtf8().constData(), d->inputOptions[key].toUtf8().constData(), 0); + locker.unlock(); + int ret = avformat_open_input(&d->ctx, url.toUtf8().constData(), inputFormat, &opts); + if (ret < 0) + return ret; + + ret = avformat_find_stream_info(d->ctx, NULL); + if (ret < 0) + return ret; + + locker.relock(); + av_log_set_callback(log_callback); + + d->seekable = d->ctx->iformat->read_seek || d->ctx->iformat->read_seek2; + if (d->ctx->pb) + d->seekable |= bool(d->ctx->pb->seekable); + + ret = resetCodecs(); + if (ret < 0) + return ret; + + const int videoStreamIndex = av_find_best_stream( + d->ctx, + AVMEDIA_TYPE_VIDEO, + -1, + -1, + nullptr, + 0); + if (videoStreamIndex >= 0) + d->currentVideoStreams.push_back(d->availableStreams[videoStreamIndex]); + + const int audioStreamIndex = av_find_best_stream( + d->ctx, + AVMEDIA_TYPE_AUDIO, + -1, + videoStreamIndex, + nullptr, + 0); + if (audioStreamIndex >= 0) + d->currentAudioStreams.push_back(d->availableStreams[audioStreamIndex]); + + const int subtitleStreamIndex = av_find_best_stream( + d->ctx, + AVMEDIA_TYPE_SUBTITLE, + -1, + audioStreamIndex >= 0 ? audioStreamIndex : videoStreamIndex, + nullptr, + 0); + if (subtitleStreamIndex >= 0) + d->currentSubtitleStreams.push_back(d->availableStreams[subtitleStreamIndex]); + + if (ret < 0) + return ret; + + if (!d->bsfs.isEmpty()) + return apply_bsf(d->bsfs, d->ctx, d->bsf_ctx); + + return 0; +} + +int QAVDemuxer::resetCodecs() +{ + Q_D(QAVDemuxer); + int ret = 0; + for (std::size_t i = 0; i < d->ctx->nb_streams && ret >= 0; ++i) { + if (!d->ctx->streams[i]->codecpar) { + qWarning() << "Could not find codecpar"; + return AVERROR(EINVAL); + } + const AVMediaType type = d->ctx->streams[i]->codecpar->codec_type; + switch (type) { + case AVMEDIA_TYPE_VIDEO: + { + QSharedPointer codec(new QAVVideoCodec); + d->availableStreams.push_back({ int(i), d->ctx, codec }); + ret = setup_video_codec(d->inputVideoCodec, d->ctx->streams[i], *static_cast(codec.data())); + } break; + case AVMEDIA_TYPE_AUDIO: + d->availableStreams.push_back({ int(i), d->ctx, QSharedPointer(new QAVAudioCodec) }); + if (!d->availableStreams.last().codec()->open(d->ctx->streams[i])) + qWarning() << "Could not open audio codec for stream:" << i; + break; + case AVMEDIA_TYPE_SUBTITLE: + d->availableStreams.push_back({ int(i), d->ctx, QSharedPointer(new QAVSubtitleCodec) }); + if (!d->availableStreams.last().codec()->open(d->ctx->streams[i])) + qWarning() << "Could not open subtitle codec for stream:" << i; + break; + default: + // Adding default stream + d->availableStreams.push_back({ int(i), d->ctx, nullptr }); + break; + } + auto &s = d->availableStreams[int(i)]; + d->progress.push_back({ s.duration(), s.framesCount(), s.frameRate() }); + } + + return ret; +} + +static bool findStream( + const QList &streams, + int index) +{ + for (const auto &stream: streams) { + if (index == stream.index()) + return true; + } + return false; +} + +AVMediaType QAVDemuxer::currentCodecType(int index) const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + // TODO: + if (findStream(d->currentVideoStreams, index)) + return AVMEDIA_TYPE_VIDEO; + if (findStream(d->currentAudioStreams, index)) + return AVMEDIA_TYPE_AUDIO; + if (findStream(d->currentSubtitleStreams, index)) + return AVMEDIA_TYPE_SUBTITLE; + return AVMEDIA_TYPE_UNKNOWN; +} + +static QList availableStreamsByType( + const QList &streams, + AVMediaType type) +{ + QList ret; + for (auto &stream : streams) { + if (stream.stream()->codecpar->codec_type == type) + ret.push_back(stream); + } + + return ret; +} + +QList QAVDemuxer::availableVideoStreams() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return availableStreamsByType(d->availableStreams, AVMEDIA_TYPE_VIDEO); +} + +QList QAVDemuxer::currentVideoStreams() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->currentVideoStreams; +} + +static bool setCurrentStreams( + const QList &streams, + const QList &availableStreams, + AVMediaType type, + QList ¤tStreams) +{ + QList ret; + for (const auto &stream: streams) { + if (stream.index() >= 0 + && stream.index() < availableStreams.size() + && availableStreams[stream.index()].stream()->codecpar->codec_type == type) + { + ret.push_back(availableStreams[stream.index()]); + } + } + if (!ret.isEmpty() || streams.isEmpty()) { + currentStreams = ret; + return true; + } + + return false; +} + +bool QAVDemuxer::setVideoStreams(const QList &streams) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + return setCurrentStreams( + streams, + d->availableStreams, + AVMEDIA_TYPE_VIDEO, + d->currentVideoStreams); +} + +QList QAVDemuxer::availableAudioStreams() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return availableStreamsByType( + d->availableStreams, + AVMEDIA_TYPE_AUDIO); +} + +QList QAVDemuxer::currentAudioStreams() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->currentAudioStreams; +} + +bool QAVDemuxer::setAudioStreams(const QList &streams) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + return setCurrentStreams( + streams, + d->availableStreams, + AVMEDIA_TYPE_AUDIO, + d->currentAudioStreams); +} + +QList QAVDemuxer::availableSubtitleStreams() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return availableStreamsByType( + d->availableStreams, + AVMEDIA_TYPE_SUBTITLE); +} + +QList QAVDemuxer::currentSubtitleStreams() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->currentSubtitleStreams; +} + +bool QAVDemuxer::setSubtitleStreams(const QList &streams) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + return setCurrentStreams( + streams, + d->availableStreams, + AVMEDIA_TYPE_SUBTITLE, + d->currentSubtitleStreams); +} + +void QAVDemuxer::unload() +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + if (d->ctx) { + avformat_close_input(&d->ctx); + avformat_free_context(d->ctx); + } + d->ctx = nullptr; + d->eof = false; + d->abortRequest = 0; + d->currentVideoStreams.clear(); + d->currentAudioStreams.clear(); + d->currentSubtitleStreams.clear(); + d->availableStreams.clear(); + d->progress.clear(); + av_bsf_free(&d->bsf_ctx); + d->bsf_ctx = nullptr; +} + +bool QAVDemuxer::eof() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->eof; +} + +QAVPacket QAVDemuxer::read() +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + if (!d->packets.isEmpty()) + return d->packets.takeFirst(); + + if (!d->ctx || d->eof) + return {}; + + QAVPacket pkt; + locker.unlock(); + int ret = av_read_frame(d->ctx, pkt.packet()); + if (ret < 0) { + if (ret == AVERROR_EOF || avio_feof(d->ctx->pb)) { + locker.relock(); + d->eof = true; + locker.unlock(); + } + } + locker.relock(); + + QAVStream stream = pkt.packet()->stream_index < d->availableStreams.size() + ? d->availableStreams[pkt.packet()->stream_index] + : QAVStream(); + Q_ASSERT(stream.stream()); + pkt.setStream(stream); + + if (d->bsf_ctx) { + ret = av_bsf_send_packet(d->bsf_ctx, d->eof ? NULL : pkt.packet()); + if (ret >= 0) { + while ((ret = av_bsf_receive_packet(d->bsf_ctx, pkt.packet())) >= 0) + d->packets.append(pkt); + } + if (ret < 0 && ret != AVERROR_EOF && ret != AVERROR(EAGAIN)) { + qWarning() << "Error applying bitstream filters to an output:" << ret; + return {}; + } + } else { + d->packets.append(pkt); + } + + return !d->packets.isEmpty() ? d->packets.takeFirst() : QAVPacket{}; +} + +void QAVDemuxer::decode(const QAVPacket &pkt, QList &frames) const +{ + if (!pkt.stream()) + return; + int sent = 0; + do { + sent = pkt.send(); + // AVERROR(EAGAIN): input is not accepted in the current state - user must read output with avcodec_receive_frame() + // (once all output is read, the packet should be resent, and the call will not fail with EAGAIN) + if (sent < 0 && sent != AVERROR(EAGAIN)) + return; + + while (true) { + QAVFrame frame; + frame.setStream(pkt.stream()); + // AVERROR(EAGAIN): output is not available in this state - user must try to send new input + int received = frame.receive(); + if (received < 0) + break; + frames.push_back(frame); + } + } while (sent == AVERROR(EAGAIN)); +} + +void QAVDemuxer::decode(const QAVPacket &pkt, QList &frames) const +{ + if (!pkt.stream()) + return; + int sent = pkt.send(); + if (sent < 0 && sent != AVERROR(EAGAIN)) + return; + + QAVSubtitleFrame frame; + frame.setStream(pkt.stream()); + if (frame.receive() >= 0) + frames.push_back(frame); +} + +void QAVDemuxer::flushCodecBuffers() +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + for (auto &s: d->availableStreams) { + auto c = s.codec(); + if (c) + c->flushBuffers(); + } +} + +bool QAVDemuxer::seekable() const +{ + return d_func()->seekable; +} + +int QAVDemuxer::seek(double sec) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + if (!d->ctx || !d->seekable) + return AVERROR(EINVAL); + + d->eof = false; + locker.unlock(); + + int flags = AVSEEK_FLAG_BACKWARD; + int64_t target = sec * AV_TIME_BASE; + int64_t min = INT_MIN; + int64_t max = target; + return avformat_seek_file(d->ctx, -1, min, target, max, flags); +} + +double QAVDemuxer::duration() const +{ + Q_D(const QAVDemuxer); + if (!d->ctx || d->ctx->duration == AV_NOPTS_VALUE) + return 0.0; + + return d->ctx->duration * av_q2d({1, AV_TIME_BASE}); +} + +double QAVDemuxer::videoFrameRate() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + if (d->currentVideoStreams.isEmpty()) + return 0.0; + // TODO: + double ret = std::numeric_limits::max(); + for (const auto &stream: d->currentVideoStreams) { + AVRational fr = av_guess_frame_rate(d->ctx, d->ctx->streams[stream.index()], NULL); + double rate = fr.num && fr.den ? av_q2d({fr.den, fr.num}) : 0.0; + if (rate < ret) + ret = rate; + } + + return ret; +} + +QMap QAVDemuxer::metadata() const +{ + Q_D(const QAVDemuxer); + QMap result; + if (d->ctx == nullptr) + return result; + + AVDictionaryEntry *tag = nullptr; + while ((tag = av_dict_get(d->ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) + result[QString::fromUtf8(tag->key)] = QString::fromUtf8(tag->value); + + return result; +} + +QString QAVDemuxer::bitstreamFilter() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->bsfs; +} + +int QAVDemuxer::applyBitstreamFilter(const QString &bsfs) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + d->bsfs = bsfs; + int ret = 0; + if (d->ctx) { + av_bsf_free(&d->bsf_ctx); + d->bsf_ctx = nullptr; + ret = apply_bsf(d->bsfs, d->ctx, d->bsf_ctx); + } + return ret; +} + +QString QAVDemuxer::inputFormat() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->inputFormat; +} + +void QAVDemuxer::setInputFormat(const QString &format) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + d->inputFormat = format; +} + +QString QAVDemuxer::inputVideoCodec() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->inputVideoCodec; +} + +void QAVDemuxer::setInputVideoCodec(const QString &codec) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + d->inputVideoCodec = codec; +} + +QMap QAVDemuxer::inputOptions() const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + return d->inputOptions; +} + +void QAVDemuxer::setInputOptions(const QMap &opts) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + d->inputOptions = opts; +} + +void QAVDemuxer::onFrameSent(const QAVStreamFrame &frame) +{ + Q_D(QAVDemuxer); + QMutexLocker locker(&d->mutex); + int index = frame.stream().index(); + if (index >= 0 && index < d->progress.size()) + d->progress[index].onFrameSent(frame.pts()); +} + +bool QAVDemuxer::isMasterStream(const QAVStream &stream) const +{ + auto s = stream.stream(); + switch (s->codecpar->codec_type) { + case AVMEDIA_TYPE_VIDEO: + return s->disposition != AV_DISPOSITION_ATTACHED_PIC; + case AVMEDIA_TYPE_AUDIO: + // Check if there are any video streams available + for (const auto &vs: currentVideoStreams()) { + if (vs.stream()->disposition != AV_DISPOSITION_ATTACHED_PIC) + return false; + } + return true; + default: + Q_ASSERT(false); + return false; + } +} + +QAVStream::Progress QAVDemuxer::progress(const QAVStream &s) const +{ + Q_D(const QAVDemuxer); + QMutexLocker locker(&d->mutex); + int index = s.index(); + if (index >= 0 && index < d->progress.size()) + return d->progress[index]; + return {}; +} + +QStringList QAVDemuxer::supportedBitstreamFilters() +{ + QStringList result; +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 0, 0) + const AVBitStreamFilter *bsf = NULL; + void *opaque = NULL; + + while ((bsf = av_bsf_iterate(&opaque))) + result.append(QString::fromUtf8(bsf->name)); +#endif + return result; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavdemuxer_p.h b/QtAVPlayer/src/QtAVPlayer/qavdemuxer_p.h new file mode 100644 index 0000000..e2dc066 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavdemuxer_p.h @@ -0,0 +1,114 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVDEMUXER_H +#define QAVDEMUXER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavpacket_p.h" +#include "qavstream.h" +#include "qavframe.h" +#include "qavsubtitleframe.h" +#include +#include + +QT_BEGIN_NAMESPACE + +extern "C" { +#include +} + +class QAVDemuxerPrivate; +class QAVVideoCodec; +class QAVAudioCodec; +class QAVIODevice; +struct AVStream; +struct AVCodecContext; +struct AVFormatContext; +class QAVDemuxer +{ +public: + QAVDemuxer(); + ~QAVDemuxer(); + + void abort(bool stop = true); + int load(const QString &url, QAVIODevice *dev = nullptr); + void unload(); + + AVMediaType currentCodecType(int index) const; + + QList availableVideoStreams() const; + QList currentVideoStreams() const; + bool setVideoStreams(const QList &streams); + + QList availableAudioStreams() const; + QList currentAudioStreams() const; + bool setAudioStreams(const QList &streams); + + QList availableSubtitleStreams() const; + QList currentSubtitleStreams() const; + bool setSubtitleStreams(const QList &streams); + + QAVPacket read(); + + void decode(const QAVPacket &pkt, QList &frames) const; + void decode(const QAVPacket &pkt, QList &frames) const; + void flushCodecBuffers(); + + double duration() const; + bool seekable() const; + int seek(double sec); + bool eof() const; + double videoFrameRate() const; + + QMap metadata() const; + + QString bitstreamFilter() const; + int applyBitstreamFilter(const QString &bsfs); + + QString inputFormat() const; + void setInputFormat(const QString &format); + + QString inputVideoCodec() const; + void setInputVideoCodec(const QString &codec); + + QMap inputOptions() const; + void setInputOptions(const QMap &opts); + + void onFrameSent(const QAVStreamFrame &frame); + QAVStream::Progress progress(const QAVStream &s) const; + + bool isMasterStream(const QAVStream &stream) const; + + static QStringList supportedFormats(); + static QStringList supportedVideoCodecs(); + static QStringList supportedProtocols(); + static QStringList supportedBitstreamFilters(); + +protected: + std::unique_ptr d_ptr; + +private: + int resetCodecs(); + + Q_DISABLE_COPY(QAVDemuxer) + Q_DECLARE_PRIVATE(QAVDemuxer) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavfilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavfilter.cpp new file mode 100644 index 0000000..7d1090f --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfilter.cpp @@ -0,0 +1,31 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavfilter_p.h" +#include "qavfilter_p_p.h" +#include + +QT_BEGIN_NAMESPACE + +QAVFilter::QAVFilter( + const QAVStream &stream, + const QString &name, + QAVFilterPrivate &d) + : d_ptr(&d) +{ + d.stream = stream; + d.name = name; +} + +QAVFilter::~QAVFilter() = default; + +bool QAVFilter::isEmpty() const +{ + return d_func()->isEmpty; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavfilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavfilter_p.h new file mode 100644 index 0000000..27f1156 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfilter_p.h @@ -0,0 +1,54 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFILTER_P_H +#define QAVFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVFilterPrivate; +class QAVFilter +{ +public: + virtual ~QAVFilter(); + + virtual int write(const QAVFrame &frame) = 0; + virtual int read(QAVFrame &frame) = 0; + // Checks if all frames have been read + bool isEmpty() const; + virtual void flush() = 0; + +protected: + QAVFilter( + const QAVStream &stream, + const QString &name, + QAVFilterPrivate &d); + std::unique_ptr d_ptr; + Q_DECLARE_PRIVATE(QAVFilter) +private: + Q_DISABLE_COPY(QAVFilter) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavfilter_p_p.h b/QtAVPlayer/src/QtAVPlayer/qavfilter_p_p.h new file mode 100644 index 0000000..8e8c76b --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfilter_p_p.h @@ -0,0 +1,48 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFILTER_P_P_H +#define QAVFILTER_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVFilter; +class QAVFilterPrivate +{ + Q_DECLARE_PUBLIC(QAVFilter) +public: + QAVFilterPrivate(QAVFilter *q, QMutex &mutex) : q_ptr(q), graphMutex(mutex) { } + virtual ~QAVFilterPrivate() = default; + + QAVFilter *q_ptr = nullptr; + QAVStream stream; + QString name; + QAVFrame sourceFrame; + QList outputFrames; + bool isEmpty = true; + QMutex &graphMutex; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavfiltergraph.cpp b/QtAVPlayer/src/QtAVPlayer/qavfiltergraph.cpp new file mode 100644 index 0000000..88d05d7 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfiltergraph.cpp @@ -0,0 +1,186 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavfiltergraph_p.h" +#include "qavcodec_p.h" +#include "qavvideocodec_p.h" +#include "qavaudiocodec_p.h" +#include + +extern "C" { +#include +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVFilterGraphPrivate +{ + Q_DECLARE_PUBLIC(QAVFilterGraph) +public: + QAVFilterGraphPrivate(QAVFilterGraph *q) : q_ptr(q) { } + + int read(); + + QAVFilterGraph *q_ptr = nullptr; + QString desc; + AVFilterGraph *graph = nullptr; + AVFilterInOut *outputs = nullptr; + AVFilterInOut *inputs = nullptr; + QList videoInputFilters; + QList videoOutputFilters; + QList audioInputFilters; + QList audioOutputFilters; + mutable QMutex mutex; +}; + +QAVFilterGraph::QAVFilterGraph() + : d_ptr(new QAVFilterGraphPrivate(this)) +{ +} + +QAVFilterGraph::~QAVFilterGraph() +{ + Q_D(QAVFilterGraph); + avfilter_graph_free(&d->graph); + avfilter_inout_free(&d->inputs); + avfilter_inout_free(&d->outputs); +} + +int QAVFilterGraph::parse(const QString &desc) +{ + Q_D(QAVFilterGraph); + d->desc = desc; + avfilter_graph_free(&d->graph); + avfilter_inout_free(&d->inputs); + avfilter_inout_free(&d->outputs); + d->graph = avfilter_graph_alloc(); + return avfilter_graph_parse2(d->graph, desc.toUtf8().constData(), &d->inputs, &d->outputs); +} + +int QAVFilterGraph::apply(const QAVFrame &frame) +{ + Q_D(QAVFilterGraph); + if (!frame.stream()) + return 0; + const AVMediaType codec_type = frame.stream().stream()->codecpar->codec_type; + switch (codec_type) { + case AVMEDIA_TYPE_VIDEO: + d->videoInputFilters.clear(); + d->videoOutputFilters.clear(); + break; + case AVMEDIA_TYPE_AUDIO: + d->audioInputFilters.clear(); + d->audioOutputFilters.clear(); + break; + default: + qWarning() << "Could not apply frame: Unsupported codec type:" << codec_type; + return AVERROR(EINVAL); + } + + int ret = 0; + int i = 0; + AVFilterInOut *cur = nullptr; + for (cur = d->inputs, i = 0; cur; cur = cur->next, ++i) { + switch (avfilter_pad_get_type(cur->filter_ctx->input_pads, cur->pad_idx)) { + case AVMEDIA_TYPE_VIDEO: { + if (codec_type == AVMEDIA_TYPE_VIDEO) { + QAVVideoInputFilter filter(frame); + ret = filter.configure(d->graph, cur); + if (ret < 0) + return ret; + d->videoInputFilters.push_back(filter); + } + } break; + case AVMEDIA_TYPE_AUDIO: { + if (codec_type == AVMEDIA_TYPE_AUDIO) { + QAVAudioInputFilter filter(frame); + ret = filter.configure(d->graph, cur); + if (ret < 0) + return ret; + d->audioInputFilters.push_back(filter); + } + } break; + default: + return AVERROR(EINVAL); + } + } + + for (cur = d->outputs, i = 0; cur; cur = cur->next, ++i) { + switch (avfilter_pad_get_type(cur->filter_ctx->output_pads, cur->pad_idx)) { + case AVMEDIA_TYPE_VIDEO: { + if (codec_type == AVMEDIA_TYPE_VIDEO) { + QAVVideoOutputFilter filter; + ret = filter.configure(d->graph, cur); + if (ret < 0) + return ret; + d->videoOutputFilters.push_back(filter); + } + } break; + case AVMEDIA_TYPE_AUDIO: { + if (codec_type == AVMEDIA_TYPE_AUDIO) { + QAVAudioOutputFilter filter; + int ret = filter.configure(d->graph, cur); + if (ret < 0) + return ret; + d->audioOutputFilters.push_back(filter); + } + } break; + default: + return AVERROR(EINVAL); + } + } + + return ret; +} + +int QAVFilterGraph::config() +{ + Q_D(QAVFilterGraph); + return avfilter_graph_config(d->graph, nullptr); +} + +QString QAVFilterGraph::desc() const +{ + Q_D(const QAVFilterGraph); + return d->desc; +} + +QMutex &QAVFilterGraph::mutex() +{ + Q_D(QAVFilterGraph); + return d->mutex; +} + +AVFilterGraph *QAVFilterGraph::graph() const +{ + return d_func()->graph; +} + +QList QAVFilterGraph::videoInputFilters() const +{ + return d_func()->videoInputFilters; +} + +QList QAVFilterGraph::videoOutputFilters() const +{ + return d_func()->videoOutputFilters; +} + +QList QAVFilterGraph::audioInputFilters() const +{ + return d_func()->audioInputFilters; +} + +QList QAVFilterGraph::audioOutputFilters() const +{ + return d_func()->audioOutputFilters; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavfiltergraph_p.h b/QtAVPlayer/src/QtAVPlayer/qavfiltergraph_p.h new file mode 100644 index 0000000..897d335 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfiltergraph_p.h @@ -0,0 +1,64 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFILTERGRAPH_P_H +#define QAVFILTERGRAPH_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavvideoinputfilter_p.h" +#include "qavvideooutputfilter_p.h" +#include "qavaudioinputfilter_p.h" +#include "qavaudiooutputfilter_p.h" +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVFilterGraphPrivate; +class QAVDemuxer; +class QAVFilterGraph +{ +public: + QAVFilterGraph(); + ~QAVFilterGraph(); + + int parse(const QString &desc); + int apply(const QAVFrame &frame); + int config(); + QString desc() const; + QMutex &mutex(); + + AVFilterGraph *graph() const; + QList videoInputFilters() const; + QList videoOutputFilters() const; + QList audioInputFilters() const; + QList audioOutputFilters() const; + +protected: + std::unique_ptr d_ptr; + Q_DECLARE_PRIVATE(QAVFilterGraph) +private: + Q_DISABLE_COPY(QAVFilterGraph) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavfilters.cpp b/QtAVPlayer/src/QtAVPlayer/qavfilters.cpp new file mode 100644 index 0000000..989e98d --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfilters.cpp @@ -0,0 +1,220 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavfilters_p.h" +#include "qavvideofilter_p.h" +#include "qavaudiofilter_p.h" +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +int QAVFilters::createFilters( + const QList &filterDescs, + const QAVFrame &frame, + const QAVDemuxer &demuxer) +{ + QMutexLocker locker(&m_mutex); + m_videoFilters.clear(); + m_audioFilters.clear(); + m_filterGraphs.clear(); + for (int i = 0; i < filterDescs.size(); ++i) { + const auto & filterDesc = filterDescs[i]; + std::unique_ptr graph(!filterDesc.isEmpty() ? new QAVFilterGraph : nullptr); + if (graph) { + int ret = graph->parse(filterDesc); + if (ret < 0) { + qWarning() << "Could not parse filter desc:" << filterDesc << ret; + return ret; + } + QAVFrame videoFrame; + QAVFrame audioFrame; + const auto videoStreams = demuxer.currentVideoStreams(); + const auto videoStream = !videoStreams.isEmpty() ? videoStreams.first() : QAVStream(); + videoFrame.setStream(videoStream); + const auto audioStreams = demuxer.currentAudioStreams(); + const auto audioStream = !audioStreams.isEmpty() ? audioStreams.first() : QAVStream(); + audioFrame.setStream(audioStream); + auto stream = frame.stream().stream(); + if (stream) { + switch (stream->codecpar->codec_type) { + case AVMEDIA_TYPE_VIDEO: + videoFrame = frame; + break; + case AVMEDIA_TYPE_AUDIO: + audioFrame = frame; + break; + default: + qWarning() << "Unsupported codec type:" << stream->codecpar->codec_type; + return AVERROR(ENOTSUP); + } + } + ret = graph->apply(videoFrame); + if (ret < 0) { + qWarning() << "Could not create video filters:" << ret; + return ret; + } + ret = graph->apply(audioFrame); + if (ret < 0) { + qWarning() << "Could not create audio filters:" << ret; + return ret; + } + ret = graph->config(); + if (ret < 0) { + qWarning() << "Could not configure filter graph:" << ret; + return ret; + } + + auto videoInput = graph->videoInputFilters(); + auto videoOutput = graph->videoOutputFilters(); + m_videoFilters.emplace_back( + std::unique_ptr( + new QAVVideoFilter( + videoStream, + QString::number(i), + videoInput, + videoOutput, + graph->mutex()) + ) + ); + auto audioInput = graph->audioInputFilters(); + auto audioOutput = graph->audioOutputFilters(); + m_audioFilters.emplace_back( + std::unique_ptr( + new QAVAudioFilter( + audioStream, + QString::number(i), + audioInput, + audioOutput, + graph->mutex()) + ) + ); + qDebug() << __FUNCTION__ << ":" << filterDesc + << "video[ input:" << videoInput.size() << "-> output:" << videoOutput.size() << "]" + << "audio[ input:" << audioInput.size() << "-> output:" << audioOutput.size() << "]"; + } + + m_filterGraphs.push_back(std::move(graph)); + } + + m_filterDescs = filterDescs; + return 0; +} + +static int writeFrame( + const QAVFrame &decodedFrame, + const std::vector> &filters) +{ + int ret = 0; + for (size_t i = 0; i < filters.size() && ret >= 0; ++i) + ret = filters[i]->write(decodedFrame); + return ret; +} + +int QAVFilters::write( + AVMediaType mediaType, + const QAVFrame &decodedFrame) +{ + QMutexLocker locker(&m_mutex); + switch (mediaType) { + case AVMEDIA_TYPE_VIDEO: + return writeFrame(decodedFrame, m_videoFilters); + case AVMEDIA_TYPE_AUDIO: + return writeFrame(decodedFrame, m_audioFilters); + default: + qWarning() << "Unsupported codec type:" << mediaType; + break; + } + return AVERROR(ENOTSUP); +} + +static int readFrames( + const QAVFrame &decodedFrame, + const std::vector> &filters, + QList &filteredFrames) +{ + QAVFrame frame; + if (filters.empty()) { + if (decodedFrame) + filteredFrames.append(decodedFrame); + return 0; + } + + // Read all frames from all filters at once + for (size_t i = 0; i < filters.size(); ++i) { + do { + int ret = filters[i]->read(frame); + if (ret >= 0 && (!frame.filterName().isEmpty() || i == 0)) + filteredFrames.append(frame); + } while (!filters[i]->isEmpty()); + } + return 0; +} + +int QAVFilters::read( + AVMediaType mediaType, + const QAVFrame &decodedFrame, + QList &filteredFrames) +{ + QMutexLocker locker(&m_mutex); + switch (mediaType) { + case AVMEDIA_TYPE_VIDEO: + return readFrames(decodedFrame, m_videoFilters, filteredFrames); + case AVMEDIA_TYPE_AUDIO: + return readFrames(decodedFrame, m_audioFilters, filteredFrames); + default: + qWarning() << "Unsupported codec type:" << mediaType; + break; + } + return AVERROR(ENOTSUP); +} + +QList QAVFilters::filterDescs() const +{ + QMutexLocker locker(&m_mutex); + return m_filterDescs; +} + +static bool filtersEmpty(const std::vector> &filters) +{ + for (const auto &filter : filters) + if (!filter->isEmpty()) + return false; + return true; +} + +bool QAVFilters::isEmpty() const +{ + QMutexLocker locker(&m_mutex); + return filtersEmpty(m_videoFilters) && filtersEmpty(m_audioFilters); +} + +static void flushFilters(const std::vector> &filters) +{ + for (const auto &filter: filters) + filter->flush(); +} + +void QAVFilters::flush() +{ + QMutexLocker locker(&m_mutex); + flushFilters(m_videoFilters); + flushFilters(m_audioFilters); +} + +void QAVFilters::clear() +{ + QMutexLocker locker(&m_mutex); + m_videoFilters.clear(); + m_audioFilters.clear(); + m_filterGraphs.clear(); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavfilters_p.h b/QtAVPlayer/src/QtAVPlayer/qavfilters_p.h new file mode 100644 index 0000000..f423e40 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavfilters_p.h @@ -0,0 +1,65 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFILTERS_P_H +#define QAVFILTERS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include "qavframe.h" +#include "qavfilter_p.h" +#include "qavdemuxer_p.h" +#include "qavfiltergraph_p.h" +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVFilters +{ +public: + QAVFilters() = default; + int createFilters( + const QList &filterDescs, + const QAVFrame &frame, + const QAVDemuxer &demuxer); + int write( + AVMediaType mediaType, + const QAVFrame &decodedFrame); + int read( + AVMediaType mediaType, + const QAVFrame &decodedFrame, + QList &filteredFrames); + QList filterDescs() const; + bool isEmpty() const; + void flush(); + void clear(); + +private: + Q_DISABLE_COPY(QAVFilters) + + QList m_filterDescs; + std::vector> m_filterGraphs; + std::vector> m_videoFilters; + std::vector> m_audioFilters; + mutable QMutex m_mutex; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavframe.cpp b/QtAVPlayer/src/QtAVPlayer/qavframe.cpp new file mode 100644 index 0000000..233c87e --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavframe.cpp @@ -0,0 +1,121 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavframe.h" +#include "qavstream.h" +#include "qavframe_p.h" +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +QAVFrame::QAVFrame() + : QAVFrame(*new QAVFramePrivate) +{ +} + +QAVFrame::QAVFrame(const QAVFrame &other) + : QAVFrame() +{ + *this = other; +} + +QAVFrame::QAVFrame(QAVFramePrivate &d) + : QAVStreamFrame(d) +{ + d.frame = av_frame_alloc(); +} + +QAVFrame &QAVFrame::operator=(const QAVFrame &other) +{ + Q_D(QAVFrame); + QAVStreamFrame::operator=(other); + + auto other_priv = static_cast(other.d_ptr.get()); + int64_t pts = d->frame->pts; + av_frame_unref(d->frame); + av_frame_ref(d->frame, other_priv->frame); + + if (d->frame->pts < 0) + d->frame->pts = pts; + + d->frameRate = other_priv->frameRate; + d->timeBase = other_priv->timeBase; + d->filterName = other_priv->filterName; + return *this; +} + +QAVFrame::operator bool() const +{ + Q_D(const QAVFrame); + return QAVStreamFrame::operator bool() && d->frame && (d->frame->data[0] || d->frame->data[1] || d->frame->data[2] || d->frame->data[3]); +} + +QAVFrame::~QAVFrame() +{ + Q_D(QAVFrame); + av_frame_free(&d->frame); +} + +AVFrame *QAVFrame::frame() const +{ + Q_D(const QAVFrame); + return d->frame; +} + +void QAVFrame::setFrameRate(const AVRational &value) +{ + Q_D(QAVFrame); + d->frameRate = value; +} + +void QAVFrame::setTimeBase(const AVRational &value) +{ + Q_D(QAVFrame); + d->timeBase = value; +} + +QString QAVFrame::filterName() const +{ + return d_func()->filterName; +} + +void QAVFrame::setFilterName(const QString &name) +{ + Q_D(QAVFrame); + d->filterName = name; +} + +double QAVFramePrivate::pts() const +{ + if (!frame || !stream) + return NAN; + + AVRational tb = timeBase.num && timeBase.den ? timeBase : stream.stream()->time_base; + return frame->pts == AV_NOPTS_VALUE ? NAN : frame->pts * av_q2d(tb); +} + +double QAVFramePrivate::duration() const +{ + if (!frame || !stream) + return 0.0; + + return frameRate.den && frameRate.num + ? av_q2d(AVRational{frameRate.den, frameRate.num}) + : +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 30, 0) + frame->pkt_duration +#else + frame->duration +#endif + * av_q2d(stream.stream()->time_base); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavframe.h b/QtAVPlayer/src/QtAVPlayer/qavframe.h new file mode 100644 index 0000000..53a50db --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavframe.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFRAME_H +#define QAVFRAME_H + +#include +#include + +QT_BEGIN_NAMESPACE + +struct AVFrame; +struct AVRational; +class QAVFramePrivate; +class QAVFrame : public QAVStreamFrame +{ +public: + QAVFrame(); + ~QAVFrame(); + QAVFrame(const QAVFrame &other); + QAVFrame &operator=(const QAVFrame &other); + operator bool() const; + AVFrame *frame() const; + + void setFrameRate(const AVRational &value); + void setTimeBase(const AVRational &value); + QString filterName() const; + void setFilterName(const QString &name); + +protected: + QAVFrame(QAVFramePrivate &d); + Q_DECLARE_PRIVATE(QAVFrame) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavframe_p.h b/QtAVPlayer/src/QtAVPlayer/qavframe_p.h new file mode 100644 index 0000000..8eb2aa2 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavframe_p.h @@ -0,0 +1,48 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFRAME_P_H +#define QAVFRAME_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavstreamframe_p.h" + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +struct AVFrame; +class QAVFramePrivate : public QAVStreamFramePrivate +{ +public: + + double pts() const override; + double duration() const override; + + AVFrame *frame = nullptr; + // Overridden data from filters if any + AVRational frameRate{}; + AVRational timeBase{}; + // Name of a filter the frame has retrieved from + QString filterName; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavframecodec.cpp b/QtAVPlayer/src/QtAVPlayer/qavframecodec.cpp new file mode 100644 index 0000000..f3cd805 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavframecodec.cpp @@ -0,0 +1,46 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavframecodec_p.h" +#include "qavcodec_p_p.h" + +#include + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +QAVFrameCodec::QAVFrameCodec() +{ +} + +QAVFrameCodec::QAVFrameCodec(QAVCodecPrivate &d) + : QAVCodec(d) +{ +} + +int QAVFrameCodec::write(const QAVPacket &pkt) +{ + Q_D(QAVCodec); + if (!d->avctx) + return AVERROR(EINVAL); + return avcodec_send_packet(d->avctx, pkt ? pkt.packet() : nullptr); +} + +int QAVFrameCodec::read(QAVStreamFrame &frame) +{ + Q_D(QAVCodec); + if (!d->avctx) + return AVERROR(EINVAL); + auto f = static_cast(&frame); + return avcodec_receive_frame(d->avctx, f->frame()); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavframecodec_p.h b/QtAVPlayer/src/QtAVPlayer/qavframecodec_p.h new file mode 100644 index 0000000..a4fdf6e --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavframecodec_p.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFRAMECODEC_P_H +#define QAVFRAMECODEC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavframe.h" +#include "qavcodec_p.h" +#include "qavpacket_p.h" + +QT_BEGIN_NAMESPACE + +class QAVFrameCodec : public QAVCodec +{ +public: + int write(const QAVPacket &pkt) override; + int read(QAVStreamFrame &frame) override; + +protected: + QAVFrameCodec(); + QAVFrameCodec(QAVCodecPrivate &d); +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11.cpp b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11.cpp new file mode 100644 index 0000000..472f5da --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11.cpp @@ -0,0 +1,225 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavhwdevice_d3d11_p.h" +#include "qavvideobuffer_gpu_p.h" +#include + +#ifdef QT_AVPLAYER_MULTIMEDIA + +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 2) +#include +#else +#include +template +using ComPtr = QWindowsIUPointer; +#endif +#include +#endif + +#endif // QT_AVPLAYER_MULTIMEDIA + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +void QAVHWDevice_D3D11::init(AVCodecContext *avctx) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + int ret = avcodec_get_hw_frames_parameters(avctx, + avctx->hw_device_ctx, + AV_PIX_FMT_D3D11, + &avctx->hw_frames_ctx); + + if (ret < 0) { + qWarning() << "Failed to allocate HW frames context:" << ret; + return; + } + + auto frames_ctx = (AVHWFramesContext *)avctx->hw_frames_ctx->data; + auto hwctx = (AVD3D11VAFramesContext *)frames_ctx->hwctx; + hwctx->MiscFlags = D3D11_RESOURCE_MISC_SHARED; + hwctx->BindFlags = D3D11_BIND_DECODER | D3D11_BIND_SHADER_RESOURCE; + ret = av_hwframe_ctx_init(avctx->hw_frames_ctx); + if (ret < 0) { + qWarning() << "Failed to initialize HW frames context:" << ret; + av_buffer_unref(&avctx->hw_frames_ctx); + } +#else + Q_UNUSED(avctx); +#endif +} + +AVPixelFormat QAVHWDevice_D3D11::format() const +{ + return AV_PIX_FMT_D3D11; +} + +AVHWDeviceType QAVHWDevice_D3D11::type() const +{ + return AV_HWDEVICE_TYPE_D3D11VA; +} + +#ifdef QT_AVPLAYER_MULTIMEDIA + +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + +template +static T **address(ComPtr &ptr) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 2) + return ptr.GetAddressOf(); +#else + return ptr.address(); +#endif +} + +template +static T *get(const ComPtr &ptr) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 2) + return ptr.Get(); +#else + return ptr.get(); +#endif +} + +static ComPtr shareTexture(ID3D11Device *dev, ID3D11Texture2D *tex) +{ + ComPtr dxgiResource; + HRESULT hr = tex->QueryInterface(__uuidof(IDXGIResource), reinterpret_cast(address(dxgiResource))); + if (FAILED(hr)) { + qWarning() << "Failed to obtain resource handle from FFmpeg texture:" << hr << std::system_category().message(hr); + return {}; + } + + HANDLE shared = nullptr; + hr = dxgiResource->GetSharedHandle(&shared); + if (FAILED(hr)) { + qWarning() << "Failed to obtain shared handle for FFmpeg texture:" << hr << std::system_category().message(hr); + return {}; + } + + ComPtr sharedTex; + hr = dev->OpenSharedResource(shared, __uuidof(ID3D11Texture2D), reinterpret_cast(address(sharedTex))); + if (FAILED(hr)) + qWarning() << "Failed to share FFmpeg texture:" << hr << std::system_category().message(hr); + return sharedTex; +} + +static ComPtr copyTexture(ID3D11Device *dev, ID3D11Texture2D *from, int index) +{ + D3D11_TEXTURE2D_DESC fromDesc = {}; + from->GetDesc(&fromDesc); + + D3D11_TEXTURE2D_DESC toDesc = {}; + toDesc.Width = fromDesc.Width; + toDesc.Height = fromDesc.Height; + toDesc.Format = fromDesc.Format; + toDesc.ArraySize = 1; + toDesc.MipLevels = 1; + toDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + toDesc.MiscFlags = 0; + toDesc.SampleDesc = { 1, 0 }; + + ComPtr copy; + HRESULT hr = dev->CreateTexture2D(&toDesc, nullptr, address(copy)); + if (FAILED(hr)) { + qWarning() << "Failed to create texture:" << hr << std::system_category().message(hr); + return {}; + } + + ComPtr ctx; + dev->GetImmediateContext(address(ctx)); + ctx->CopySubresourceRegion(get(copy), 0, 0, 0, 0, from, index, nullptr); + return copy; +} + +class VideoBuffer_D3D11: public QAVVideoBuffer_GPU +{ +public: + VideoBuffer_D3D11(const QAVVideoFrame &frame) + : QAVVideoBuffer_GPU(frame) + { + } + + QAVVideoFrame::HandleType handleType() const override + { + return QAVVideoFrame::D3D11Texture2DHandle; + } + + QVariant handle(QRhi *rhi) const override + { + if (!rhi || rhi->backend() != QRhi::D3D11) + return {}; + + if (!m_texture) { + if (frame().format() != AV_PIX_FMT_NV12) { + qWarning() << "Only NV12 is supported"; + return {}; + } + auto av_frame = frame().frame(); + auto texture = (ID3D11Texture2D *)(uintptr_t)av_frame->data[0]; + auto texture_index = (intptr_t)av_frame->data[1]; + if (!texture) { + qWarning() << "No texture in the frame" << frame().pts(); + return {}; + } + auto nh = static_cast(rhi->nativeHandles()); + if (!nh) { + qWarning() << "No QRhiD3D11NativeHandles"; + return {}; + } + + auto dev = reinterpret_cast(nh->dev); + if (!dev) { + qWarning() << "No ID3D11Device device"; + return {}; + } + auto shared = shareTexture(dev, texture); + if (shared) + const_cast(this)->m_texture = copyTexture(dev, get(shared), texture_index); + } + + QList textures = {quint64(get(m_texture)), quint64(get(m_texture))}; + return QVariant::fromValue(textures); + } + + ComPtr m_texture; +}; + +QAVVideoBuffer *QAVHWDevice_D3D11::videoBuffer(const QAVVideoFrame &frame) const +{ + return new VideoBuffer_D3D11(frame); +} + +#else // QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + +QAVVideoBuffer *QAVHWDevice_D3D11::videoBuffer(const QAVVideoFrame &frame) const +{ + return new QAVVideoBuffer_GPU(frame); +} + +#endif + +#else // QT_AVPLAYER_MULTIMEDIA + +QAVVideoBuffer *QAVHWDevice_D3D11::videoBuffer(const QAVVideoFrame &frame) const +{ + return new QAVVideoBuffer_GPU(frame); +} + +#endif // QT_AVPLAYER_MULTIMEDIA + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11_p.h new file mode 100644 index 0000000..9214341 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11_p.h @@ -0,0 +1,44 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_D3D11_P_H +#define QAVHWDEVICE_D3D11_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavhwdevice_p.h" + +QT_BEGIN_NAMESPACE + +struct AVCodecContext; +class QAVHWDevice_D3D11 : public QAVHWDevice +{ +public: + QAVHWDevice_D3D11() = default; + ~QAVHWDevice_D3D11() = default; + + void init(AVCodecContext *avctx) override; + AVPixelFormat format() const override; + AVHWDeviceType type() const override; + QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const override; + +private: + Q_DISABLE_COPY(QAVHWDevice_D3D11) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec.cpp b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec.cpp new file mode 100644 index 0000000..a4157b9 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec.cpp @@ -0,0 +1,115 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavhwdevice_mediacodec_p.h" +#include "qavvideobuffer_gpu_p.h" +#include "qavcodec_p.h" +#include "qavandroidsurfacetexture_p.h" +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +Q_GLOBAL_STATIC(QAVAndroidSurfaceTexture, androidSurfaceTexture); + +class QAVHWDevice_MediaCodecPrivate +{ +public: + GLuint texture = 0; +}; + +QAVHWDevice_MediaCodec::QAVHWDevice_MediaCodec() + : d_ptr(new QAVHWDevice_MediaCodecPrivate) +{ +} + +QAVHWDevice_MediaCodec::~QAVHWDevice_MediaCodec() +{ + Q_D(QAVHWDevice_MediaCodec); + if (d->texture) + glDeleteTextures(1, &d->texture); +} + +void QAVHWDevice_MediaCodec::init(AVCodecContext *avctx) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + AVBufferRef *hw_device_ctx = avctx->hw_device_ctx; + if (hw_device_ctx) { + AVHWDeviceContext *deviceContext = reinterpret_cast(hw_device_ctx->data); + if (deviceContext->hwctx) { + AVMediaCodecDeviceContext *mediaDeviceContext = + reinterpret_cast(deviceContext->hwctx); + if (mediaDeviceContext) + mediaDeviceContext->surface = androidSurfaceTexture->surface(); + } + } +#else + Q_UNUSED(avctx); +#endif +} + +AVPixelFormat QAVHWDevice_MediaCodec::format() const +{ + return AV_PIX_FMT_MEDIACODEC; +} + +AVHWDeviceType QAVHWDevice_MediaCodec::type() const +{ + return AV_HWDEVICE_TYPE_MEDIACODEC; +} + +class VideoBuffer_MediaCodec : public QAVVideoBuffer_GPU +{ +public: + VideoBuffer_MediaCodec(QAVHWDevice_MediaCodecPrivate *hw, const QAVVideoFrame &frame) + : QAVVideoBuffer_GPU(frame) + , m_hw(hw) + { + } + + QAVVideoFrame::HandleType handleType() const override + { + return QAVVideoFrame::GLTextureHandle; + } + + QVariant handle(QRhi */*rhi*/) const override + { + if (!androidSurfaceTexture->isValid()) + return {}; + + if (!m_hw->texture) { + androidSurfaceTexture->detachFromGLContext(); + glGenTextures(1, &m_hw->texture); + androidSurfaceTexture->attachToGLContext(m_hw->texture); + } + + AVMediaCodecBuffer *buffer = reinterpret_cast(frame().frame()->data[3]); + if (!buffer) { + qWarning() << "Received a frame without AVMediaCodecBuffer."; + } else if (av_mediacodec_release_buffer(buffer, 1) < 0) { + qWarning() << "Failed to render buffer to surface."; + return {}; + } + + androidSurfaceTexture->updateTexImage(); + return m_hw->texture; + } + + QAVHWDevice_MediaCodecPrivate *m_hw = nullptr; +}; + +QAVVideoBuffer *QAVHWDevice_MediaCodec::videoBuffer(const QAVVideoFrame &frame) const +{ + return new VideoBuffer_MediaCodec(d_ptr.get(), frame); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec_p.h new file mode 100644 index 0000000..5a8b0f4 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec_p.h @@ -0,0 +1,47 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_MEDIACODEC_P_H +#define QAVHWDEVICE_MEDIACODEC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavhwdevice_p.h" +#include + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_MediaCodecPrivate; +class QAVHWDevice_MediaCodec : public QAVHWDevice +{ +public: + QAVHWDevice_MediaCodec(); + ~QAVHWDevice_MediaCodec(); + + void init(AVCodecContext *avctx) override; + AVPixelFormat format() const override; + AVHWDeviceType type() const override; + QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const override; + +private: + std::unique_ptr d_ptr; + Q_DISABLE_COPY(QAVHWDevice_MediaCodec) + Q_DECLARE_PRIVATE(QAVHWDevice_MediaCodec) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_p.h new file mode 100644 index 0000000..b91d4d2 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_p.h @@ -0,0 +1,51 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_P_H +#define QAVHWDEVICE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavvideoframe.h" +#include "qtavplayerglobal.h" + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +struct AVCodecContext; +class QAVVideoBuffer; +class QAVHWDevice +{ +public: + QAVHWDevice() = default; + virtual ~QAVHWDevice() = default; + + virtual void init(AVCodecContext *) { } + virtual AVPixelFormat format() const = 0; + virtual AVHWDeviceType type() const = 0; + virtual QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const = 0; + +private: + Q_DISABLE_COPY(QAVHWDevice) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl.cpp b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl.cpp new file mode 100644 index 0000000..435aa91 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl.cpp @@ -0,0 +1,163 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavhwdevice_vaapi_drm_egl_p.h" +#include "qavvideocodec_p.h" +#include "qavvideobuffer_gpu_p.h" +#include "qavstream.h" +#include + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +static PFNEGLCREATEIMAGEKHRPROC s_eglCreateImageKHR = nullptr; +static PFNEGLDESTROYIMAGEKHRPROC s_eglDestroyImageKHR = nullptr; +static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC s_glEGLImageTargetTexture2DOES = nullptr; + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VAAPI_DRM_EGLPrivate +{ +public: + GLuint textures[2] = {0}; +}; + +QAVHWDevice_VAAPI_DRM_EGL::QAVHWDevice_VAAPI_DRM_EGL() + : d_ptr(new QAVHWDevice_VAAPI_DRM_EGLPrivate) +{ +} + +QAVHWDevice_VAAPI_DRM_EGL::~QAVHWDevice_VAAPI_DRM_EGL() +{ + Q_D(QAVHWDevice_VAAPI_DRM_EGL); + + if (d->textures[0]) + glDeleteTextures(2, &d->textures[0]); +} + +AVPixelFormat QAVHWDevice_VAAPI_DRM_EGL::format() const +{ + return AV_PIX_FMT_VAAPI; +} + +AVHWDeviceType QAVHWDevice_VAAPI_DRM_EGL::type() const +{ + return AV_HWDEVICE_TYPE_VAAPI; +} + +class VideoBuffer_EGL : public QAVVideoBuffer_GPU +{ +public: + VideoBuffer_EGL(QAVHWDevice_VAAPI_DRM_EGLPrivate *hw, const QAVVideoFrame &frame) + : QAVVideoBuffer_GPU(frame) + , m_hw(hw) + { + if (!s_eglCreateImageKHR) { + s_eglCreateImageKHR = reinterpret_cast(eglGetProcAddress("eglCreateImageKHR")); + s_eglDestroyImageKHR = reinterpret_cast(eglGetProcAddress("eglDestroyImageKHR")); + s_glEGLImageTargetTexture2DOES = reinterpret_cast(eglGetProcAddress("glEGLImageTargetTexture2DOES")); + } + } + + QAVVideoFrame::HandleType handleType() const override + { + return QAVVideoFrame::GLTextureHandle; + } + + QVariant textures() const + { + return QList() << m_hw->textures[0] << m_hw->textures[1]; + } + + QVariant handle(QRhi */*rhi*/) const override + { + if (!m_hw->textures[0]) + glGenTextures(2, m_hw->textures); + + auto va_frame = frame().frame(); + AVHWDeviceContext *hwctx = (AVHWDeviceContext *)frame().stream().codec()->avctx()->hw_device_ctx->data; + AVVAAPIDeviceContext *vactx = (AVVAAPIDeviceContext *)hwctx->hwctx; + VADisplay va_display = vactx->display; + VASurfaceID va_surface = (VASurfaceID)(uintptr_t)va_frame->data[3]; + + VADRMPRIMESurfaceDescriptor prime; + auto status = vaExportSurfaceHandle(va_display, va_surface, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, + &prime); + if (status != VA_STATUS_SUCCESS) { + qWarning() << "vaExportSurfaceHandle failed" << status; + return textures(); + } + + if (prime.fourcc != VA_FOURCC_NV12) { + qWarning() << "prime.fourcc != VA_FOURCC_NV12"; + return textures(); + } + + vaSyncSurface(va_display, va_surface); + + static const uint32_t formats[2] = { DRM_FORMAT_R8, DRM_FORMAT_GR88 }; + for (int i = 0; i < 2; ++i) { + if (prime.layers[i].drm_format != formats[i]) + qWarning() << "Wrong DRM format:" << prime.layers[i].drm_format << formats[i]; + + EGLint img_attr[] = { + EGL_LINUX_DRM_FOURCC_EXT, EGLint(formats[i]), + EGL_WIDTH, va_frame->width / (i + 1), + EGL_HEIGHT, va_frame->height / (i + 1), + EGL_DMA_BUF_PLANE0_FD_EXT, prime.objects[prime.layers[i].object_index[0]].fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGLint(prime.layers[i].offset[0]), + EGL_DMA_BUF_PLANE0_PITCH_EXT, EGLint(prime.layers[i].pitch[0]), + EGL_NONE + }; + + EGLImage img = s_eglCreateImageKHR(eglGetCurrentDisplay(), + EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, + NULL, img_attr); + if (!img) { + qWarning() << "eglCreateImageKHR failed"; + return textures(); + } + + glActiveTexture(GL_TEXTURE0 + i); + glBindTexture(GL_TEXTURE_2D, m_hw->textures[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + s_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, img); + if (glGetError()) + qWarning() << "glEGLImageTargetTexture2DOES failed"; + + glBindTexture(GL_TEXTURE_2D, 0); + s_eglDestroyImageKHR(eglGetCurrentDisplay(), img); + } + + return textures(); + } + + QAVHWDevice_VAAPI_DRM_EGLPrivate *m_hw = nullptr; +}; + +QAVVideoBuffer *QAVHWDevice_VAAPI_DRM_EGL::videoBuffer(const QAVVideoFrame &frame) const +{ + return new VideoBuffer_EGL(d_ptr.get(), frame); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl_p.h new file mode 100644 index 0000000..5c008d3 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl_p.h @@ -0,0 +1,46 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_VAAPI_DRM_EGL_P_H +#define QAVHWDEVICE_VAAPI_DRM_EGL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavhwdevice_p.h" +#include + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VAAPI_DRM_EGLPrivate; +class QAVHWDevice_VAAPI_DRM_EGL : public QAVHWDevice +{ +public: + QAVHWDevice_VAAPI_DRM_EGL(); + ~QAVHWDevice_VAAPI_DRM_EGL(); + + AVPixelFormat format() const override; + AVHWDeviceType type() const override; + QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const override; + +private: + std::unique_ptr d_ptr; + Q_DISABLE_COPY(QAVHWDevice_VAAPI_DRM_EGL) + Q_DECLARE_PRIVATE(QAVHWDevice_VAAPI_DRM_EGL) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx.cpp b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx.cpp new file mode 100644 index 0000000..9e12041 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx.cpp @@ -0,0 +1,180 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavhwdevice_vaapi_x11_glx_p.h" +#include "qavvideocodec_p.h" +#include "qavstream.h" +#include "qavvideobuffer_gpu_p.h" +#include + +#include +#include + +extern "C" { +#include +#include +} + +typedef void (*glXBindTexImageEXT_)(Display *dpy, GLXDrawable drawable, int buffer, const int *attrib_list); +typedef void (*glXReleaseTexImageEXT_)(Display *dpy, GLXDrawable draw, int buffer); +static glXBindTexImageEXT_ s_glXBindTexImageEXT = nullptr; +static glXReleaseTexImageEXT_ s_glXReleaseTexImageEXT = nullptr; + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VAAPI_X11_GLXPrivate +{ +public: + Pixmap pixmap = 0; + GLXPixmap glxpixmap = 0; + Display *display = nullptr; + GLuint texture = 0; +}; + +QAVHWDevice_VAAPI_X11_GLX::QAVHWDevice_VAAPI_X11_GLX() + : d_ptr(new QAVHWDevice_VAAPI_X11_GLXPrivate) +{ +} + +QAVHWDevice_VAAPI_X11_GLX::~QAVHWDevice_VAAPI_X11_GLX() +{ + Q_D(QAVHWDevice_VAAPI_X11_GLX); + + if (d->glxpixmap) { + s_glXReleaseTexImageEXT(d->display, d->glxpixmap, GLX_FRONT_EXT); + glXDestroyPixmap(d->display, d->glxpixmap); + } + if (d->pixmap) + XFreePixmap(d->display, d->pixmap); + + if (d->texture) + glDeleteTextures(1, &d->texture); +} + +AVPixelFormat QAVHWDevice_VAAPI_X11_GLX::format() const +{ + return AV_PIX_FMT_VAAPI; +} + +AVHWDeviceType QAVHWDevice_VAAPI_X11_GLX::type() const +{ + return AV_HWDEVICE_TYPE_VAAPI; +} + +class VideoBuffer_GLX : public QAVVideoBuffer_GPU +{ +public: + VideoBuffer_GLX(QAVHWDevice_VAAPI_X11_GLXPrivate *hw, const QAVVideoFrame &frame) + : QAVVideoBuffer_GPU(frame) + , m_hw(hw) + { + if (!s_glXBindTexImageEXT) { + s_glXBindTexImageEXT = (glXBindTexImageEXT_) glXGetProcAddressARB((const GLubyte *)"glXBindTexImageEXT"); + s_glXReleaseTexImageEXT = (glXReleaseTexImageEXT_) glXGetProcAddressARB((const GLubyte *)"glXReleaseTexImageEXT"); + } + } + + QAVVideoFrame::HandleType handleType() const override + { + return QAVVideoFrame::GLTextureHandle; + } + + QVariant handle(QRhi */*rhi*/) const override + { + if (!s_glXBindTexImageEXT) { + qWarning() << "Could not get proc address: s_glXBindTexImageEXT"; + return 0; + } + + auto av_frame = frame().frame(); + AVHWDeviceContext *hwctx = (AVHWDeviceContext *)frame().stream().codec()->avctx()->hw_device_ctx->data; + AVVAAPIDeviceContext *vactx = (AVVAAPIDeviceContext *)hwctx->hwctx; + VADisplay va_display = vactx->display; + VASurfaceID va_surface = (VASurfaceID)(uintptr_t)av_frame->data[3]; + + int w = av_frame->width; + int h = av_frame->height; + + if (!m_hw->display) { + glGenTextures(1, &m_hw->texture); + auto display = (Display *)glXGetCurrentDisplay(); + m_hw->display = display; + int xscr = DefaultScreen(display); + const char *glxext = glXQueryExtensionsString(display, xscr); + if (!glxext || !strstr(glxext, "GLX_EXT_texture_from_pixmap")) { + qWarning() << "GLX_EXT_texture_from_pixmap is not supported"; + return 0; + } + + int attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_X_RENDERABLE, True, + GLX_BIND_TO_TEXTURE_RGBA_EXT, True, + GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT, + GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT, + GLX_Y_INVERTED_EXT, True, + GLX_DOUBLEBUFFER, False, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, + GLX_ALPHA_SIZE, 8, + None + }; + + int fbcount; + GLXFBConfig *fbcs = glXChooseFBConfig(display, xscr, attribs, &fbcount); + if (!fbcount) { + XFree(fbcs); + qWarning() << "No texture-from-pixmap support"; + return 0; + } + + GLXFBConfig fbc = fbcs[0]; + XFree(fbcs); + + XWindowAttributes xwa; + XGetWindowAttributes(display, DefaultRootWindow(display), &xwa); + + m_hw->pixmap = XCreatePixmap(display, DefaultRootWindow(display), w, h, xwa.depth); + + const int attribs_pixmap[] = { + GLX_TEXTURE_TARGET_EXT, GLX_TEXTURE_2D_EXT, + GLX_TEXTURE_FORMAT_EXT, xwa.depth == 32 ? GLX_TEXTURE_FORMAT_RGBA_EXT : GLX_TEXTURE_FORMAT_RGB_EXT, + GLX_MIPMAP_TEXTURE_EXT, False, + None, + }; + + m_hw->glxpixmap = glXCreatePixmap(display, fbc, m_hw->pixmap, attribs_pixmap); + } + + vaSyncSurface(va_display, va_surface); + auto status = vaPutSurface(va_display, va_surface, m_hw->pixmap, + 0, 0, w, h, + 0, 0, w, h, + NULL, 0, VA_FRAME_PICTURE | VA_SRC_BT709); + if (status != VA_STATUS_SUCCESS) { + qWarning() << "vaPutSurface failed" << status; + return 0; + } + + XSync(m_hw->display, False); + glBindTexture(GL_TEXTURE_2D, m_hw->texture); + s_glXBindTexImageEXT(m_hw->display, m_hw->glxpixmap, GLX_FRONT_EXT, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + + return m_hw->texture; + } + + QAVHWDevice_VAAPI_X11_GLXPrivate *m_hw = nullptr; +}; + +QAVVideoBuffer *QAVHWDevice_VAAPI_X11_GLX::videoBuffer(const QAVVideoFrame &frame) const +{ + return new VideoBuffer_GLX(d_ptr.get(), frame); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx_p.h new file mode 100644 index 0000000..56b05c0 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx_p.h @@ -0,0 +1,46 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_VAAPI_X11_GLX_P_H +#define QAVHWDEVICE_VAAPI_X11_GLX_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavhwdevice_p.h" +#include + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VAAPI_X11_GLXPrivate; +class QAVHWDevice_VAAPI_X11_GLX : public QAVHWDevice +{ +public: + QAVHWDevice_VAAPI_X11_GLX(); + ~QAVHWDevice_VAAPI_X11_GLX(); + + AVPixelFormat format() const override; + AVHWDeviceType type() const override; + QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const override; + +private: + std::unique_ptr d_ptr; + Q_DISABLE_COPY(QAVHWDevice_VAAPI_X11_GLX) + Q_DECLARE_PRIVATE(QAVHWDevice_VAAPI_X11_GLX) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau.cpp b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau.cpp new file mode 100644 index 0000000..3d589bc --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau.cpp @@ -0,0 +1,235 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavhwdevice_vdpau_p.h" +#include "qavvideocodec_p.h" +#include "qavvideobuffer_gpu_p.h" +#include + +#include + +extern "C" { +#include +#include +} + +typedef void (*VDPAUInitNV_)(const GLvoid *, const GLvoid *); +static VDPAUInitNV_ s_VDPAUInitNV = nullptr; + +typedef void (*VDPAUFiniNV_)(void); +static VDPAUFiniNV_ s_VDPAUFiniNV = nullptr; + +typedef GLvdpauSurfaceNV (*VDPAURegisterOutputSurfaceNV_)(GLvoid *, GLenum, GLsizei, const GLuint *); +static VDPAURegisterOutputSurfaceNV_ s_VDPAURegisterOutputSurfaceNV = nullptr; + +typedef void (*VDPAUSurfaceAccessNV_)(GLvdpauSurfaceNV, GLenum); +static VDPAUSurfaceAccessNV_ s_VDPAUSurfaceAccessNV = nullptr; + +typedef void (*VDPAUMapSurfacesNV_)(GLsizei, const GLvdpauSurfaceNV *); +static VDPAUMapSurfacesNV_ s_VDPAUMapSurfacesNV = nullptr; + +typedef void (*VDPAUUnmapSurfacesNV_)(GLsizei, const GLvdpauSurfaceNV *); +static VDPAUUnmapSurfacesNV_ s_VDPAUUnmapSurfacesNV = nullptr; + +typedef void (*VDPAUUnregisterSurfaceNV_)(GLvdpauSurfaceNV); +static VDPAUUnregisterSurfaceNV_ s_VDPAUUnregisterSurfaceNV = nullptr; + +static VdpOutputSurfaceCreate *s_output_surface_create = nullptr; +static VdpOutputSurfaceDestroy *s_output_surface_destroy = nullptr; +static VdpVideoMixerCreate *s_video_mixer_create = nullptr; +static VdpVideoMixerDestroy *s_video_mixer_destroy = nullptr; +static VdpVideoSurfaceGetParameters *s_video_surface_get_parameters = nullptr; +static VdpVideoMixerRender *s_video_mixer_render = nullptr; + +#define VDP_NUM_MIXER_PARAMETER 3 +#define MAX_NUM_FEATURES 6 +#define MP_VDP_HISTORY_FRAMES 2 + +QT_BEGIN_NAMESPACE + +QAVHWDevice_VDPAU::QAVHWDevice_VDPAU() +{ +} + +QAVHWDevice_VDPAU::~QAVHWDevice_VDPAU() +{ +} + +AVPixelFormat QAVHWDevice_VDPAU::format() const +{ + return AV_PIX_FMT_VDPAU; +} + +AVHWDeviceType QAVHWDevice_VDPAU::type() const +{ + return AV_HWDEVICE_TYPE_VDPAU; +} + +template +static int get_proc_address(AVVDPAUDeviceContext *vactx, VdpFuncId funcid, T &out) +{ + void *tmp = nullptr; + auto err = vactx->get_proc_address(vactx->device, funcid, &tmp); + if (err != VDP_STATUS_OK) + qWarning() << "Could not get proc address:" << funcid; + out = reinterpret_cast(tmp); + return err; +} + +class VideoBuffer_VDPAU_GLX : public QAVVideoBuffer_GPU +{ +public: + VideoBuffer_VDPAU_GLX(const QAVVideoFrame &frame) + : QAVVideoBuffer_GPU(frame) + { + if (!s_VDPAUInitNV) { + s_VDPAUInitNV = (VDPAUInitNV_) glXGetProcAddressARB((const GLubyte *)"VDPAUInitNV"); + s_VDPAUFiniNV = (VDPAUFiniNV_) glXGetProcAddressARB((const GLubyte *)"VDPAUFiniNV"); + s_VDPAURegisterOutputSurfaceNV = (VDPAURegisterOutputSurfaceNV_) glXGetProcAddressARB((const GLubyte *)"VDPAURegisterOutputSurfaceNV"); + s_VDPAUSurfaceAccessNV = (VDPAUSurfaceAccessNV_) glXGetProcAddressARB((const GLubyte *)"VDPAUSurfaceAccessNV"); + s_VDPAUMapSurfacesNV = (VDPAUMapSurfacesNV_) glXGetProcAddressARB((const GLubyte *)"VDPAUMapSurfacesNV"); + s_VDPAUUnmapSurfacesNV = (VDPAUUnmapSurfacesNV_) glXGetProcAddressARB((const GLubyte *)"VDPAUUnmapSurfacesNV"); + s_VDPAUUnregisterSurfaceNV = (VDPAUUnregisterSurfaceNV_) glXGetProcAddressARB((const GLubyte *)"VDPAUUnregisterSurfaceNV"); + } + + for (int n = 0; n < MP_VDP_HISTORY_FRAMES; ++n) + past[n] = future[n] = VDP_INVALID_HANDLE; + } + + ~VideoBuffer_VDPAU_GLX() + { + if (vdpgl_surface) { + s_VDPAUUnmapSurfacesNV(1, &vdpgl_surface); + s_VDPAUUnregisterSurfaceNV(vdpgl_surface); + } + + if (vdp_surface != VDP_INVALID_HANDLE) + s_output_surface_destroy(vdp_surface); + + if (video_mixer != VDP_INVALID_HANDLE) + s_video_mixer_destroy(video_mixer); + + if (gl_texture) { + glDeleteTextures(1, &gl_texture); + s_VDPAUFiniNV(); + } + } + + QAVVideoFrame::HandleType handleType() const override + { + return QAVVideoFrame::GLTextureHandle; + } + + GLuint texture() + { + if (!s_VDPAUInitNV) { + qWarning() << "Could not get proc address: s_VDPAUInitNV"; + return 0; + } + + if (!frame()) + return 0; + + if (gl_texture) + return gl_texture; + + auto av_frame = frame().frame(); + int w = av_frame->width; + int h = av_frame->height; + + AVHWDeviceContext *hwctx = (AVHWDeviceContext *)frame().stream().codec()->avctx()->hw_device_ctx->data; + AVVDPAUDeviceContext *vactx = (AVVDPAUDeviceContext *)hwctx->hwctx; + VdpVideoSurface surf = (VdpVideoSurface)(uintptr_t)av_frame->data[3]; + + if (!gl_texture) { + if (!s_output_surface_create) { + get_proc_address(vactx, VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, s_output_surface_create); + get_proc_address(vactx, VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, s_output_surface_destroy); + get_proc_address(vactx, VDP_FUNC_ID_VIDEO_MIXER_CREATE, s_video_mixer_create); + get_proc_address(vactx, VDP_FUNC_ID_VIDEO_MIXER_DESTROY, s_video_mixer_destroy); + get_proc_address(vactx, VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, s_video_surface_get_parameters); + get_proc_address(vactx, VDP_FUNC_ID_VIDEO_MIXER_RENDER, s_video_mixer_render); + get_proc_address(vactx, VDP_FUNC_ID_VIDEO_MIXER_RENDER, s_video_mixer_render); + } + + VdpVideoMixerFeature features[MAX_NUM_FEATURES]; + static const VdpVideoMixerParameter parameters[VDP_NUM_MIXER_PARAMETER] = { + VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH, + VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT, + VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE, + }; + + VdpChromaType chroma_type; + uint32_t s_w, s_h; + + s_video_surface_get_parameters(surf, &chroma_type, &s_w, &s_h); + const void *const parameter_values[VDP_NUM_MIXER_PARAMETER] = { + &s_w, + &s_h, + &chroma_type, + }; + + s_video_mixer_create(vactx->device, 0, features, + VDP_NUM_MIXER_PARAMETER, + parameters, parameter_values, + &video_mixer); + + glGenTextures(1, &gl_texture); + s_VDPAUInitNV(reinterpret_cast(vactx->device), (const GLvoid *)vactx->get_proc_address); + glBindTexture(GL_TEXTURE_2D, gl_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + + s_output_surface_create(vactx->device, + VDP_RGBA_FORMAT_B8G8R8A8, + s_w, + s_h, + &vdp_surface); + + vdpgl_surface = s_VDPAURegisterOutputSurfaceNV(reinterpret_cast(vdp_surface), + GL_TEXTURE_2D, + 1, &gl_texture); + + s_VDPAUSurfaceAccessNV(vdpgl_surface, GL_READ_ONLY); + } + + VdpRect video_rect = {0, 0, static_cast(w), static_cast(h)}; + s_video_mixer_render(video_mixer, VDP_INVALID_HANDLE, + 0, field, + MP_VDP_HISTORY_FRAMES, past, + surf, + MP_VDP_HISTORY_FRAMES, future, + &video_rect, + vdp_surface, NULL, NULL, + 0, NULL); + s_VDPAUMapSurfacesNV(1, &vdpgl_surface); + return gl_texture; + } + + QVariant handle(QRhi */*rhi*/) const override + { + return const_cast(this)->texture(); + } + + GLuint gl_texture = 0; + VdpOutputSurface vdp_surface = VDP_INVALID_HANDLE; + GLvdpauSurfaceNV vdpgl_surface = 0; + VdpVideoMixer video_mixer = VDP_INVALID_HANDLE; + VdpVideoSurface past[MP_VDP_HISTORY_FRAMES]; + VdpVideoSurface future[MP_VDP_HISTORY_FRAMES]; + VdpVideoMixerPictureStructure field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME; +}; + +QAVVideoBuffer *QAVHWDevice_VDPAU::videoBuffer(const QAVVideoFrame &frame) const +{ + return new VideoBuffer_VDPAU_GLX(frame); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau_p.h new file mode 100644 index 0000000..e6cccb1 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau_p.h @@ -0,0 +1,42 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_VDPAU_P_H +#define QAVHWDEVICE_VDPAU_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavhwdevice_p.h" + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VDPAU : public QAVHWDevice +{ +public: + QAVHWDevice_VDPAU(); + ~QAVHWDevice_VDPAU(); + + AVPixelFormat format() const override; + AVHWDeviceType type() const override; + QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const override; + +private: + Q_DISABLE_COPY(QAVHWDevice_VDPAU) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox.mm b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox.mm new file mode 100644 index 0000000..6eca97b --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox.mm @@ -0,0 +1,112 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavhwdevice_videotoolbox_p.h" +#include "qavvideobuffer_gpu_p.h" + +#import +#if defined(Q_OS_MACOS) +#import +#else +#import +#endif +#import + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VideoToolboxPrivate +{ +public: + id device = nullptr; + CVPixelBufferRef pbuf = nullptr; +}; + +QAVHWDevice_VideoToolbox::QAVHWDevice_VideoToolbox() + : d_ptr(new QAVHWDevice_VideoToolboxPrivate) +{ +} + +QAVHWDevice_VideoToolbox::~QAVHWDevice_VideoToolbox() +{ + Q_D(QAVHWDevice_VideoToolbox); + CVPixelBufferRelease(d->pbuf); + [d->device release]; +} + +AVPixelFormat QAVHWDevice_VideoToolbox::format() const +{ + return AV_PIX_FMT_VIDEOTOOLBOX; +} + +AVHWDeviceType QAVHWDevice_VideoToolbox::type() const +{ + return AV_HWDEVICE_TYPE_VIDEOTOOLBOX; +} + +class VideoBuffer_MTL : public QAVVideoBuffer_GPU +{ +public: + VideoBuffer_MTL(QAVHWDevice_VideoToolboxPrivate *hw, const QAVVideoFrame &frame) + : QAVVideoBuffer_GPU(frame) + , m_hw(hw) + { + } + + QAVVideoFrame::HandleType handleType() const override + { + return QAVVideoFrame::MTLTextureHandle; + } + + QVariant handle(QRhi */*rhi*/) const override + { + CVPixelBufferRelease(m_hw->pbuf); + m_hw->pbuf = (CVPixelBufferRef)frame().frame()->data[3]; + CVPixelBufferRetain(m_hw->pbuf); + QList textures = { 0, 0 }; + + if (!m_hw->pbuf) + return textures; + + if (CVPixelBufferGetDataSize(m_hw->pbuf) <= 0) + return textures; + + auto format = CVPixelBufferGetPixelFormatType(m_hw->pbuf); + if (format != '420v') { + qWarning() << "420v is supported only"; + return textures; + } + + if (!m_hw->device) + m_hw->device = MTLCreateSystemDefaultDevice(); + + IOSurfaceRef surface = CVPixelBufferGetIOSurface(m_hw->pbuf); + int planes = CVPixelBufferGetPlaneCount(m_hw->pbuf); + for (int i = 0; i < planes; ++i) { + int w = IOSurfaceGetWidthOfPlane(surface, i); + int h = IOSurfaceGetHeightOfPlane(surface, i) ; + MTLPixelFormat f = i ? MTLPixelFormatRG8Unorm : MTLPixelFormatR8Unorm; + MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:f width:w height:h mipmapped:NO]; + + textures[i] = quint64([m_hw->device newTextureWithDescriptor:desc iosurface:surface plane:i]); + } + + return textures; + } + + QAVHWDevice_VideoToolboxPrivate *m_hw = nullptr; +}; + +QAVVideoBuffer *QAVHWDevice_VideoToolbox::videoBuffer(const QAVVideoFrame &frame) const +{ + return new VideoBuffer_MTL(d_ptr.get(), frame); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox_p.h b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox_p.h new file mode 100644 index 0000000..30b09d5 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox_p.h @@ -0,0 +1,46 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVHWDEVICE_VIDEOTOOLBOX_P_H +#define QAVHWDEVICE_VIDEOTOOLBOX_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavhwdevice_p.h" +#include + +QT_BEGIN_NAMESPACE + +class QAVHWDevice_VideoToolboxPrivate; +class QAVHWDevice_VideoToolbox : public QAVHWDevice +{ +public: + QAVHWDevice_VideoToolbox(); + ~QAVHWDevice_VideoToolbox(); + + AVPixelFormat format() const override; + AVHWDeviceType type() const override; + QAVVideoBuffer *videoBuffer(const QAVVideoFrame &frame) const override; + +private: + std::unique_ptr d_ptr; + Q_DISABLE_COPY(QAVHWDevice_VideoToolbox) + Q_DECLARE_PRIVATE(QAVHWDevice_VideoToolbox) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavinoutfilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavinoutfilter.cpp new file mode 100644 index 0000000..ebbc3b4 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavinoutfilter.cpp @@ -0,0 +1,64 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavinoutfilter_p.h" +#include "qavinoutfilter_p_p.h" +#include + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +QAVInOutFilter::QAVInOutFilter() + : QAVInOutFilter(*new QAVInOutFilterPrivate(this)) +{ +} + +QAVInOutFilter::QAVInOutFilter(QAVInOutFilterPrivate &d) + : d_ptr(&d) +{ +} + +QAVInOutFilter::~QAVInOutFilter() = default; + +QAVInOutFilter::QAVInOutFilter(const QAVInOutFilter &other) + : QAVInOutFilter() +{ + *this = other; +} + +QAVInOutFilter &QAVInOutFilter::operator=(const QAVInOutFilter &other) +{ + d_ptr->ctx = other.d_ptr->ctx; + d_ptr->name = other.d_ptr->name; + return *this; +} + +int QAVInOutFilter::configure(AVFilterGraph *graph, AVFilterInOut *in) +{ + Q_D(QAVInOutFilter); + Q_UNUSED(graph); + if (in->name) + d->name = QString::fromUtf8(in->name); + return 0; +} + +AVFilterContext *QAVInOutFilter::ctx() const +{ + Q_D(const QAVInOutFilter); + return d->ctx; +} + +QString QAVInOutFilter::name() const +{ + return d_func()->name; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p.h new file mode 100644 index 0000000..028efe6 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p.h @@ -0,0 +1,50 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVINOUTFILTER_P_H +#define QAVINOUTFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +struct AVFilterGraph; +struct AVFilterInOut; +struct AVFilterContext; +class QAVInOutFilterPrivate; +class QAVInOutFilter +{ +public: + QAVInOutFilter(); + virtual ~QAVInOutFilter(); + QAVInOutFilter(const QAVInOutFilter &other); + QAVInOutFilter &operator=(const QAVInOutFilter &other); + virtual int configure(AVFilterGraph *graph, AVFilterInOut *in); + AVFilterContext *ctx() const; + QString name() const; + +protected: + std::unique_ptr d_ptr; + QAVInOutFilter(QAVInOutFilterPrivate &d); + Q_DECLARE_PRIVATE(QAVInOutFilter) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p_p.h b/QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p_p.h new file mode 100644 index 0000000..2a9114b --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p_p.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVINOUTFILTER_P_P_H +#define QAVINOUTFILTER_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QAVInOutFilter; +struct AVFilterContext; +class QAVInOutFilterPrivate +{ +public: + QAVInOutFilterPrivate(QAVInOutFilter *q) : q_ptr(q) { } + virtual ~QAVInOutFilterPrivate() = default; + + QAVInOutFilter *q_ptr = nullptr; + AVFilterContext *ctx = nullptr; + QString name; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qaviodevice.cpp b/QtAVPlayer/src/QtAVPlayer/qaviodevice.cpp new file mode 100644 index 0000000..1888bd5 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qaviodevice.cpp @@ -0,0 +1,173 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qaviodevice.h" +#include +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +struct ReadRequest +{ + ReadRequest() = default; + ReadRequest(unsigned char *data, int maxSize): data(data), maxSize(maxSize) { } + int wroteBytes = 0; + unsigned char *data = nullptr; + int maxSize = 0; +}; + +class QAVIODevicePrivate +{ + Q_DECLARE_PUBLIC(QAVIODevice) +public: + explicit QAVIODevicePrivate(QAVIODevice *q, const QSharedPointer &device) + : q_ptr(q) + , device(device) + , buffer(static_cast(av_malloc(buffer_size))) + , ctx(avio_alloc_context(buffer, static_cast(buffer_size), 0, this, &QAVIODevicePrivate::read, nullptr, !device->isSequential() ? &QAVIODevicePrivate::seek : nullptr)) + { + if (!device->isSequential()) + ctx->seekable = AVIO_SEEKABLE_NORMAL; + } + + ~QAVIODevicePrivate() + { + av_free(ctx); + } + + void readData() + { + QMutexLocker locker(&mutex); + // if no request or it is being processed + if (readRequest.data == nullptr || readRequest.wroteBytes) + return; + + readRequest.wroteBytes = !device->atEnd() ? device->read((char *)readRequest.data, readRequest.maxSize) : AVERROR_EOF; + // Unblock the decoder thread when there is available bytes + if (readRequest.wroteBytes) { + waitCond.wakeAll(); + wakeRead = true; + } + } + + static int read(void *opaque, unsigned char *data, int maxSize) + { + auto d = static_cast(opaque); + QMutexLocker locker(&d->mutex); + if (d->aborted) + return ECANCELED; + + d->readRequest = { data, maxSize }; + // When decoder thread is the same as current + d->wakeRead = false; + locker.unlock(); + // Reading is done on thread where the object is created + QMetaObject::invokeMethod(d->q_ptr, [d] { d->readData(); }, nullptr); + locker.relock(); + // Blocks until data is available + if (!d->wakeRead) + d->waitCond.wait(&d->mutex); + + int bytes = d->readRequest.wroteBytes; + d->readRequest = {}; + return bytes; + } + + static int64_t seek(void *opaque, int64_t offset, int whence) + { + auto d = static_cast(opaque); + QMutexLocker locker(&d->mutex); + if (d->aborted) + return ECANCELED; + + int64_t pos = 0; + bool wake = false; + locker.unlock(); + QMetaObject::invokeMethod(d->q_ptr, [&] { + QMutexLocker locker(&d->mutex); + if (whence == AVSEEK_SIZE) { + pos = d->device->size() > 0 ? d->device->size() : 0; + } else { + if (whence == SEEK_END) + offset = d->device->size() - offset; + else if (whence == SEEK_CUR) + offset = d->device->pos() + offset; + + pos = d->device->seek(offset) ? d->device->pos() : -1; + } + d->waitCond.wakeAll(); + wake = true; + }, nullptr); + + locker.relock(); + if (!wake) + d->waitCond.wait(&d->mutex); + + return pos; + } + + size_t buffer_size = 64 * 1024; + QAVIODevice *q_ptr = nullptr; + QSharedPointer device; + unsigned char *buffer = nullptr; + AVIOContext *ctx = nullptr; + mutable QMutex mutex; + QWaitCondition waitCond; + bool aborted = false; + bool wakeRead = false; + ReadRequest readRequest; +}; + +QAVIODevice::QAVIODevice(const QSharedPointer &device, QObject *parent) + : QObject(parent) + , d_ptr(new QAVIODevicePrivate(this, device)) +{ + connect(device.get(), &QIODevice::readyRead, this, [this] { + Q_D(QAVIODevice); + d->readData(); + }); +} + +QAVIODevice::~QAVIODevice() +{ + abort(true); +} + +AVIOContext *QAVIODevice::ctx() const +{ + return d_func()->ctx; +} + +void QAVIODevice::abort(bool aborted) +{ + Q_D(QAVIODevice); + QMutexLocker locker(&d->mutex); + d->aborted = aborted; + d->waitCond.wakeAll(); +} + +void QAVIODevice::setBufferSize(size_t size) +{ + Q_D(QAVIODevice); + QMutexLocker locker(&d->mutex); + d->buffer_size = size; +} + +size_t QAVIODevice::bufferSize() const +{ + Q_D(const QAVIODevice); + QMutexLocker locker(&d->mutex); + return d->buffer_size; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qaviodevice.h b/QtAVPlayer/src/QtAVPlayer/qaviodevice.h new file mode 100644 index 0000000..a9abbec --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qaviodevice.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFIODEVICE_P_H +#define QAVFIODEVICE_P_H + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +struct AVIOContext; +class QAVIODevicePrivate; +class QAVIODevice : public QObject +{ +public: + QAVIODevice(const QSharedPointer &device, QObject *parent = nullptr); + ~QAVIODevice(); + + AVIOContext *ctx() const; + void abort(bool aborted); + + void setBufferSize(size_t size); + size_t bufferSize() const; + +protected: + std::unique_ptr d_ptr; + +private: + Q_DECLARE_PRIVATE(QAVIODevice) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavpacket.cpp b/QtAVPlayer/src/QtAVPlayer/qavpacket.cpp new file mode 100644 index 0000000..b277748 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavpacket.cpp @@ -0,0 +1,108 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavpacket_p.h" +#include "qavcodec_p.h" +#include "qavstream.h" +#include +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVPacketPrivate +{ +public: + AVPacket *pkt = nullptr; + QAVStream stream; +}; + +QAVPacket::QAVPacket() + : d_ptr(new QAVPacketPrivate) +{ + d_ptr->pkt = av_packet_alloc(); + d_ptr->pkt->size = 0; + d_ptr->pkt->stream_index = -1; + d_ptr->pkt->pts = AV_NOPTS_VALUE; +} + +QAVPacket::QAVPacket(const QAVPacket &other) + : QAVPacket() +{ + *this = other; +} + +QAVPacket &QAVPacket::operator=(const QAVPacket &other) +{ + av_packet_unref(d_ptr->pkt); + av_packet_ref(d_ptr->pkt, other.d_ptr->pkt); + + d_ptr->stream = other.d_ptr->stream; + + return *this; +} + +QAVPacket::operator bool() const +{ + Q_D(const QAVPacket); + return d->pkt->size; +} + +QAVPacket::~QAVPacket() +{ + Q_D(QAVPacket); + av_packet_free(&d->pkt); +} + +AVPacket *QAVPacket::packet() const +{ + return d_func()->pkt; +} + +double QAVPacket::duration() const +{ + Q_D(const QAVPacket); + if (!d->stream) + return 0.0; + + auto tb = d->stream.stream()->time_base; + return tb.num && tb.den ? d->pkt->duration * av_q2d(tb) : 0.0; +} + +double QAVPacket::pts() const +{ + Q_D(const QAVPacket); + if (!d->stream) + return 0.0; + auto tb = d->stream.stream()->time_base; + return tb.num && tb.den ? d->pkt->pts * av_q2d(tb) : 0.0; +} + +QAVStream QAVPacket::stream() const +{ + Q_D(const QAVPacket); + return d->stream; +} + +void QAVPacket::setStream(const QAVStream &stream) +{ + Q_D(QAVPacket); + d->stream = stream; +} + +int QAVPacket::send() const +{ + Q_D(const QAVPacket); + return d->stream ? d->stream.codec()->write(*this) : 0; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavpacket_p.h b/QtAVPlayer/src/QtAVPlayer/qavpacket_p.h new file mode 100644 index 0000000..fd09913 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavpacket_p.h @@ -0,0 +1,58 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVPACKET_H +#define QAVPACKET_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavframe.h" +#include "qavstream.h" +#include + +QT_BEGIN_NAMESPACE + +struct AVPacket; +class QAVPacketPrivate; +class QAVPacket +{ +public: + QAVPacket(); + ~QAVPacket(); + QAVPacket(const QAVPacket &other); + QAVPacket &operator=(const QAVPacket &other); + operator bool() const; + + AVPacket *packet() const; + double duration() const; + double pts() const; + + QAVStream stream() const; + void setStream(const QAVStream &stream); + + // Sends the packet to the codec + int send() const; + +protected: + std::unique_ptr d_ptr; + +private: + Q_DECLARE_PRIVATE(QAVPacket) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavpacketqueue_p.h b/QtAVPlayer/src/QtAVPlayer/qavpacketqueue_p.h new file mode 100644 index 0000000..1cc048c --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavpacketqueue_p.h @@ -0,0 +1,275 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVPACKETQUEUE_H +#define QAVPACKETQUEUE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavpacket_p.h" +#include "qavfilter_p.h" +#include "qavfiltergraph_p.h" +#include "qavframe.h" +#include "qavsubtitleframe.h" +#include "qavstreamframe.h" +#include "qavdemuxer_p.h" +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVQueueClock +{ +public: + QAVQueueClock(double v = 0.0) + : frameRate(v) + { + } + + bool wait(bool shouldSync, double pts, double speed = 1.0, double master = -1) + { + QMutexLocker locker(&m_mutex); + double delay = pts - prevPts; + if (isnan(delay) || delay <= 0 || delay > maxFrameDuration) + delay = frameRate; + + if (master > 0) { + double diff = pts - master; + double sync_threshold = qMax(minThreshold, qMin(maxThreshold, delay)); + if (!isnan(diff) && fabs(diff) < maxFrameDuration) { + if (diff <= -sync_threshold) + delay = qMax(0.0, delay + diff); + else if (diff >= sync_threshold && delay > frameDuplicationThreshold) + delay = delay + diff; + else if (diff >= sync_threshold) + delay = 2 * delay; + } + } + + delay /= speed; + const double time = av_gettime_relative() / 1000000.0; + if (shouldSync) { + if (time < frameTimer + delay) { + double remaining_time = qMin(frameTimer + delay - time, refreshRate); + locker.unlock(); + av_usleep((int64_t)(remaining_time * 1000000.0)); + return false; + } + } + + prevPts = pts; + frameTimer += delay; + if ((delay > 0 && time - frameTimer > maxThreshold) || !shouldSync) + frameTimer = time; + + return true; + } + + double pts() const + { + QMutexLocker locker(&m_mutex); + return prevPts; + } + + void clear() + { + QMutexLocker locker(&m_mutex); + prevPts = 0; + frameTimer = 0; + } + + void setFrameRate(double v) + { + QMutexLocker locker(&m_mutex); + frameRate = v; + } + +private: + double frameRate = 0; + double frameTimer = 0; + double prevPts = 0; + mutable QMutex m_mutex; + const double maxFrameDuration = 10.0; + const double minThreshold = 0.04; + const double maxThreshold = 0.1; + const double frameDuplicationThreshold = 0.1; + const double refreshRate = 0.01; +}; + +template +class QAVPacketQueue +{ +public: + QAVPacketQueue(AVMediaType mediaType, QAVDemuxer &demuxer) + : m_mediaType(mediaType) + , m_demuxer(demuxer) + { + } + + ~QAVPacketQueue() + { + abort(); + } + + AVMediaType mediaType() const + { + return m_mediaType; + } + + bool isEmpty() const + { + QMutexLocker locker(&m_mutex); + return m_packets.isEmpty() && m_decodedFrames.isEmpty(); + } + + void enqueue(const QAVPacket &packet) + { + QMutexLocker locker(&m_mutex); + m_packets.append(packet); + m_bytes += packet.packet()->size + sizeof(packet); + m_duration += packet.duration(); + m_consumerWaiter.wakeAll(); + m_abort = false; + m_waitingForPackets = false; + } + + bool frontFrame(T &frame) + { + QMutexLocker locker(&m_mutex); + if (m_decodedFrames.isEmpty()) + m_demuxer.decode(dequeue(), m_decodedFrames); + if (m_decodedFrames.isEmpty()) + return false; + frame = m_decodedFrames.front(); + return true; + } + + void popFrame() + { + QMutexLocker locker(&m_mutex); + if (!m_decodedFrames.isEmpty()) + m_decodedFrames.pop_front(); + } + + void waitForEmpty() + { + QMutexLocker locker(&m_mutex); + clearPackets(); + if (!m_abort && !m_waitingForPackets) + m_producerWaiter.wait(&m_mutex); + } + + void abort() + { + QMutexLocker locker(&m_mutex); + m_abort = true; + m_waitingForPackets = true; + m_consumerWaiter.wakeAll(); + m_producerWaiter.wakeAll(); + } + + bool enough() const + { + QMutexLocker locker(&m_mutex); + const int minFrames = 15; + return m_packets.size() > minFrames && (!m_duration || m_duration > 1.0); + } + + int bytes() const + { + QMutexLocker locker(&m_mutex); + return m_bytes; + } + + void clear() + { + QMutexLocker locker(&m_mutex); + clearPackets(); + } + + void clearFrames() + { + QMutexLocker locker(&m_mutex); + m_decodedFrames.clear(); + } + + void wake(bool wake) + { + QMutexLocker locker(&m_mutex); + if (wake) + m_consumerWaiter.wakeAll(); + m_wake = wake; + } + +private: + QAVPacket dequeue() + { + if (m_packets.isEmpty()) { + m_producerWaiter.wakeAll(); + if (!m_abort && !m_wake) { + m_waitingForPackets = true; + m_consumerWaiter.wait(&m_mutex); + m_waitingForPackets = false; + } + } + if (m_packets.isEmpty()) + return {}; + + auto packet = m_packets.takeFirst(); + m_bytes -= packet.packet()->size + sizeof(packet); + m_duration -= packet.duration(); + return packet; + } + + void clearPackets() + { + m_packets.clear(); + m_decodedFrames.clear(); + m_bytes = 0; + m_duration = 0; + } + + const AVMediaType m_mediaType = AVMEDIA_TYPE_UNKNOWN; + QAVDemuxer &m_demuxer; + QList m_packets; + // Tracks decoded frames to prevent EOF if not all frames are landed + QList m_decodedFrames; + mutable QMutex m_mutex; + QWaitCondition m_consumerWaiter; + QWaitCondition m_producerWaiter; + bool m_abort = false; + bool m_waitingForPackets = true; + bool m_wake = false; + + int m_bytes = 0; + int m_duration = 0; + +private: + Q_DISABLE_COPY(QAVPacketQueue) +}; + + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavplayer.cpp b/QtAVPlayer/src/QtAVPlayer/qavplayer.cpp new file mode 100644 index 0000000..15ccddb --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavplayer.cpp @@ -0,0 +1,1399 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavplayer.h" +#include "qavdemuxer_p.h" +#include "qaviodevice.h" +#include "qavvideocodec_p.h" +#include "qavaudiocodec_p.h" +#include "qavvideoframe.h" +#include "qavaudioframe.h" +#include "qavsubtitleframe.h" +#include "qavpacketqueue_p.h" +#include "qavfiltergraph_p.h" +#include "qavvideofilter_p.h" +#include "qavaudiofilter_p.h" +#include "qavfilters_p.h" +#include +#include +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcAVPlayer, "qt.QtAVPlayer") + +enum PendingMediaStatus +{ + LoadingMedia, + PlayingMedia, + PausingMedia, + StoppingMedia, + SteppingMedia, + SeekingMedia, + EndOfMedia +}; + +class QAVPlayerPrivate +{ + Q_DECLARE_PUBLIC(QAVPlayer) +public: + QAVPlayerPrivate(QAVPlayer *q) + : q_ptr(q) + , videoQueue(AVMEDIA_TYPE_VIDEO, demuxer) + , audioQueue(AVMEDIA_TYPE_AUDIO, demuxer) + , subtitleQueue(AVMEDIA_TYPE_SUBTITLE, demuxer) + { + threadPool.setMaxThreadCount(4); + } + + QAVPlayer::Error currentError() const; + void setMediaStatus(QAVPlayer::MediaStatus status); + void resetPendingStatuses(); + void setPendingMediaStatus(PendingMediaStatus status); + void step(bool hasFrame); + bool doStep(PendingMediaStatus status, bool hasFrame); + bool setState(QAVPlayer::State s); + void setSeekable(bool seekable); + void setError(QAVPlayer::Error err, const QString &str); + void setDuration(double d); + bool isSeeking() const; + bool isEndOfFile() const; + void endOfFile(bool v); + void setVideoFrameRate(double v); + void setPts(double v); + double pts() const; + void applyFilters(); + void applyFilters(bool reset, const QAVFrame &frame); + + void terminate(); + + void doWait(); + void wait(bool v); + void doLoad(); + void doDemux(); + bool skipFrame( + bool master, + const QAVStreamFrame &frame, + bool isEmpty); + bool doApplyFilters( + const QAVFrame &decodedFrame, + const std::vector> &filters, + QList &filteredFrames); + + void doPlayStep( + bool &master, + double refPts, + QAVQueueClock &clock, + QAVPacketQueue &queue, + bool &sync, + const std::function &cb); + void doPlayStep( + QAVQueueClock &clock, + QAVPacketQueue &queue, + bool &sync, + const std::function &cb); + + void doPlayVideo(); + void doPlayAudio(); + void doPlaySubtitle(); + + template + void dispatch(T fn); + + QAVPlayer *q_ptr = nullptr; + QString url; + QSharedPointer dev; + QAVPlayer::MediaStatus mediaStatus = QAVPlayer::NoMedia; + QList pendingMediaStatuses; + QAVPlayer::State state = QAVPlayer::StoppedState; + mutable QMutex stateMutex; + + bool seekable = false; + qreal speed = 1.0; + mutable QMutex speedMutex; + double videoFrameRate = 0.0; + + double duration = 0; + double pendingPosition = 0; + bool pendingSeek = false; + double currPts = 0.0; + mutable QMutex positionMutex; + bool synced = true; + + QAVPlayer::Error error = QAVPlayer::NoError; + + QAVDemuxer demuxer; + + QThreadPool threadPool; + QFuture loaderFuture; + QFuture demuxerFuture; + + QFuture videoPlayFuture; + QAVPacketQueue videoQueue; + QAVQueueClock videoClock; + + QFuture audioPlayFuture; + QAVPacketQueue audioQueue; + QAVQueueClock audioClock; + + QFuture subtitlePlayFuture; + QAVPacketQueue subtitleQueue; + QAVQueueClock subtitleClock; + + bool quit = 0; + bool isWaiting = false; + mutable QMutex waitMutex; + QWaitCondition waitCond; + bool eof = false; + std::atomic_bool startDemuxing {false}; + + QList filterDescs; + QAVFilters filters; +}; + +static QString err_str(int err) +{ + char errbuf[128]; + const char *errbuf_ptr = errbuf; + if (av_strerror(err, errbuf, sizeof(errbuf)) < 0) + errbuf_ptr = strerror(AVUNERROR(err)); + + return QString::fromUtf8(errbuf_ptr); +} + +QAVPlayer::Error QAVPlayerPrivate::currentError() const +{ + QMutexLocker locker(&stateMutex); + return error; +} + +void QAVPlayerPrivate::setMediaStatus(QAVPlayer::MediaStatus status) +{ + { + QMutexLocker locker(&stateMutex); + if (mediaStatus == status) + return; + + if (status != QAVPlayer::InvalidMedia) + error = QAVPlayer::NoError; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << mediaStatus << "->" << status; + mediaStatus = status; + } + + Q_EMIT q_ptr->mediaStatusChanged(status); +} + +void QAVPlayerPrivate::resetPendingStatuses() +{ + QMutexLocker locker(&stateMutex); + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << pendingMediaStatuses; + pendingMediaStatuses.clear(); + wait(true); +} + +void QAVPlayerPrivate::setPendingMediaStatus(PendingMediaStatus status) +{ + QMutexLocker locker(&stateMutex); + pendingMediaStatuses.push_back(status); + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << mediaStatus << "->" << pendingMediaStatuses; +} + +bool QAVPlayerPrivate::setState(QAVPlayer::State s) +{ + Q_Q(QAVPlayer); + bool result = false; + { + QMutexLocker locker(&stateMutex); + if (state == s) + return result; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << state << "->" << s; + state = s; + result = true; + } + + Q_EMIT q->stateChanged(s); + return result; +} + +void QAVPlayerPrivate::setSeekable(bool s) +{ + Q_Q(QAVPlayer); + if (seekable == s) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << seekable << "->" << s; + seekable = s; + Q_EMIT q->seekableChanged(seekable); +} + +void QAVPlayerPrivate::setDuration(double d) +{ + Q_Q(QAVPlayer); + if (qFuzzyCompare(duration, d)) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << duration << "->" << d; + duration = d; + Q_EMIT q->durationChanged(q->duration()); +} + +bool QAVPlayerPrivate::isSeeking() const +{ + QMutexLocker locker(&positionMutex); + return pendingSeek; +} + +bool QAVPlayerPrivate::isEndOfFile() const +{ + QMutexLocker locker(&stateMutex); + return eof; +} + +void QAVPlayerPrivate::endOfFile(bool v) +{ + QMutexLocker locker(&stateMutex); + eof = v; +} + +void QAVPlayerPrivate::setVideoFrameRate(double v) +{ + Q_Q(QAVPlayer); + if (qFuzzyCompare(videoFrameRate, v)) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << videoFrameRate << "->" << v; + videoFrameRate = v; + Q_EMIT q->videoFrameRateChanged(v); +} + +void QAVPlayerPrivate::setPts(double v) +{ + QMutexLocker locker(&positionMutex); + if (!isnan(v)) + currPts = v; +} + +double QAVPlayerPrivate::pts() const +{ + QMutexLocker locker(&positionMutex); + return currPts; +} + +template +void QAVPlayerPrivate::dispatch(T fn) +{ + QMetaObject::invokeMethod(q_ptr, fn, nullptr); +} + +void QAVPlayerPrivate::setError(QAVPlayer::Error err, const QString &str) +{ + Q_Q(QAVPlayer); + { + QMutexLocker locker(&stateMutex); + error = err; + } + + qWarning() << err << ":" << str; + Q_EMIT q->errorOccurred(err, str); + setMediaStatus(QAVPlayer::InvalidMedia); + setState(QAVPlayer::StoppedState); + resetPendingStatuses(); +} + +void QAVPlayerPrivate::terminate() +{ + qCDebug(lcAVPlayer) << __FUNCTION__; + setState(QAVPlayer::StoppedState); + quit = true; + wait(false); + videoFrameRate = 0.0; + videoQueue.clear(); + videoQueue.abort(); + videoClock.clear(); + audioQueue.clear(); + audioQueue.abort(); + audioClock.clear(); + subtitleQueue.clear(); + subtitleQueue.abort(); + subtitleClock.clear(); + if (dev) + dev->abort(true); + loaderFuture.waitForFinished(); + demuxerFuture.waitForFinished(); + videoPlayFuture.waitForFinished(); + audioPlayFuture.waitForFinished(); + pendingPosition = 0; + pendingSeek = false; + currPts = 0.0; + pendingMediaStatuses.clear(); + filters.clear(); + setDuration(0); + error = QAVPlayer::NoError; + dev.reset(); + eof = false; + startDemuxing = false; +} + +void QAVPlayerPrivate::step(bool hasFrame) +{ + QMutexLocker locker(&stateMutex); + while (!pendingMediaStatuses.isEmpty()) { + auto status = pendingMediaStatuses.first(); + locker.unlock(); + if (!doStep(status, hasFrame)) + break; + locker.relock(); + if (!pendingMediaStatuses.isEmpty()) { + pendingMediaStatuses.removeFirst(); + qCDebug(lcAVPlayer) << "Step done:" << status << ", pending" << pendingMediaStatuses; + } + } + + if (pendingMediaStatuses.isEmpty()) { + videoQueue.wake(false); + audioQueue.wake(false); + subtitleQueue.wake(false); + } else { + wait(false); + } +} + +bool QAVPlayerPrivate::doStep(PendingMediaStatus status, bool hasFrame) +{ + bool result = false; + const bool valid = hasFrame && !isSeeking() && q_ptr->mediaStatus() != QAVPlayer::NoMedia; + switch (status) { + case PlayingMedia: + if (valid) { + result = true; + qCDebug(lcAVPlayer) << "Played from pos:" << q_ptr->position(); + Q_EMIT q_ptr->played(q_ptr->position()); + wait(false); + } + break; + + case PausingMedia: + if (valid) { + result = true; + qCDebug(lcAVPlayer) << "Paused to pos:" << q_ptr->position(); + Q_EMIT q_ptr->paused(q_ptr->position()); + wait(true); + } + break; + + case SeekingMedia: + if (valid) { + result = true; + if (q_ptr->mediaStatus() == QAVPlayer::EndOfMedia) + setMediaStatus(QAVPlayer::LoadedMedia); + qCDebug(lcAVPlayer) << "Seeked to pos:" << q_ptr->position(); + Q_EMIT q_ptr->seeked(q_ptr->position()); + QAVPlayer::State currState = q_ptr->state(); + if (currState == QAVPlayer::PausedState || currState == QAVPlayer::StoppedState) + wait(true); + } + break; + + case StoppingMedia: + if (q_ptr->mediaStatus() != QAVPlayer::NoMedia) { + result = true; + qCDebug(lcAVPlayer) << "Stopped to pos:" << q_ptr->position(); + Q_EMIT q_ptr->stopped(q_ptr->position()); + wait(true); + } + break; + + case SteppingMedia: + result = isEndOfFile(); + if (valid) { + result = true; + qCDebug(lcAVPlayer) << "Stepped to pos:" << q_ptr->position(); + Q_EMIT q_ptr->stepped(q_ptr->position()); + wait(true); + } + break; + + case LoadingMedia: + result = true; + setMediaStatus(QAVPlayer::LoadedMedia); + break; + + case EndOfMedia: + result = true; + setMediaStatus(QAVPlayer::EndOfMedia); + break; + + default: + break; + } + + // The step is finished but queues are empty => no more frames will be sent. + // Need to skip current status and move to next to prevent the blocking. + if (!result + && demuxer.eof() + && videoQueue.isEmpty() + && audioQueue.isEmpty() + && filters.isEmpty() + && !isSeeking()) + { + result = true; + qCDebug(lcAVPlayer) << __FUNCTION__ << ": EndOfMedia -> skipping:" << status; + } + + return result; +} + +void QAVPlayerPrivate::doWait() +{ + QMutexLocker lock(&waitMutex); + if (isWaiting) + waitCond.wait(&waitMutex); +} + +void QAVPlayerPrivate::wait(bool v) +{ + { + QMutexLocker locker(&waitMutex); + if (isWaiting != v) + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << isWaiting << "->" << v; + isWaiting = v; + } + + if (!v) { + startDemuxing = true; + waitCond.wakeAll(); + } + videoQueue.wake(true); + audioQueue.wake(true); + subtitleQueue.wake(true); +} + +void QAVPlayerPrivate::applyFilters() +{ + applyFilters(false, {}); +} + +void QAVPlayerPrivate::applyFilters(bool reset, const QAVFrame &frame) +{ + if ((filterDescs == filters.filterDescs()) && !reset) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << filters.filterDescs() << "->" << filterDescs << "reset:" << reset; + int ret = filters.createFilters(filterDescs, frame, demuxer); + if (ret < 0) { + setError(QAVPlayer::FilterError, QLatin1String("Could not create filters: ") + err_str(ret)); + return; + } + videoQueue.clearFrames(); + audioQueue.clearFrames(); + if (error == QAVPlayer::FilterError) + setMediaStatus(QAVPlayer::LoadedMedia); +} + +void QAVPlayerPrivate::doLoad() +{ + demuxer.abort(false); + demuxer.unload(); + int ret = demuxer.load(url, dev.get()); + if (ret < 0) { + setError(QAVPlayer::ResourceError, err_str(ret)); + return; + } + + if (demuxer.currentVideoStreams().isEmpty() && demuxer.currentAudioStreams().isEmpty()) { + setError(QAVPlayer::ResourceError, QLatin1String("No codecs found")); + return; + } + + applyFilters(true, {}); + dispatch([this] { + qCDebug(lcAVPlayer) << "[" << url << "]: Loaded, seekable:" << demuxer.seekable() << ", duration:" << demuxer.duration(); + setSeekable(demuxer.seekable()); + setDuration(demuxer.duration()); + setVideoFrameRate(demuxer.videoFrameRate()); + step(false); + }); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + demuxerFuture = QtConcurrent::run(&threadPool, this, &QAVPlayerPrivate::doDemux); + if (!q_ptr->availableVideoStreams().isEmpty()) + videoPlayFuture = QtConcurrent::run(&threadPool, this, &QAVPlayerPrivate::doPlayVideo); + if (!q_ptr->availableAudioStreams().isEmpty()) + audioPlayFuture = QtConcurrent::run(&threadPool, this, &QAVPlayerPrivate::doPlayAudio); + if (!q_ptr->availableSubtitleStreams().isEmpty()) + subtitlePlayFuture = QtConcurrent::run(&threadPool, this, &QAVPlayerPrivate::doPlaySubtitle); +#else + demuxerFuture = QtConcurrent::run(&threadPool, &QAVPlayerPrivate::doDemux, this); + if (!q_ptr->availableVideoStreams().isEmpty()) + videoPlayFuture = QtConcurrent::run(&threadPool, &QAVPlayerPrivate::doPlayVideo, this); + if (!q_ptr->availableAudioStreams().isEmpty()) + audioPlayFuture = QtConcurrent::run(&threadPool, &QAVPlayerPrivate::doPlayAudio, this); + if (!q_ptr->availableSubtitleStreams().isEmpty()) + subtitlePlayFuture = QtConcurrent::run(&threadPool, &QAVPlayerPrivate::doPlaySubtitle, this); +#endif + qCDebug(lcAVPlayer) << __FUNCTION__ << "finished"; +} + +void QAVPlayerPrivate::doDemux() +{ + const int maxQueueBytes = 15 * 1024 * 1024; + QMutex waiterMutex; + QWaitCondition waiter; + + while (!quit) { + if (videoQueue.bytes() + audioQueue.bytes() > maxQueueBytes + || (videoQueue.enough() && audioQueue.enough()) + || !startDemuxing) + { + QMutexLocker locker(&waiterMutex); + waiter.wait(&waiterMutex, 10); + continue; + } + + { + QMutexLocker locker(&positionMutex); + if (pendingSeek) { + if (pendingPosition < 0) + pendingPosition += demuxer.duration(); + if (pendingPosition < 0) + pendingPosition = 0; + const double pos = pendingPosition; + locker.unlock(); + qCDebug(lcAVPlayer) << "Seeking to pos:" << pos * 1000; + int ret = demuxer.seek(pos); + if (ret >= 0) { + qCDebug(lcAVPlayer) << "Waiting video thread finished processing packets"; + videoQueue.waitForEmpty(); + videoClock.clear(); + qCDebug(lcAVPlayer) << "Waiting audio thread finished processing packets"; + audioQueue.waitForEmpty(); + audioClock.clear(); + qCDebug(lcAVPlayer) << "Waiting subtitle thread finished processing packets"; + subtitleQueue.waitForEmpty(); + subtitleClock.clear(); + qCDebug(lcAVPlayer) << "Flush codec buffers"; + demuxer.flushCodecBuffers(); + qCDebug(lcAVPlayer) << "Reset filters"; + applyFilters(true, {}); + qCDebug(lcAVPlayer) << "Start reading packets from" << pos * 1000; + } else { + qWarning() << "Could not seek:" << ret << ":" << err_str(ret); + } + locker.relock(); + if (qFuzzyCompare(pendingPosition, pos)) + pendingSeek = false; + } + } + + auto packet = demuxer.read(); + if (packet.stream()) { + endOfFile(false); + // Empty packet points to EOF and it needs to flush codecs + switch (demuxer.currentCodecType(packet.packet()->stream_index)) { + case AVMEDIA_TYPE_VIDEO: + videoQueue.enqueue(packet); + break; + case AVMEDIA_TYPE_AUDIO: + audioQueue.enqueue(packet); + break; + case AVMEDIA_TYPE_SUBTITLE: + subtitleQueue.enqueue(packet); + break; + default: + break; + } + } else { + if (demuxer.eof() + && videoQueue.isEmpty() + && audioQueue.isEmpty() + && subtitleQueue.isEmpty() + && filters.isEmpty() + && !isEndOfFile()) + { + filters.flush(); + endOfFile(true); + qCDebug(lcAVPlayer) << "EndOfMedia"; + setPendingMediaStatus(EndOfMedia); + q_ptr->stop(); + wait(false); + } + + QMutexLocker locker(&waiterMutex); + waiter.wait(&waiterMutex, 10); + } + } + qCDebug(lcAVPlayer) << __FUNCTION__ << "finished"; +} + +static double streamDuration(const QAVStreamFrame &frame, const QAVDemuxer &demuxer) +{ + double duration = demuxer.duration(); + const double stream_duration = frame.stream().duration(); + if (stream_duration > 0 && stream_duration < duration) + duration = stream_duration; + return duration; +} + +static bool isLastFrame(const QAVStreamFrame &frame, const QAVDemuxer &demuxer) +{ + bool result = false; + if (!isnan(frame.duration()) && frame.duration() > 0) { + const double requestedPos = streamDuration(frame, demuxer); + const int frameNumber = frame.pts() / frame.duration(); + const int requestedFrameNumber = requestedPos / frame.duration(); + result = frameNumber + 1 >= requestedFrameNumber; + } + return result; +} + +bool QAVPlayerPrivate::skipFrame( + bool master, + const QAVStreamFrame &frame, + bool isEmpty) +{ + QMutexLocker locker(&positionMutex); + bool result = pendingSeek; + if (!pendingSeek && pendingPosition > 0) { + const bool isQueueEOF = demuxer.eof() && isEmpty; + // Assume that no frames will be sent after this duration + const double duration = streamDuration(frame, demuxer); + const double requestedPos = qMin(pendingPosition, duration); + double pos = frame.pts(); + // Show last frame if seeked to duration + bool lastFrame = false; + if (pendingPosition >= duration) { + pos += frame.duration(); + // Additional check if frame rate has been changed, + // thus last frame could be far away from duration by pts, + // but frame number points to the latest frame. + lastFrame = isLastFrame(frame, demuxer); + } + result = pos < requestedPos && !isQueueEOF && !lastFrame; + if (master) { + if (result) + qCDebug(lcAVPlayer) << __FUNCTION__ << pos << "<" << requestedPos; + else + pendingPosition = 0; + } + } + + return result; +} + +void QAVPlayerPrivate::doPlayStep( + bool &master, + double refPts, + QAVQueueClock &clock, + QAVPacketQueue &queue, + bool &sync, + const std::function &cb) +{ + doWait(); + + // 1. Decode a frame + QAVFrame decodedFrame; + queue.frontFrame(decodedFrame); + bool flushEvents = false; + int ret = 0; + + // Determine if current thread is handling events and pts + if (decodedFrame) + master = demuxer.isMasterStream(decodedFrame.stream()); + + // 2. Filter decoded frame + QList filteredFrames; + if (decodedFrame) + ret = filters.write(queue.mediaType(), decodedFrame); + if (ret >= 0 || ret == AVERROR(EAGAIN)) + ret = filters.read(queue.mediaType(), decodedFrame, filteredFrames); + if (ret < 0 && ret != AVERROR(EAGAIN)) { + // Try filters again + filteredFrames.clear(); + if (ret != AVERROR(ENOTSUP)) { + setError(QAVPlayer::FilterError, err_str(ret)); + return; + } + applyFilters(true, decodedFrame); + } else { + // The frame is already filtered, decode next one + queue.popFrame(); + } + + // 3. Sync filtered frames + while (!quit && !filteredFrames.isEmpty()) { + auto &frame = filteredFrames.front(); + Q_ASSERT(frame); + if (clock.wait( + synced ? sync : synced, + frame.pts(), + q_ptr->speed(), + refPts)) + { + sync = !skipFrame(master, frame, queue.isEmpty()); + if (sync) { + if (master) + setPts(frame.pts()); + if (!flushEvents) + flushEvents = true; + cb(frame); + demuxer.onFrameSent(frame); + } + filteredFrames.pop_front(); + } else { + flushEvents = isLastFrame(frame, demuxer); + } + } + + if (master) + step(flushEvents); +} + +void QAVPlayerPrivate::doPlayVideo() +{ + videoClock.setFrameRate(demuxer.videoFrameRate()); + bool master = true; + bool sync = true; + + while (!quit) { + doPlayStep( + master, + !demuxer.currentAudioStreams().isEmpty() ? audioClock.pts() : -1, + videoClock, + videoQueue, + sync, + [&](const QAVFrame &frame) { Q_EMIT q_ptr->videoFrame(frame); } + ); + } + + videoQueue.clear(); + videoClock.clear(); + setMediaStatus(QAVPlayer::NoMedia); + qCDebug(lcAVPlayer) << __FUNCTION__ << "finished"; +} + +void QAVPlayerPrivate::doPlayAudio() +{ + bool master = false; + const double ref = -1; + bool sync = true; + + while (!quit) { + doPlayStep( + master, + ref, + audioClock, + audioQueue, + sync, + [this](const QAVFrame &frame) { + frame.frame()->sample_rate *= q_ptr->speed(); + Q_EMIT q_ptr->audioFrame(frame); + } + ); + } + + audioQueue.clear(); + audioClock.clear(); + if (master) + setMediaStatus(QAVPlayer::NoMedia); + qCDebug(lcAVPlayer) << __FUNCTION__ << "finished"; +} + +void QAVPlayerPrivate::doPlayStep( + QAVQueueClock &clock, + QAVPacketQueue &queue, + bool &sync, + const std::function &cb) +{ + doWait(); + + // 1. Decode a frame + QAVSubtitleFrame decodedFrame; + if (!queue.frontFrame(decodedFrame)) + return; + + // 2. Sync decoded frame + if (clock.wait( + synced ? sync : synced, + decodedFrame.pts(), + q_ptr->speed(), + -1)) + { + sync = !skipFrame(false, decodedFrame, queue.isEmpty()); + if (sync && decodedFrame) { + cb(decodedFrame); + demuxer.onFrameSent(decodedFrame); + } + queue.popFrame(); + } +} + +void QAVPlayerPrivate::doPlaySubtitle() +{ + bool sync = true; + while (!quit) { + doPlayStep( + subtitleClock, + subtitleQueue, + sync, + [this](const QAVSubtitleFrame &frame) { Q_EMIT q_ptr->subtitleFrame(frame); } + ); + } + + subtitleQueue.clear(); + subtitleClock.clear(); + qCDebug(lcAVPlayer) << __FUNCTION__ << "finished"; +} + +QAVPlayer::QAVPlayer(QObject *parent) + : QObject(parent) + , d_ptr(new QAVPlayerPrivate(this)) +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); +} + +QAVPlayer::~QAVPlayer() +{ + Q_D(QAVPlayer); + d->terminate(); +} + +void QAVPlayer::setSource(const QString &url, const QSharedPointer &dev) +{ + Q_D(QAVPlayer); + if (d->url == url) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << url; + + d->terminate(); + d->url = url; + d->dev = dev; + Q_EMIT sourceChanged(url); + d->wait(true); + d->quit = false; + if (url.isEmpty()) + return; + + d->setPendingMediaStatus(LoadingMedia); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + d->loaderFuture = QtConcurrent::run(&d->threadPool, d, &QAVPlayerPrivate::doLoad); +#else + d->loaderFuture = QtConcurrent::run(&d->threadPool, &QAVPlayerPrivate::doLoad, d); +#endif +} + +QString QAVPlayer::source() const +{ + return d_func()->url; +} + +QList QAVPlayer::availableVideoStreams() const +{ + Q_D(const QAVPlayer); + return d->demuxer.availableVideoStreams(); +} + +QList QAVPlayer::currentVideoStreams() const +{ + Q_D(const QAVPlayer); + return d->demuxer.currentVideoStreams(); +} + +void QAVPlayer::setVideoStream(const QAVStream &stream) +{ + Q_D(QAVPlayer); + if (d->demuxer.currentVideoStreams() == QList({stream})) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->demuxer.currentVideoStreams() << "->" << stream.index(); + if (d->demuxer.setVideoStreams({stream})) + Q_EMIT videoStreamsChanged(d->demuxer.currentVideoStreams()); +} + +void QAVPlayer::setVideoStreams(const QList &streams) +{ + Q_D(QAVPlayer); + if (d->demuxer.currentVideoStreams() == streams) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->demuxer.currentVideoStreams() << "->" << streams; + if (d->demuxer.setVideoStreams(streams)) + Q_EMIT videoStreamsChanged(d->demuxer.currentVideoStreams()); +} + +QList QAVPlayer::availableAudioStreams() const +{ + Q_D(const QAVPlayer); + return d->demuxer.availableAudioStreams(); +} + +QList QAVPlayer::currentAudioStreams() const +{ + Q_D(const QAVPlayer); + return d->demuxer.currentAudioStreams(); +} + +void QAVPlayer::setAudioStream(const QAVStream &stream) +{ + Q_D(QAVPlayer); + if (d->demuxer.currentAudioStreams() == QList({stream})) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->demuxer.currentAudioStreams() << "->" << stream.index(); + if (d->demuxer.setAudioStreams({stream})) + Q_EMIT audioStreamsChanged(d->demuxer.currentAudioStreams()); +} + +void QAVPlayer::setAudioStreams(const QList &streams) +{ + Q_D(QAVPlayer); + if (d->demuxer.currentAudioStreams() == streams) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->demuxer.currentAudioStreams() << "->" << streams; + if (d->demuxer.setAudioStreams(streams)) + Q_EMIT audioStreamsChanged(d->demuxer.currentAudioStreams()); +} + +QList QAVPlayer::availableSubtitleStreams() const +{ + Q_D(const QAVPlayer); + return d->demuxer.availableSubtitleStreams(); +} + +QList QAVPlayer::currentSubtitleStreams() const +{ + Q_D(const QAVPlayer); + return d->demuxer.currentSubtitleStreams(); +} + +void QAVPlayer::setSubtitleStream(const QAVStream &stream) +{ + Q_D(QAVPlayer); + if (d->demuxer.currentSubtitleStreams() == QList({stream})) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->demuxer.currentSubtitleStreams() << "->" << stream.index(); + if (d->demuxer.setSubtitleStreams({stream})) + Q_EMIT subtitleStreamsChanged(d->demuxer.currentSubtitleStreams()); +} + +void QAVPlayer::setSubtitleStreams(const QList &streams) +{ + Q_D(QAVPlayer); + if (d->demuxer.currentSubtitleStreams() == streams) + return; + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->demuxer.currentSubtitleStreams() << "->" << streams; + if (d->demuxer.setSubtitleStreams(streams)) + Q_EMIT subtitleStreamsChanged(d->demuxer.currentSubtitleStreams()); +} + +QAVPlayer::State QAVPlayer::state() const +{ + Q_D(const QAVPlayer); + QMutexLocker locker(&d->stateMutex); + return d->state; +} + +QAVPlayer::MediaStatus QAVPlayer::mediaStatus() const +{ + Q_D(const QAVPlayer); + QMutexLocker locker(&d->stateMutex); + return d->mediaStatus; +} + +void QAVPlayer::play() +{ + Q_D(QAVPlayer); + if (d->url.isEmpty() || d->currentError() == QAVPlayer::ResourceError) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__; + if (d->setState(QAVPlayer::PlayingState)) { + if (d->isEndOfFile()) { + qCDebug(lcAVPlayer) << "Playing from beginning"; + seek(0); + } + d->setPendingMediaStatus(PlayingMedia); + } + d->wait(false); + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +void QAVPlayer::pause() +{ + Q_D(QAVPlayer); + if (d->currentError() == QAVPlayer::ResourceError) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__; + if (d->setState(QAVPlayer::PausedState)) { + if (d->isEndOfFile()) { + qCDebug(lcAVPlayer) << "Pausing from beginning"; + seek(0); + } + d->setPendingMediaStatus(PausingMedia); + d->wait(false); + } else { + d->wait(true); + } + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +void QAVPlayer::stop() +{ + Q_D(QAVPlayer); + if (d->currentError() == QAVPlayer::ResourceError) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__; + if (d->setState(QAVPlayer::StoppedState)) { + d->setPendingMediaStatus(StoppingMedia); + d->wait(false); + } else { + d->wait(true); + } + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +void QAVPlayer::stepForward() +{ + Q_D(QAVPlayer); + if (d->currentError() == QAVPlayer::ResourceError) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__; + d->setState(QAVPlayer::PausedState); + if (d->isEndOfFile()) { + qCDebug(lcAVPlayer) << "Stepping from beginning"; + seek(0); + } + d->setPendingMediaStatus(SteppingMedia); + d->wait(false); + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +void QAVPlayer::stepBackward() +{ + Q_D(QAVPlayer); + if (d->currentError() == QAVPlayer::ResourceError) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__; + d->setState(QAVPlayer::PausedState); + const qint64 pos = d->pts() > 0 ? (d->pts() - videoFrameRate()) * 1000 : duration(); + seek(pos); + d->setPendingMediaStatus(SteppingMedia); + d->wait(false); + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +bool QAVPlayer::isSeekable() const +{ + return d_func()->seekable; +} + +void QAVPlayer::seek(qint64 pos) +{ + Q_D(QAVPlayer); + if ((duration() > 0 && pos > duration()) || d->currentError() == QAVPlayer::ResourceError) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << "pos:" << pos; + { + QMutexLocker locker(&d->positionMutex); + d->pendingSeek = true; + d->pendingPosition = pos / 1000.0; + } + + d->setPendingMediaStatus(SeekingMedia); + d->wait(false); + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +qint64 QAVPlayer::duration() const +{ + return d_func()->duration * 1000; +} + +qint64 QAVPlayer::position() const +{ + Q_D(const QAVPlayer); + + { + QMutexLocker locker(&d->positionMutex); + if (d->pendingSeek) + return d->pendingPosition * 1000 + (d->pendingPosition < 0 ? duration() : 0); + } + + if (mediaStatus() == QAVPlayer::EndOfMedia) + return duration(); + + return d->pts() * 1000; +} + +void QAVPlayer::setSpeed(qreal r) +{ + Q_D(QAVPlayer); + + { + QMutexLocker locker(&d->speedMutex); + if (qFuzzyCompare(d->speed, r)) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->speed << "->" << r; + d->speed = r; + } + Q_EMIT speedChanged(r); +} + +qreal QAVPlayer::speed() const +{ + Q_D(const QAVPlayer); + + QMutexLocker locker(&d->speedMutex); + return d->speed; +} + +double QAVPlayer::videoFrameRate() const +{ + return d_func()->videoFrameRate; +} + +void QAVPlayer::setFilter(const QString &desc) +{ + Q_D(QAVPlayer); + { + QMutexLocker locker(&d->stateMutex); + if (d->filterDescs.size() == 1 && d->filterDescs.front() == desc) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->filterDescs << "->" << desc; + if (desc.isEmpty()) + d->filterDescs.clear(); + else + d->filterDescs = {desc}; + } + + Q_EMIT filtersChanged({desc}); + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +void QAVPlayer::setFilters(const QList &filters) +{ + Q_D(QAVPlayer); + { + QMutexLocker locker(&d->stateMutex); + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << d->filterDescs << "->" << filters; + d->filterDescs = filters; + } + + Q_EMIT filtersChanged(filters); + if (mediaStatus() != QAVPlayer::NoMedia) + d->applyFilters(); +} + +QList QAVPlayer::filters() const +{ + Q_D(const QAVPlayer); + QMutexLocker locker(&d->stateMutex); + return d->filterDescs; +} + +void QAVPlayer::setBitstreamFilter(const QString &desc) +{ + Q_D(QAVPlayer); + QString bsf = d->demuxer.bitstreamFilter(); + if (bsf == desc) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << bsf << "->" << desc; + int ret = d->demuxer.applyBitstreamFilter(desc); + Q_EMIT bitstreamFilterChanged(desc); + if (ret < 0) + d->setError(QAVPlayer::FilterError, QLatin1String("Could not parse bitstream filter desc: ") + err_str(ret)); +} + +QString QAVPlayer::bitstreamFilter() const +{ + Q_D(const QAVPlayer); + return d->demuxer.bitstreamFilter(); +} + +bool QAVPlayer::isSynced() const +{ + Q_D(const QAVPlayer); + return d->synced; +} + +void QAVPlayer::setSynced(bool sync) +{ + Q_D(QAVPlayer); + if (d->synced == sync) + return; + + d->synced = sync; + Q_EMIT syncedChanged(sync); +} + +QString QAVPlayer::inputFormat() const +{ + Q_D(const QAVPlayer); + return d->demuxer.inputFormat(); +} + +void QAVPlayer::setInputFormat(const QString &format) +{ + Q_D(QAVPlayer); + QString current = inputFormat(); + if (format == current) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << current << "->" << format; + d->demuxer.setInputFormat(format); + Q_EMIT inputFormatChanged(format); +} + +QString QAVPlayer::inputVideoCodec() const +{ + Q_D(const QAVPlayer); + return d->demuxer.inputVideoCodec(); +} + +void QAVPlayer::setInputVideoCodec(const QString &codec) +{ + Q_D(QAVPlayer); + QString current = inputVideoCodec(); + if (codec == current) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << current << "->" << codec; + d->demuxer.setInputVideoCodec(codec); + Q_EMIT inputVideoCodecChanged(codec); +} + +QStringList QAVPlayer::supportedVideoCodecs() +{ + return QAVDemuxer::supportedVideoCodecs(); +} + +QMap QAVPlayer::inputOptions() const +{ + Q_D(const QAVPlayer); + return d->demuxer.inputOptions(); +} + +void QAVPlayer::setInputOptions(const QMap &opts) +{ + Q_D(QAVPlayer); + auto current = inputOptions(); + if (opts == current) + return; + + qCDebug(lcAVPlayer) << __FUNCTION__ << ":" << current << "->" << opts; + d->demuxer.setInputOptions(opts); + Q_EMIT inputOptionsChanged(opts); +} + +QAVStream::Progress QAVPlayer::progress(const QAVStream &s) const +{ + return d_func()->demuxer.progress(s); +} + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug dbg, QAVPlayer::State state) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + switch (state) { + case QAVPlayer::StoppedState: + return dbg << "StoppedState"; + case QAVPlayer::PlayingState: + return dbg << "PlayingState"; + case QAVPlayer::PausedState: + return dbg << "PausedState"; + default: + return dbg << QString(QLatin1String("UserType(%1)" )).arg(int(state)).toLatin1().constData(); + } +} + +QDebug operator<<(QDebug dbg, QAVPlayer::MediaStatus status) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + switch (status) { + case QAVPlayer::NoMedia: + return dbg << "NoMedia"; + case QAVPlayer::LoadedMedia: + return dbg << "LoadedMedia"; + case QAVPlayer::EndOfMedia: + return dbg << "EndOfMedia"; + case QAVPlayer::InvalidMedia: + return dbg << "InvalidMedia"; + default: + return dbg << QString(QLatin1String("UserType(%1)" )).arg(int(status)).toLatin1().constData(); + } +} + +QDebug operator<<(QDebug dbg, PendingMediaStatus status) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + switch (status) { + case LoadingMedia: + return dbg << "LoadingMedia"; + case PlayingMedia: + return dbg << "PlayingMedia"; + case PausingMedia: + return dbg << "PausingMedia"; + case StoppingMedia: + return dbg << "StoppingMedia"; + case SteppingMedia: + return dbg << "SteppingMedia"; + case SeekingMedia: + return dbg << "SeekingMedia"; + case EndOfMedia: + return dbg << "EndOfMedia"; + default: + return dbg << QString(QLatin1String("UserType(%1)" )).arg(int(status)).toLatin1().constData(); + } +} + +QDebug operator<<(QDebug dbg, QAVPlayer::Error err) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + switch (err) { + case QAVPlayer::NoError: + return dbg << "NoError"; + case QAVPlayer::ResourceError: + return dbg << "ResourceError"; + case QAVPlayer::FilterError: + return dbg << "FilterError"; + default: + return dbg << QString(QLatin1String("UserType(%1)" )).arg(int(err)).toLatin1().constData(); + } +} +#endif + +Q_DECLARE_METATYPE(PendingMediaStatus) + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavplayer.h b/QtAVPlayer/src/QtAVPlayer/qavplayer.h new file mode 100644 index 0000000..fec9fed --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavplayer.h @@ -0,0 +1,162 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVPLAYER_H +#define QAVPLAYER_H + +#include +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVIODevice; +class QAVPlayerPrivate; +class QAVPlayer : public QObject +{ + Q_OBJECT + Q_ENUMS(State) + Q_ENUMS(MediaStatus) + Q_ENUMS(Error) + +public: + enum State + { + StoppedState, + PlayingState, + PausedState + }; + + enum MediaStatus + { + NoMedia, + LoadedMedia, + EndOfMedia, + InvalidMedia + }; + + enum Error + { + NoError, + ResourceError, + FilterError + }; + + QAVPlayer(QObject *parent = nullptr); + ~QAVPlayer(); + + void setSource(const QString &url, const QSharedPointer &dev = {}); + QString source() const; + + QList availableVideoStreams() const; + QList currentVideoStreams() const; + void setVideoStream(const QAVStream &stream); + void setVideoStreams(const QList &streams); + + QList availableAudioStreams() const; + QList currentAudioStreams() const; + void setAudioStream(const QAVStream &stream); + void setAudioStreams(const QList &streams); + + QList availableSubtitleStreams() const; + QList currentSubtitleStreams() const; + void setSubtitleStream(const QAVStream &stream); + void setSubtitleStreams(const QList &streams); + + State state() const; + MediaStatus mediaStatus() const; + qint64 duration() const; + qint64 position() const; + qreal speed() const; + double videoFrameRate() const; + + void setFilter(const QString &desc); + void setFilters(const QList &filters); + QList filters() const; + + void setBitstreamFilter(const QString &desc); + QString bitstreamFilter() const; + + bool isSeekable() const; + + bool isSynced() const; + void setSynced(bool sync); + + QString inputFormat() const; + void setInputFormat(const QString &format); + + QString inputVideoCodec() const; + void setInputVideoCodec(const QString &codec); + static QStringList supportedVideoCodecs(); + + QMap inputOptions() const; + void setInputOptions(const QMap &opts); + + QAVStream::Progress progress(const QAVStream &stream) const; + +public Q_SLOTS: + void play(); + void pause(); + void stop(); + void seek(qint64 position); + void setSpeed(qreal rate); + void stepForward(); + void stepBackward(); + +Q_SIGNALS: + void sourceChanged(const QString &url); + void stateChanged(QAVPlayer::State newState); + void mediaStatusChanged(QAVPlayer::MediaStatus status); + void errorOccurred(QAVPlayer::Error, const QString &str); + void durationChanged(qint64 duration); + void seekableChanged(bool seekable); + void speedChanged(qreal rate); + void videoFrameRateChanged(double rate); + void videoStreamsChanged(const QList &streams); + void audioStreamsChanged(const QList &streams); + void subtitleStreamsChanged(const QList &streams); + void played(qint64 pos); + void paused(qint64 pos); + void stopped(qint64 pos); + void stepped(qint64 pos); + void seeked(qint64 pos); + void filtersChanged(const QList &filters); + void bitstreamFilterChanged(const QString &desc); + void syncedChanged(bool sync); + void inputFormatChanged(const QString &format); + void inputVideoCodecChanged(const QString &codec); + void inputOptionsChanged(const QMap &opts); + + void videoFrame(const QAVVideoFrame &frame); + void audioFrame(const QAVAudioFrame &frame); + void subtitleFrame(const QAVSubtitleFrame &frame); + +protected: + std::unique_ptr d_ptr; + +private: + Q_DISABLE_COPY(QAVPlayer) + Q_DECLARE_PRIVATE(QAVPlayer) +}; + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug, QAVPlayer::State); +QDebug operator<<(QDebug, QAVPlayer::MediaStatus); +QDebug operator<<(QDebug, QAVPlayer::Error); +#endif + +Q_DECLARE_METATYPE(QAVPlayer::State) +Q_DECLARE_METATYPE(QAVPlayer::MediaStatus) +Q_DECLARE_METATYPE(QAVPlayer::Error) + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavstream.cpp b/QtAVPlayer/src/QtAVPlayer/qavstream.cpp new file mode 100644 index 0000000..9398803 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavstream.cpp @@ -0,0 +1,285 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavstream.h" +#include "qavdemuxer_p.h" +#include "qavcodec_p.h" +#include + +extern "C" { +#include +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVStreamPrivate +{ + Q_DECLARE_PUBLIC(QAVStream) +public: + QAVStreamPrivate(QAVStream *q) : q_ptr(q) { } + + QAVStream *q_ptr = nullptr; + int index = -1; + AVFormatContext *ctx = nullptr; + QSharedPointer codec; + QMap metadata; +}; + +QAVStream::QAVStream() + : d_ptr(new QAVStreamPrivate(this)) +{ +} + +QAVStream::QAVStream(int index, AVFormatContext *ctx, const QSharedPointer &codec) + : QAVStream() +{ + d_ptr->index = index; + d_ptr->ctx = ctx; + d_ptr->codec = codec; +} + +QAVStream::~QAVStream() +{ +} + +QAVStream::QAVStream(const QAVStream &other) + : QAVStream() +{ + *this = other; +} + +QAVStream &QAVStream::operator=(const QAVStream &other) +{ + d_ptr->index = other.d_ptr->index; + d_ptr->ctx = other.d_ptr->ctx; + d_ptr->codec = other.d_ptr->codec; + return *this; +} + +QAVStream::operator bool() const +{ + Q_D(const QAVStream); + return d->ctx != nullptr && d->codec && d->index >= 0; +} + +AVStream *QAVStream::stream() const +{ + Q_D(const QAVStream); + return d->index >= 0 && d->index < static_cast(d->ctx->nb_streams) ? d->ctx->streams[d->index] : nullptr; +} + +int QAVStream::index() const +{ + return d_func()->index; +} + +static int streamRotation(const AVStream *stream) +{ +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 29, 100) + auto ptr = av_packet_side_data_get(stream->codecpar->coded_side_data, + stream->codecpar->nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX); + auto sideData = ptr ? ptr->data : nullptr; +#elif LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(55, 18, 0) + auto sideData = av_stream_get_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, nullptr); +#else + auto cb = [](const auto &data) { return data.type == AV_PKT_DATA_DISPLAYMATRIX; }; + auto end = stream->side_data + stream->nb_side_data; + auto ptr = std::find_if(stream->side_data, end, cb); + auto sideData = ptr != end ? ptr->data : nullptr; +#endif + if (!sideData) + return 0; + auto rotation = static_cast(std::round(av_display_rotation_get(reinterpret_cast(sideData)))); + if (rotation % 90 != 0) + return 0; + return rotation > 0 ? -rotation % 360 + 360 : -rotation % 360; +} + +static QMap streamMetadata(const AVStream *stream) +{ + QMap metadata; + AVDictionaryEntry *tag = nullptr; + while ((tag = av_dict_get(stream->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) + metadata[QString::fromUtf8(tag->key)] = QString::fromUtf8(tag->value); + if (!metadata.contains(QString::fromLatin1("rotate"))) + metadata[QString::fromLatin1("rotate")] = QString::number(streamRotation(stream)); + return metadata; +} + +QMap QAVStream::metadata() const +{ + Q_D(const QAVStream); + if (!d->metadata.isEmpty()) + return d->metadata; + auto s = stream(); + if (!s) + return {}; + const_cast(d)->metadata = streamMetadata(s); + return d->metadata; +} + +QSharedPointer QAVStream::codec() const +{ + Q_D(const QAVStream); + return d->codec; +} + +double QAVStream::duration() const +{ + Q_D(const QAVStream); + auto s = stream(); + if (!s) + return 0.0; + + double ret = 0.0; + if (s->duration != AV_NOPTS_VALUE) + ret = s->duration * av_q2d(s->time_base); + if (!ret && d->ctx->duration != AV_NOPTS_VALUE) + ret = d->ctx->duration / AV_TIME_BASE; + return ret; +} + +int64_t QAVStream::framesCount() const +{ + auto s = stream(); + if (s == nullptr) + return 0; + + auto frames = s->nb_frames; + if (frames) + return frames; + + auto dur = duration(); + // If frame count is not known, estimating it + if (s->avg_frame_rate.num && s->avg_frame_rate.den && dur) + return dur * av_q2d(s->avg_frame_rate); + + const auto tb = s->time_base; + if ((tb.num == 1 && tb.den >= 24 && tb.den <= 60) || + (tb.num == 1001 && tb.den >= 24000 && tb.den <= 60000)) + { + return s->duration; + } + + return 0; +} + +double QAVStream::frameRate() const +{ + Q_D(const QAVStream); + auto s = stream(); + if (s == nullptr) + return 0.0; + AVRational fr = av_guess_frame_rate(d->ctx, s, nullptr); + return fr.num && fr.den ? av_q2d({fr.den, fr.num}) : 0.0; +} + +QAVStream::Progress::Progress(double duration, qint64 frames, double fr) + : m_duration(duration) + , m_expectedFramesCount(frames) + , m_expectedFrameRate(fr) +{ +} + +QAVStream::Progress::Progress(const Progress &other) +{ + *this = other; +} + +QAVStream::Progress &QAVStream::Progress::operator=(const Progress &other) +{ + m_pts = other.m_pts; + m_duration = other.m_duration; + m_framesCount = other.m_framesCount; + m_expectedFramesCount = other.m_expectedFramesCount; + m_expectedFrameRate = other.m_expectedFrameRate; + m_time = other.m_time; + m_diffs = other.m_diffs; + return *this; +} + +double QAVStream::Progress::pts() const +{ + return m_pts; +} + +double QAVStream::Progress::duration() const +{ + return m_duration; +} + +qint64 QAVStream::Progress::framesCount() const +{ + return m_framesCount; +} + +qint64 QAVStream::Progress::expectedFramesCount() const +{ + return m_expectedFramesCount; +} + +double QAVStream::Progress::expectedFrameRate() const +{ + return m_expectedFrameRate; +} + +double QAVStream::Progress::frameRate() const +{ + return m_framesCount ? m_diffs / 1000000.0 / static_cast(m_framesCount) : 0.0; +} + +unsigned QAVStream::Progress::fps() const +{ + double fr = frameRate(); + return fr ? static_cast(1 / fr) : 0; +} + +void QAVStream::Progress::onFrameSent(double pts) +{ + m_pts = pts; + qint64 cur = av_gettime_relative(); + if (m_framesCount++ > 0) { + qint64 diff = cur - m_time; + if (diff > 0) + m_diffs += diff; + } + m_time = cur; +} + +bool operator==(const QAVStream &lhs, const QAVStream &rhs) +{ + return lhs.index() == rhs.index(); +} + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug dbg, const QAVStream &stream) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + return dbg << QString(QLatin1String("QAVStream(%1)" )).arg(stream.index()).toLatin1().constData(); +} + +QDebug operator<<(QDebug dbg, const QAVStream::Progress &p) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + return dbg << QString(QLatin1String("Progress(%1/%2 pts, %3/%4 frames, %5/%6 frame rate, %7 fps)")) + .arg(p.pts()) + .arg(p.duration()) + .arg(p.framesCount()) + .arg(p.expectedFramesCount()) + .arg(p.frameRate()) + .arg(p.expectedFrameRate()) + .arg(p.fps()).toLatin1().constData(); +} +#endif + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavstream.h b/QtAVPlayer/src/QtAVPlayer/qavstream.h new file mode 100644 index 0000000..eba7245 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavstream.h @@ -0,0 +1,83 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVSTREAM_H +#define QAVSTREAM_H + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +struct AVStream; +struct AVFormatContext; +class QAVCodec; +class QAVStreamPrivate; +class QAVStream +{ +public: + QAVStream(); + QAVStream(int index, AVFormatContext *ctx = nullptr, const QSharedPointer &codec = {}); + QAVStream(const QAVStream &other); + ~QAVStream(); + QAVStream &operator=(const QAVStream &other); + operator bool() const; + + int index() const; + AVStream *stream() const; + double duration() const; + int64_t framesCount() const; + double frameRate() const; + QMap metadata() const; + + QSharedPointer codec() const; + + class Progress + { + public: + Progress(double duration = 0.0, qint64 frames = 0, double fr = 0.0); + Progress(const Progress &other); + Progress &operator=(const Progress &other); + + double pts() const; + double duration() const; + qint64 framesCount() const; + qint64 expectedFramesCount() const; + double frameRate() const; + double expectedFrameRate() const; + unsigned fps() const; + + void onFrameSent(double pts); + private: + double m_pts = 0.0; + double m_duration = 0.0; + qint64 m_framesCount = 0; + qint64 m_expectedFramesCount = 0; + double m_expectedFrameRate = 0.0; + qint64 m_time = 0; + qint64 m_diffs = 0; + }; + +private: + std::unique_ptr d_ptr; + Q_DECLARE_PRIVATE(QAVStream) +}; + +bool operator==(const QAVStream &lhs, const QAVStream &rhs); + +Q_DECLARE_METATYPE(QAVStream) + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug, const QAVStream &); +QDebug operator<<(QDebug, const QAVStream::Progress &); +#endif + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavstreamframe.cpp b/QtAVPlayer/src/QtAVPlayer/qavstreamframe.cpp new file mode 100644 index 0000000..b7a6973 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavstreamframe.cpp @@ -0,0 +1,81 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavstreamframe.h" +#include "qavstreamframe_p.h" +#include "qavframe_p.h" +#include "qavcodec_p.h" +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +QAVStreamFrame::QAVStreamFrame() + : QAVStreamFrame(*new QAVStreamFramePrivate) +{ +} + +QAVStreamFrame::QAVStreamFrame(const QAVStreamFrame &other) + : QAVStreamFrame() +{ + *this = other; +} + +QAVStreamFrame::QAVStreamFrame(QAVStreamFramePrivate &d) + : d_ptr(&d) +{ +} + +QAVStreamFrame::~QAVStreamFrame() +{ +} + +QAVStream QAVStreamFrame::stream() const +{ + return d_ptr->stream; +} + +void QAVStreamFrame::setStream(const QAVStream &stream) +{ + Q_D(QAVStreamFrame); + d->stream = stream; +} + +QAVStreamFrame &QAVStreamFrame::operator=(const QAVStreamFrame &other) +{ + d_ptr->stream = other.d_ptr->stream; + return *this; +} + +QAVStreamFrame::operator bool() const +{ + Q_D(const QAVStreamFrame); + return d->stream; +} + +double QAVStreamFrame::pts() const +{ + Q_D(const QAVStreamFrame); + return d->pts(); +} + +double QAVStreamFrame::duration() const +{ + Q_D(const QAVStreamFrame); + return d->duration(); +} + +int QAVStreamFrame::receive() +{ + Q_D(QAVStreamFrame); + return d->stream ? d->stream.codec()->read(*this) : 0; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavstreamframe.h b/QtAVPlayer/src/QtAVPlayer/qavstreamframe.h new file mode 100644 index 0000000..c19c905 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavstreamframe.h @@ -0,0 +1,45 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVSTREAMFRAME_H +#define QAVSTREAMFRAME_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAVStreamFramePrivate; +class QAVStreamFrame +{ +public: + QAVStreamFrame(); + QAVStreamFrame(const QAVStreamFrame &other); + ~QAVStreamFrame(); + QAVStreamFrame &operator=(const QAVStreamFrame &other); + + QAVStream stream() const; + void setStream(const QAVStream &stream); + operator bool() const; + + double pts() const; + double duration() const; + + // Receives a data from the codec from the stream + int receive(); + +protected: + QAVStreamFrame(QAVStreamFramePrivate &d); + + std::unique_ptr d_ptr; + Q_DECLARE_PRIVATE(QAVStreamFrame) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavstreamframe_p.h b/QtAVPlayer/src/QtAVPlayer/qavstreamframe_p.h new file mode 100644 index 0000000..84d6ed7 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavstreamframe_p.h @@ -0,0 +1,41 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVSTREAMFRAME_P_H +#define QAVSTREAMFRAME_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavstream.h" +#include + +QT_BEGIN_NAMESPACE + +class QAVStreamFramePrivate +{ +public: + QAVStreamFramePrivate() = default; + virtual ~QAVStreamFramePrivate() = default; + + virtual double pts() const { return NAN; } + virtual double duration() const { return 0.0; } + + QAVStream stream; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec.cpp b/QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec.cpp new file mode 100644 index 0000000..0b330f8 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec.cpp @@ -0,0 +1,60 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavsubtitlecodec_p.h" +#include "qavcodec_p_p.h" +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +class QAVSubtitleCodecPrivate : public QAVCodecPrivate +{ + Q_DECLARE_PUBLIC(QAVSubtitleCodec) +public: + QAVSubtitleCodecPrivate(QAVSubtitleCodec *q) : q_ptr(q) { } + + QAVSubtitleCodec *q_ptr = nullptr; + QAVSubtitleFrame frame; + int gotOutput = 0; +}; + +QAVSubtitleCodec::QAVSubtitleCodec() + : QAVCodec(*new QAVSubtitleCodecPrivate(this)) +{ +} + +int QAVSubtitleCodec::write(const QAVPacket &pkt) +{ + Q_D(QAVSubtitleCodec); + if (!d->avctx) + return AVERROR(EINVAL); + d->frame.setStream(pkt.stream()); + return avcodec_decode_subtitle2( + d->avctx, + d->frame.subtitle(), + &d->gotOutput, + const_cast(pkt.packet())); +} + +int QAVSubtitleCodec::read(QAVStreamFrame &frame) +{ + Q_D(QAVSubtitleCodec); + if (!d->avctx) + return AVERROR(EINVAL); + if (!d->gotOutput) + return AVERROR(EAGAIN); + *static_cast(&frame) = d->frame; + d->gotOutput = 0; + d->frame = {}; + return 0; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec_p.h b/QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec_p.h new file mode 100644 index 0000000..914d725 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec_p.h @@ -0,0 +1,43 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVSUBTITLECODEC_P_H +#define QAVSUBTITLECODEC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavsubtitleframe.h" +#include "qavcodec_p.h" +#include "qavpacket_p.h" + +QT_BEGIN_NAMESPACE + +class QAVSubtitleCodecPrivate; +class QAVSubtitleCodec : public QAVCodec +{ +public: + QAVSubtitleCodec(); + + int write(const QAVPacket &pkt) override; + int read(QAVStreamFrame &frame) override; + +private: + Q_DECLARE_PRIVATE(QAVSubtitleCodec) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.cpp b/QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.cpp new file mode 100644 index 0000000..f95d9da --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.cpp @@ -0,0 +1,83 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavsubtitleframe.h" +#include "qavstreamframe_p.h" +#include + +extern "C" { +#include "libavcodec/avcodec.h" +#include "libavformat/avformat.h" +} + +QT_BEGIN_NAMESPACE + +class QAVSubtitleFramePrivate : public QAVStreamFramePrivate +{ +public: + QSharedPointer subtitle; + + double pts() const override; + double duration() const override; +}; + +static void subtitle_free(AVSubtitle *subtitle) +{ + avsubtitle_free(subtitle); +} + +QAVSubtitleFrame::QAVSubtitleFrame() + : QAVStreamFrame(*new QAVSubtitleFramePrivate) +{ + Q_D(QAVSubtitleFrame); + d->subtitle.reset(new AVSubtitle, subtitle_free); + memset(d->subtitle.data(), 0, sizeof(*d->subtitle.data())); +} + +QAVSubtitleFrame::~QAVSubtitleFrame() +{ +} + +QAVSubtitleFrame::QAVSubtitleFrame(const QAVSubtitleFrame &other) + : QAVSubtitleFrame() +{ + operator=(other); +} + +QAVSubtitleFrame &QAVSubtitleFrame::operator=(const QAVSubtitleFrame &other) +{ + Q_D(QAVSubtitleFrame); + QAVStreamFrame::operator=(other); + d->subtitle = static_cast(other.d_ptr.get())->subtitle; + + return *this; +} + +AVSubtitle *QAVSubtitleFrame::subtitle() const +{ + Q_D(const QAVSubtitleFrame); + return d->subtitle.data(); +} + +double QAVSubtitleFramePrivate::pts() const +{ + if (!subtitle) + return NAN; + AVRational tb; + tb.num = 1; + tb.den = AV_TIME_BASE; + return subtitle->pts == AV_NOPTS_VALUE ? NAN : subtitle->pts * av_q2d(tb); +} + +double QAVSubtitleFramePrivate::duration() const +{ + if (!subtitle) + return 0.0; + return (subtitle->end_display_time - subtitle->start_display_time) / 1000.0; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.h b/QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.h new file mode 100644 index 0000000..35849df --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.h @@ -0,0 +1,35 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFSUBTITLERAME_H +#define QAVFSUBTITLERAME_H + +#include + +QT_BEGIN_NAMESPACE + +struct AVSubtitle; +class QAVSubtitleFramePrivate; +class QAVSubtitleFrame : public QAVStreamFrame +{ +public: + QAVSubtitleFrame(); + ~QAVSubtitleFrame(); + QAVSubtitleFrame(const QAVSubtitleFrame &other); + QAVSubtitleFrame &operator=(const QAVSubtitleFrame &other); + + AVSubtitle *subtitle() const; + +private: + Q_DECLARE_PRIVATE(QAVSubtitleFrame) +}; + +Q_DECLARE_METATYPE(QAVSubtitleFrame) + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu.cpp new file mode 100644 index 0000000..9723de9 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu.cpp @@ -0,0 +1,38 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavvideobuffer_cpu_p.h" + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +QAVVideoFrame::MapData QAVVideoBuffer_CPU::map() +{ + QAVVideoFrame::MapData mapData; + auto frame = m_frame.frame(); + if (frame->format == AV_PIX_FMT_NONE) + return mapData; + + mapData.size = av_image_get_buffer_size(AVPixelFormat(frame->format), frame->width, frame->height, 1); + mapData.format = AVPixelFormat(frame->format); + + for (int i = 0; i < 4; ++i) { + if (!frame->linesize[i]) + break; + + mapData.bytesPerLine[i] = frame->linesize[i]; + mapData.data[i] = static_cast(frame->data[i]); + } + + return mapData; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu_p.h new file mode 100644 index 0000000..ad19fc5 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu_p.h @@ -0,0 +1,38 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOBUFFER_CPU_P_H +#define QAVVIDEOBUFFER_CPU_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavvideobuffer_p.h" + +QT_BEGIN_NAMESPACE + +class QAVVideoBuffer_CPU : public QAVVideoBuffer +{ +public: + QAVVideoBuffer_CPU() = default; + ~QAVVideoBuffer_CPU() = default; + explicit QAVVideoBuffer_CPU(const QAVVideoFrame &frame) : QAVVideoBuffer(frame) { } + + QAVVideoFrame::MapData map() override; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu.cpp new file mode 100644 index 0000000..2451f00 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu.cpp @@ -0,0 +1,34 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavvideobuffer_gpu_p.h" +#include + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +QAVVideoFrame::MapData QAVVideoBuffer_GPU::map() +{ + auto mapData = m_cpu.map(); + if (mapData.format == AV_PIX_FMT_NONE) { + int ret = av_hwframe_transfer_data(m_cpu.frame().frame(), m_frame.frame(), 0); + if (ret < 0) { + qWarning() << "Could not av_hwframe_transfer_data:" << ret; + return {}; + } + m_frame = QAVVideoFrame(); + mapData = m_cpu.map(); + } + + return mapData; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu_p.h new file mode 100644 index 0000000..f41f995 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu_p.h @@ -0,0 +1,42 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOBUFFER_GPU_P_H +#define QAVVIDEOBUFFER_GPU_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavvideobuffer_p.h" +#include "qavvideobuffer_cpu_p.h" + +QT_BEGIN_NAMESPACE + +class QAVVideoBuffer_GPU : public QAVVideoBuffer +{ +public: + QAVVideoBuffer_GPU() = default; + explicit QAVVideoBuffer_GPU(const QAVVideoFrame &frame) : QAVVideoBuffer(frame) { } + ~QAVVideoBuffer_GPU() = default; + + QAVVideoFrame::MapData map() override; + +protected: + QAVVideoBuffer_CPU m_cpu; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_p.h new file mode 100644 index 0000000..a98d907 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideobuffer_p.h @@ -0,0 +1,45 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOBUFFER_P_H +#define QAVVIDEOBUFFER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QRhi; +class QAVVideoBuffer +{ +public: + QAVVideoBuffer() = default; + explicit QAVVideoBuffer(const QAVVideoFrame &frame) : m_frame(frame) { } + virtual ~QAVVideoBuffer() = default; + const QAVVideoFrame &frame() const { return m_frame; } + + virtual QAVVideoFrame::MapData map() = 0; + virtual QAVVideoFrame::HandleType handleType() const { return QAVVideoFrame::NoHandle; } + virtual QVariant handle(QRhi */*rhi*/ = nullptr) const { return {}; } +protected: + QAVVideoFrame m_frame; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideocodec.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideocodec.cpp new file mode 100644 index 0000000..8af0fba --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideocodec.cpp @@ -0,0 +1,147 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavvideocodec_p.h" +#include "qavhwdevice_p.h" +#include "qavcodec_p_p.h" +#include "qavpacket_p.h" +#include "qavframe.h" +#include "qavvideoframe.h" +#include + +extern "C" { +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVVideoCodecPrivate : public QAVCodecPrivate +{ +public: + QSharedPointer hw_device; +}; + +static bool isSoftwarePixelFormat(AVPixelFormat from) +{ + switch (from) { + case AV_PIX_FMT_VAAPI: + case AV_PIX_FMT_VDPAU: + case AV_PIX_FMT_MEDIACODEC: + case AV_PIX_FMT_VIDEOTOOLBOX: + case AV_PIX_FMT_D3D11: + case AV_PIX_FMT_D3D11VA_VLD: +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(56, 0, 0) + case AV_PIX_FMT_OPENCL: +#endif + case AV_PIX_FMT_CUDA: + case AV_PIX_FMT_DXVA2_VLD: +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(52, 58, 101) + case AV_PIX_FMT_XVMC: +#endif +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 134, 0) + case AV_PIX_FMT_VULKAN: +#endif + case AV_PIX_FMT_DRM_PRIME: + case AV_PIX_FMT_MMAL: + case AV_PIX_FMT_QSV: + return false; + default: + return true; + } +} + +static AVPixelFormat negotiate_pixel_format(AVCodecContext *c, const AVPixelFormat *f) +{ + auto d = reinterpret_cast(c->opaque); + + QList supported; +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 0, 0) + for (int i = 0;; ++i) { + const AVCodecHWConfig *config = avcodec_get_hw_config(c->codec, i); + if (!config) + break; + + if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) + supported.append(config->device_type); + } + + if (!supported.isEmpty()) { + qDebug() << c->codec->name << ": supported hardware device contexts:"; + for (auto a: supported) + qDebug() << " " << av_hwdevice_get_type_name(a); + } else { + qWarning() << "None of the hardware accelerations are supported"; + } +#endif + + QList softwareFormats; + QList hardwareFormats; + for (int i = 0; f[i] != AV_PIX_FMT_NONE; ++i) { + if (!isSoftwarePixelFormat(f[i])) { + hardwareFormats.append(f[i]); + continue; + } + softwareFormats.append(f[i]); + } + + qDebug() << "Available pixel formats:"; + for (auto a : softwareFormats) { + auto dsc = av_pix_fmt_desc_get(a); + qDebug() << " " << dsc->name << ": AVPixelFormat(" << a << ")"; + } + + for (auto a : hardwareFormats) { + auto dsc = av_pix_fmt_desc_get(a); + qDebug() << " " << dsc->name << ": AVPixelFormat(" << a << ")"; + } + + AVPixelFormat pf = !softwareFormats.isEmpty() ? softwareFormats[0] : AV_PIX_FMT_NONE; + const char *decStr = "software"; + if (d->hw_device) { + for (auto f : hardwareFormats) { + if (f == d->hw_device->format()) { + d->hw_device->init(c); + pf = d->hw_device->format(); + decStr = "hardware"; + break; + } + } + } + + auto dsc = av_pix_fmt_desc_get(pf); + if (dsc) + qDebug() << "Using" << decStr << "decoding in" << dsc->name; + else + qDebug() << "None of the pixel formats"; + + return pf; +} + +QAVVideoCodec::QAVVideoCodec() + : QAVFrameCodec(*new QAVVideoCodecPrivate) +{ + d_ptr->avctx->opaque = d_ptr.get(); + d_ptr->avctx->get_format = negotiate_pixel_format; +} + +QAVVideoCodec::~QAVVideoCodec() +{ + av_buffer_unref(&avctx()->hw_device_ctx); +} + +void QAVVideoCodec::setDevice(const QSharedPointer &d) +{ + d_func()->hw_device = d; +} + +QAVHWDevice *QAVVideoCodec::device() const +{ + return d_func()->hw_device.data(); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideocodec_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideocodec_p.h new file mode 100644 index 0000000..d7e811f --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideocodec_p.h @@ -0,0 +1,44 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOCODEC_P_H +#define QAVVIDEOCODEC_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavframecodec_p.h" + +QT_BEGIN_NAMESPACE + +class QAVVideoCodecPrivate; +class QAVHWDevice; +class QAVVideoCodec : public QAVFrameCodec +{ +public: + QAVVideoCodec(); + ~QAVVideoCodec(); + + void setDevice(const QSharedPointer &d); + QAVHWDevice *device() const; + +private: + Q_DISABLE_COPY(QAVVideoCodec) + Q_DECLARE_PRIVATE(QAVVideoCodec) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideofilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideofilter.cpp new file mode 100644 index 0000000..b751b24 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideofilter.cpp @@ -0,0 +1,149 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavvideofilter_p.h" +#include "qavfilter_p_p.h" +#include "qavcodec_p.h" +#include "qavvideoframe.h" +#include "qavstream.h" +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVVideoFilterPrivate : public QAVFilterPrivate +{ +public: + QAVVideoFilterPrivate(QAVFilter *q, QMutex &mutex) : QAVFilterPrivate(q, mutex) { } + + QList inputs; + QList outputs; +}; + +QAVVideoFilter::QAVVideoFilter( + const QAVStream &stream, + const QString &name, + const QList &inputs, + const QList &outputs, + QMutex &mutex) + : QAVFilter( + stream, + name, + *new QAVVideoFilterPrivate(this, mutex)) +{ + Q_D(QAVVideoFilter); + d->inputs = inputs; + d->outputs = outputs; +} + +int QAVVideoFilter::write(const QAVFrame &frame) +{ + Q_D(QAVVideoFilter); + if (!frame || frame.stream().stream()->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) { + qWarning() << "Frame is not video"; + return AVERROR(EINVAL); + } + if (!d->isEmpty) + return AVERROR(EAGAIN); + + d->sourceFrame = frame; + for (const auto &filter : d->inputs) { + if (!filter.supports(d->sourceFrame)) { + d->sourceFrame = {}; + return AVERROR(ENOTSUP); + } + QAVFrame ref = d->sourceFrame; + QMutexLocker locker(&d->graphMutex); + int ret = av_buffersrc_add_frame_flags(filter.ctx(), ref.frame(), AV_BUFFERSRC_FLAG_PUSH); + if (ret < 0) + return ret; + } + + d->isEmpty = false; + return 0; +} + +int QAVVideoFilter::read(QAVFrame &frame) +{ + Q_D(QAVVideoFilter); + if (d->outputs.isEmpty() || d->isEmpty) { + int ret = AVERROR(EAGAIN); + if (d->sourceFrame && d->outputs.isEmpty()) { + frame = d->sourceFrame; + ret = 0; + } + d->sourceFrame = {}; + d->isEmpty = true; + return ret; + } + + int ret = 0; + if (d->outputFrames.isEmpty()) { + for (int i = 0; i < d->outputs.size(); ++i) { + const auto &filter = d->outputs[i]; + while (true) { + QAVFrame out = d->sourceFrame; + // av_buffersink_get_frame_flags allocates frame's data + av_frame_unref(out.frame()); + { + QMutexLocker locker(&d->graphMutex); + ret = av_buffersink_get_frame_flags(filter.ctx(), out.frame(), 0); + } + if (ret < 0) + break; + +#if LIBAVUTIL_VERSION_INT <= AV_VERSION_INT(57, 30, 0) + if (!out.frame()->pkt_duration) + out.frame()->pkt_duration = d->sourceFrame.frame()->pkt_duration; +#else + if (out.frame()->duration == AV_NOPTS_VALUE || out.frame()->duration == 0) + out.frame()->duration = d->sourceFrame.frame()->duration; +#endif + out.setFrameRate(av_buffersink_get_frame_rate(filter.ctx())); + out.setTimeBase(av_buffersink_get_time_base(filter.ctx())); + out.setFilterName( + !filter.name().isEmpty() + ? filter.name() + : QString(QLatin1String("%1:%2")).arg(d->name).arg(QString::number(i))); + if (!out.stream()) + out.setStream(d->stream); + d->outputFrames.push_back(out); + } + } + } + + ret = AVERROR(EAGAIN); + if (!d->outputFrames.isEmpty()) { + frame = d->outputFrames.takeFirst(); + ret = 0; + } + if (d->outputFrames.isEmpty()) { + d->sourceFrame = {}; + d->isEmpty = true; + } + return ret; +} + +void QAVVideoFilter::flush() +{ + Q_D(QAVVideoFilter); + for (const auto &filter : d->inputs) { + int ret = av_buffersrc_add_frame(filter.ctx(), nullptr); + if (ret < 0) + qWarning() << "Could not flush:" << ret; + } + d->isEmpty = false; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideofilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideofilter_p.h new file mode 100644 index 0000000..ed38df4 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideofilter_p.h @@ -0,0 +1,52 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOFILTER_P_H +#define QAVVIDEOFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavfilter_p.h" +#include "qavvideoinputfilter_p.h" +#include "qavvideooutputfilter_p.h" +#include + +QT_BEGIN_NAMESPACE + +class QAVVideoFilterPrivate; +class QAVVideoFilter : public QAVFilter +{ +public: + QAVVideoFilter( + const QAVStream &stream, + const QString &name, + const QList &inputs, + const QList &outputs, + QMutex &mutex); + + int write(const QAVFrame &frame) override; + int read(QAVFrame &frame) override; + void flush() override; + +protected: + Q_DECLARE_PRIVATE(QAVVideoFilter) +private: + Q_DISABLE_COPY(QAVVideoFilter) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideoframe.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideoframe.cpp new file mode 100644 index 0000000..f1a1bff --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideoframe.cpp @@ -0,0 +1,475 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavvideoframe.h" +#include "qavvideobuffer_cpu_p.h" +#include "qavframe_p.h" +#include "qavvideocodec_p.h" +#include "qavhwdevice_p.h" +#include +#ifdef QT_AVPLAYER_MULTIMEDIA +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#include +#endif +#endif +#include + +extern "C" { +#include +#include +#include "libavutil/imgutils.h" +#include +}; + +QT_BEGIN_NAMESPACE + +static const QAVVideoCodec *videoCodec(const QAVCodec *c) +{ + return reinterpret_cast(c); +} + +class QAVVideoFramePrivate : public QAVFramePrivate +{ + Q_DECLARE_PUBLIC(QAVVideoFrame) +public: + QAVVideoFramePrivate(QAVVideoFrame *q) : q_ptr(q) { } + + QAVVideoBuffer &videoBuffer() const + { + if (!buffer) { + auto c = videoCodec(stream.codec().data()); + auto buf = c && c->device() && frame->format == c->device()->format() ? c->device()->videoBuffer(*q_ptr) : new QAVVideoBuffer_CPU(*q_ptr); + const_cast(this)->buffer.reset(buf); + } + + return *buffer; + } + + QAVVideoFrame *q_ptr = nullptr; + QScopedPointer buffer; +}; + +QAVVideoFrame::QAVVideoFrame() + : QAVFrame(*new QAVVideoFramePrivate(this)) +{ +} + +QAVVideoFrame::QAVVideoFrame(const QAVFrame &other) + : QAVVideoFrame() +{ + operator=(other); +} + +QAVVideoFrame::QAVVideoFrame(const QAVVideoFrame &other) + : QAVVideoFrame() +{ + operator=(other); +} + +QAVVideoFrame::QAVVideoFrame(const QSize &size, AVPixelFormat fmt) + : QAVVideoFrame() +{ + frame()->format = fmt; + frame()->width = size.width(); + frame()->height = size.height(); + av_frame_get_buffer(frame(), 1); +} + +QAVVideoFrame &QAVVideoFrame::operator=(const QAVFrame &other) +{ + Q_D(QAVVideoFrame); + QAVFrame::operator=(other); + d->buffer.reset(); + return *this; +} + +QAVVideoFrame &QAVVideoFrame::operator=(const QAVVideoFrame &other) +{ + Q_D(QAVVideoFrame); + QAVFrame::operator=(other); + d->buffer.reset(); + return *this; +} + +QSize QAVVideoFrame::size() const +{ + Q_D(const QAVFrame); + return {d->frame->width, d->frame->height}; +} + +QAVVideoFrame::MapData QAVVideoFrame::map() const +{ + Q_D(const QAVVideoFrame); + return d->videoBuffer().map(); +} + +QAVVideoFrame::HandleType QAVVideoFrame::handleType() const +{ + Q_D(const QAVVideoFrame); + return d->videoBuffer().handleType(); +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +QVariant QAVVideoFrame::handle(QRhi *rhi) const +{ + Q_D(const QAVVideoFrame); + return d->videoBuffer().handle(rhi); +} +#else +QVariant QAVVideoFrame::handle() const +{ + Q_D(const QAVVideoFrame); + return d->videoBuffer().handle(); +} +#endif + +AVPixelFormat QAVVideoFrame::format() const +{ + return static_cast(frame()->format); +} + +QString QAVVideoFrame::formatName() const +{ + return QLatin1String(av_pix_fmt_desc_get(QAVVideoFrame::format())->name); +} + +QAVVideoFrame QAVVideoFrame::convertTo(AVPixelFormat fmt) const +{ + if (fmt == frame()->format) + return *this; + + auto mapData = map(); + if (mapData.format == AV_PIX_FMT_NONE) { + qWarning() << __FUNCTION__ << "Could not map:" << formatName(); + return QAVVideoFrame(); + } + auto ctx = sws_getContext(size().width(), size().height(), mapData.format, + size().width(), size().height(), fmt, + SWS_BICUBIC, NULL, NULL, NULL); + if (ctx == nullptr) { + qWarning() << __FUNCTION__ << ": Could not get sws context:" << formatName(); + return QAVVideoFrame(); + } + + int ret = sws_setColorspaceDetails(ctx, sws_getCoefficients(SWS_CS_ITU601), + 0, sws_getCoefficients(SWS_CS_ITU709), 0, 0, 1 << 16, 1 << 16); + if (ret == -1) { + qWarning() << __FUNCTION__ << "Colorspace not support"; + return QAVVideoFrame(); + } + + QAVVideoFrame result(size(), fmt); + result.d_ptr->stream = d_ptr->stream; + sws_scale(ctx, mapData.data, mapData.bytesPerLine, 0, result.size().height(), result.frame()->data, result.frame()->linesize); + sws_freeContext(ctx); + + return result; +} + +#ifdef QT_AVPLAYER_MULTIMEDIA +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +class PlanarVideoBuffer : public QAbstractPlanarVideoBuffer +{ +public: + PlanarVideoBuffer(const QAVVideoFrame &frame, HandleType type = NoHandle) + : QAbstractPlanarVideoBuffer(type), m_frame(frame) + { + } + + QVariant handle() const override + { + return m_frame.handle(); + } + + MapMode mapMode() const override { return m_mode; } + using QAbstractPlanarVideoBuffer::map; + int map(MapMode mode, int *numBytes, int bytesPerLine[4], uchar *data[4]) override + { + if (m_mode != NotMapped || mode == NotMapped) + return 0; + + auto mapData = m_frame.map(); + m_mode = mode; + if (numBytes) + *numBytes = mapData.size; + + int i = 0; + for (; i < 4; ++i) { + if (!mapData.bytesPerLine[i]) + break; + + bytesPerLine[i] = mapData.bytesPerLine[i]; + data[i] = mapData.data[i]; + } + + return i; + } + void unmap() override { m_mode = NotMapped; } + +private: + QAVVideoFrame m_frame; + MapMode m_mode = NotMapped; +}; +#else +class PlanarVideoBuffer : public QAbstractVideoBuffer +{ +public: + PlanarVideoBuffer(const QAVVideoFrame &frame, QVideoFrameFormat::PixelFormat format + , QVideoFrame::HandleType type = QVideoFrame::NoHandle) + : QAbstractVideoBuffer(type) + , m_frame(frame) + , m_pixelFormat(format) + { + } + + quint64 textureHandle(int plane) const override + { + if (m_textures.isNull()) + const_cast(this)->m_textures = m_frame.handle(m_rhi); + if (m_textures.canConvert>()) { + auto textures = m_textures.toList(); + auto r = plane < textures.size() ? textures[plane].toULongLong() : 0; + return r; + } + return m_textures.toULongLong(); + } + + QVideoFrame::MapMode mapMode() const override { return m_mode; } + MapData map(QVideoFrame::MapMode mode) override + { + MapData res; + if (m_mode != QVideoFrame::NotMapped || mode == QVideoFrame::NotMapped) + return res; + + m_mode = mode; + auto mapData = m_frame.map(); + auto *desc = QVideoTextureHelper::textureDescription(m_pixelFormat); + res.nPlanes = desc->nplanes; + for (int i = 0; i < res.nPlanes; ++i) { + if (!mapData.bytesPerLine[i]) + break; + + res.data[i] = mapData.data[i]; + res.bytesPerLine[i] = mapData.bytesPerLine[i]; + // TODO: Reimplement heightForPlane + res.size[i] = mapData.bytesPerLine[i] * desc->heightForPlane(m_frame.size().height(), i); + } + return res; + } + void unmap() override { m_mode = QVideoFrame::NotMapped; } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + std::unique_ptr mapTextures(QRhi *rhi) override + { + m_rhi = rhi; + if (m_textures.isNull()) + m_textures = m_frame.handle(m_rhi); + return nullptr; + } + + static QVideoFrameFormat::ColorSpace colorSpace(const AVFrame *frame) + { + switch (frame->colorspace) { + default: + case AVCOL_SPC_UNSPECIFIED: + case AVCOL_SPC_RESERVED: + case AVCOL_SPC_FCC: + case AVCOL_SPC_SMPTE240M: + case AVCOL_SPC_YCGCO: + case AVCOL_SPC_SMPTE2085: + case AVCOL_SPC_CHROMA_DERIVED_NCL: + case AVCOL_SPC_CHROMA_DERIVED_CL: + case AVCOL_SPC_ICTCP: // BT.2100 ICtCp + return QVideoFrameFormat::ColorSpace_Undefined; + case AVCOL_SPC_RGB: + return QVideoFrameFormat::ColorSpace_AdobeRgb; + case AVCOL_SPC_BT709: + return QVideoFrameFormat::ColorSpace_BT709; + case AVCOL_SPC_BT470BG: // BT601 + case AVCOL_SPC_SMPTE170M: // Also BT601 + return QVideoFrameFormat::ColorSpace_BT601; + case AVCOL_SPC_BT2020_NCL: // Non constant luminence + case AVCOL_SPC_BT2020_CL: // Constant luminence + return QVideoFrameFormat::ColorSpace_BT2020; + } + } + + static QVideoFrameFormat::ColorTransfer colorTransfer(const AVFrame *frame) + { + switch (frame->color_trc) { + case AVCOL_TRC_BT709: + // The following three cases have transfer characteristics identical to BT709 + case AVCOL_TRC_BT1361_ECG: + case AVCOL_TRC_BT2020_10: + case AVCOL_TRC_BT2020_12: + case AVCOL_TRC_SMPTE240M: // almost identical to bt709 + return QVideoFrameFormat::ColorTransfer_BT709; + case AVCOL_TRC_GAMMA22: + case AVCOL_TRC_SMPTE428: // No idea, let's hope for the best... + case AVCOL_TRC_IEC61966_2_1: // sRGB, close enough to 2.2... + case AVCOL_TRC_IEC61966_2_4: // not quite, but probably close enough + return QVideoFrameFormat::ColorTransfer_Gamma22; + case AVCOL_TRC_GAMMA28: + return QVideoFrameFormat::ColorTransfer_Gamma28; + case AVCOL_TRC_SMPTE170M: + return QVideoFrameFormat::ColorTransfer_BT601; + case AVCOL_TRC_LINEAR: + return QVideoFrameFormat::ColorTransfer_Linear; + case AVCOL_TRC_SMPTE2084: + return QVideoFrameFormat::ColorTransfer_ST2084; + case AVCOL_TRC_ARIB_STD_B67: + return QVideoFrameFormat::ColorTransfer_STD_B67; + default: + break; + } + return QVideoFrameFormat::ColorTransfer_Unknown; + } + + static QVideoFrameFormat::ColorRange colorRange(const AVFrame *frame) + { + switch (frame->color_range) { + case AVCOL_RANGE_MPEG: + return QVideoFrameFormat::ColorRange_Video; + case AVCOL_RANGE_JPEG: + return QVideoFrameFormat::ColorRange_Full; + default: + return QVideoFrameFormat::ColorRange_Unknown; + } + } + + static float maxNits(const AVFrame *frame) + { + float maxNits = -1; + for (int i = 0; i < frame->nb_side_data; ++i) { + AVFrameSideData *sd = frame->side_data[i]; + // TODO: Longer term we might want to also support HDR10+ dynamic metadata + if (sd->type == AV_FRAME_DATA_MASTERING_DISPLAY_METADATA) { + auto data = reinterpret_cast(sd->data); + auto b = data->max_luminance; + auto maybeLum = b.den != 0 ? 10'000.0 * qreal(b.num) / qreal(b.den) : std::optional{}; + if (maybeLum) + maxNits = float(maybeLum.value()); + } + } + return maxNits; + } +#endif + +private: + QAVVideoFrame m_frame; + QVideoFrameFormat::PixelFormat m_pixelFormat = QVideoFrameFormat::Format_Invalid; + QVideoFrame::MapMode m_mode = QVideoFrame::NotMapped; + QVariant m_textures; +#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0) + QRhi *m_rhi = nullptr; +#endif +}; + +#endif // #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +QAVVideoFrame::operator QVideoFrame() const +{ + QAVVideoFrame result = *this; + if (!result) + return QVideoFrame(); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + using VideoFrame = QVideoFrame; +#else + using VideoFrame = QVideoFrameFormat; +#endif + + VideoFrame::PixelFormat format = VideoFrame::Format_Invalid; + switch (frame()->format) { + case AV_PIX_FMT_RGB32: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + format = VideoFrame::Format_RGB32; +#else + format = QVideoFrameFormat::Format_BGRA8888; +#endif + break; + case AV_PIX_FMT_YUV420P: + format = VideoFrame::Format_YUV420P; + break; + case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUV422P: +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + result = convertTo(AV_PIX_FMT_YUV420P); + format = VideoFrame::Format_YUV420P; +#else + format = VideoFrame::Format_YUV422P; +#endif + break; + case AV_PIX_FMT_VAAPI: + case AV_PIX_FMT_VDPAU: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + format = VideoFrame::Format_BGRA32; +#else + format = QVideoFrameFormat::Format_RGBA8888; +#endif + break; + case AV_PIX_FMT_D3D11: + case AV_PIX_FMT_VIDEOTOOLBOX: + case AV_PIX_FMT_NV12: + format = VideoFrame::Format_NV12; + break; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + case AV_PIX_FMT_MEDIACODEC: + format = VideoFrame::Format_SamplerExternalOES; + break; +#endif + default: + // TODO: Add more supported formats instead of converting + result = convertTo(AV_PIX_FMT_YUV420P); + format = VideoFrame::Format_YUV420P; + break; + } + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + using HandleType = QAbstractVideoBuffer::HandleType; +#else + using HandleType = QVideoFrame::HandleType; +#endif + + HandleType type = HandleType::NoHandle; + switch (handleType()) { + case GLTextureHandle: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + type = HandleType::GLTextureHandle; +#else + type = HandleType::RhiTextureHandle; +#endif + break; + case MTLTextureHandle: + case D3D11Texture2DHandle: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + type = HandleType::RhiTextureHandle; +#endif + break; + default: + break; + } + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + return QVideoFrame(new PlanarVideoBuffer(result, type), size(), format); +#else + QVideoFrameFormat videoFormat(size(), format); +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + videoFormat.setColorSpace(PlanarVideoBuffer::colorSpace(frame())); + videoFormat.setColorTransfer(PlanarVideoBuffer::colorTransfer(frame())); + videoFormat.setColorRange(PlanarVideoBuffer::colorRange(frame())); + videoFormat.setMaxLuminance(PlanarVideoBuffer::maxNits(frame())); +#endif + return QVideoFrame(new PlanarVideoBuffer(result, format, type), videoFormat); +#endif +} +#endif // #ifdef QT_AVPLAYER_MULTIMEDIA + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideoframe.h b/QtAVPlayer/src/QtAVPlayer/qavvideoframe.h new file mode 100644 index 0000000..e9ee277 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideoframe.h @@ -0,0 +1,80 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVFVIDEORAME_H +#define QAVFVIDEORAME_H + +#include +#include +#ifdef QT_AVPLAYER_MULTIMEDIA +#include +#endif + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +class QAVVideoFramePrivate; +class QAVCodec; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +class QRhi; +#endif +class QAVVideoFrame : public QAVFrame +{ +public: + enum HandleType + { + NoHandle, + GLTextureHandle, + MTLTextureHandle, + D3D11Texture2DHandle + }; + + QAVVideoFrame(); + QAVVideoFrame(const QAVFrame &other); + QAVVideoFrame(const QAVVideoFrame &other); + QAVVideoFrame(const QSize &size, AVPixelFormat fmt); + + QAVVideoFrame &operator=(const QAVFrame &other); + QAVVideoFrame &operator=(const QAVVideoFrame &other); + + QSize size() const; + + struct MapData + { + int size = 0; + int bytesPerLine[4] = {0}; + uchar *data[4] = {nullptr}; + AVPixelFormat format = AV_PIX_FMT_NONE; + }; + + MapData map() const; + HandleType handleType() const; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QVariant handle(QRhi *rhi = nullptr) const; +#else + QVariant handle() const; +#endif + AVPixelFormat format() const; + QString formatName() const; + QAVVideoFrame convertTo(AVPixelFormat fmt) const; +#ifdef QT_AVPLAYER_MULTIMEDIA + operator QVideoFrame() const; +#endif + +protected: + Q_DECLARE_PRIVATE(QAVVideoFrame) +}; + +Q_DECLARE_METATYPE(QAVVideoFrame) +Q_DECLARE_METATYPE(AVPixelFormat) + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter.cpp new file mode 100644 index 0000000..bc91087 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter.cpp @@ -0,0 +1,116 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavframe.h" +#include "qavvideoinputfilter_p.h" +#include "qavinoutfilter_p_p.h" +#include "qavdemuxer_p.h" +#include + +extern "C" { +#include +#include +#include +} + +QT_BEGIN_NAMESPACE + +class QAVVideoInputFilterPrivate : public QAVInOutFilterPrivate +{ +public: + QAVVideoInputFilterPrivate(QAVInOutFilter *q) + : QAVInOutFilterPrivate(q) + { } + + AVPixelFormat format = AV_PIX_FMT_NONE; + int width = 0; + int height = 0; + AVRational sample_aspect_ratio{}; + AVRational time_base{}; + AVRational frame_rate{}; +}; + +QAVVideoInputFilter::QAVVideoInputFilter() + : QAVInOutFilter(*new QAVVideoInputFilterPrivate(this)) +{ +} + +QAVVideoInputFilter::QAVVideoInputFilter(const QAVFrame &frame) + : QAVVideoInputFilter() +{ + Q_D(QAVVideoInputFilter); + const auto & frm = frame.frame(); + const auto & stream = frame.stream().stream(); + d->format = frm->format != AV_PIX_FMT_NONE ? AVPixelFormat(frm->format) : AVPixelFormat(stream->codecpar->format); + d->width = frm->width ? frm->width : stream->codecpar->width; + d->height = frm->height ? frm->height : stream->codecpar->height; + d->sample_aspect_ratio = frm->sample_aspect_ratio.num && frm->sample_aspect_ratio.den ? frm->sample_aspect_ratio : stream->codecpar->sample_aspect_ratio; + d->time_base = stream->time_base; + d->frame_rate = stream->avg_frame_rate; +} + +QAVVideoInputFilter::QAVVideoInputFilter(const QAVVideoInputFilter &other) + : QAVVideoInputFilter() +{ + *this = other; +} + +QAVVideoInputFilter::~QAVVideoInputFilter() = default; + +QAVVideoInputFilter &QAVVideoInputFilter::operator=(const QAVVideoInputFilter &other) +{ + Q_D(QAVVideoInputFilter); + QAVInOutFilter::operator=(other); + d->format = other.d_func()->format; + d->width = other.d_func()->width; + d->height = other.d_func()->height; + d->sample_aspect_ratio = other.d_func()->sample_aspect_ratio; + d->time_base = other.d_func()->time_base; + d->frame_rate = other.d_func()->frame_rate; + return *this; +} + +int QAVVideoInputFilter::configure(AVFilterGraph *graph, AVFilterInOut *in) +{ + QAVInOutFilter::configure(graph, in); + Q_D(QAVVideoInputFilter); + AVBPrint args; + av_bprint_init(&args, 0, AV_BPRINT_SIZE_AUTOMATIC); + av_bprintf(&args, + "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:" + "pixel_aspect=%d/%d", + d->width, d->height, d->format, + d->time_base.num, d->time_base.den, + d->sample_aspect_ratio.num, qMax(d->sample_aspect_ratio.den, 1)); + if (d->frame_rate.num && d->frame_rate.den) + av_bprintf(&args, ":frame_rate=%d/%d", d->frame_rate.num, d->frame_rate.den); + + static int index = 0; + char name[255]; + snprintf(name, sizeof(name), "buffer_%d", index++); + + int ret = avfilter_graph_create_filter(&d->ctx, + avfilter_get_by_name("buffer"), + name, args.str, nullptr, graph); + if (ret < 0) + return ret; + + return avfilter_link(d->ctx, 0, in->filter_ctx, in->pad_idx); +} + +bool QAVVideoInputFilter::supports(const QAVFrame &frame) const +{ + Q_D(const QAVVideoInputFilter); + if (!frame) + return true; + const auto & frm = frame.frame(); + return d->width == frm->width + && d->height == frm->height + && d->format == frm->format; +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter_p.h new file mode 100644 index 0000000..e882542 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter_p.h @@ -0,0 +1,46 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOINPUTFILTER_P_H +#define QAVVIDEOINPUTFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavinoutfilter_p.h" + +QT_BEGIN_NAMESPACE + +class QAVFrame; +class QAVVideoInputFilterPrivate; +class QAVVideoInputFilter : public QAVInOutFilter +{ +public: + QAVVideoInputFilter(const QAVFrame &frame); + QAVVideoInputFilter(const QAVVideoInputFilter &other); + ~QAVVideoInputFilter(); + QAVVideoInputFilter &operator=(const QAVVideoInputFilter &other); + + int configure(AVFilterGraph *graph, AVFilterInOut *in) override; + bool supports(const QAVFrame &frame) const; + +protected: + QAVVideoInputFilter(); + Q_DECLARE_PRIVATE(QAVVideoInputFilter) +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter.cpp b/QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter.cpp new file mode 100644 index 0000000..a8d943b --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter.cpp @@ -0,0 +1,42 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#include "qavvideooutputfilter_p.h" +#include "qavinoutfilter_p_p.h" +#include + +extern "C" { +#include +} + +QT_BEGIN_NAMESPACE + +QAVVideoOutputFilter::QAVVideoOutputFilter() + : QAVInOutFilter(*new QAVInOutFilterPrivate(this)) +{ +} + +QAVVideoOutputFilter::~QAVVideoOutputFilter() = default; + +int QAVVideoOutputFilter::configure(AVFilterGraph *graph, AVFilterInOut *out) +{ + QAVInOutFilter::configure(graph, out); + Q_D(QAVInOutFilter); + static int index = 0; + char name[255]; + snprintf(name, sizeof(name), "buffersink_%d", index++); + + int ret = avfilter_graph_create_filter(&d->ctx, + avfilter_get_by_name("buffersink"), + name, nullptr, nullptr, graph); + if (ret < 0) + return ret; + + return avfilter_link(out->filter_ctx, out->pad_idx, d->ctx, 0); +} + +QT_END_NAMESPACE diff --git a/QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter_p.h b/QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter_p.h new file mode 100644 index 0000000..50b14e2 --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter_p.h @@ -0,0 +1,38 @@ +/********************************************************* + * Copyright (C) 2021, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QAVVIDEOOUTPUTFILTER_P_H +#define QAVVIDEOOUTPUTFILTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qavinoutfilter_p.h" + +QT_BEGIN_NAMESPACE + +class QAVVideoOutputFilterPrivate; +class QAVVideoOutputFilter : public QAVInOutFilter +{ +public: + QAVVideoOutputFilter(); + ~QAVVideoOutputFilter(); + + int configure(AVFilterGraph *graph, AVFilterInOut *out) override; +}; + +QT_END_NAMESPACE + +#endif diff --git a/QtAVPlayer/src/QtAVPlayer/qtavplayerglobal.h b/QtAVPlayer/src/QtAVPlayer/qtavplayerglobal.h new file mode 100644 index 0000000..6c597ed --- /dev/null +++ b/QtAVPlayer/src/QtAVPlayer/qtavplayerglobal.h @@ -0,0 +1,13 @@ +/********************************************************* + * Copyright (C) 2020, Val Doroshchuk * + * * + * This file is part of QtAVPlayer. * + * Free Qt Media Player based on FFmpeg. * + *********************************************************/ + +#ifndef QTAVPLAYERGLOBAL_H +#define QTAVPLAYERGLOBAL_H + +#include + +#endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..e64189f --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# JustVideo # + +JustVideo is a playlist based video player that also works as a font-end +video surveillance player for JustMotion. + +In surveillance mode it automatically detects and plays video footage of any +JustMotion based mounted filesystem. If not playing from JustMotion it will +playlist all videos found in the opened folder. + +# Playback Support # + +JustVideo uses libav so it supports a very wide verity of audio/video codecs +depending on your installation. Run the following cmd to get a full list: + +``` +ffmpeg -codecs +``` + +### Build/Install ### + +This application is currently only compatible with a Linux based operating +systems that are capable of installing python3 and the QT API (QT6.X.X or +better). + +``` +./build.py <--run this first +./install.py <--run this next +``` +``` +note 1: the build script will search for the QT api installed in your + system. if not found, it will ask you where it is. either way + it is recommended to install the QT API before running this + script. +note 2: both scripts assume python3 is already installed. +``` diff --git a/build.py b/build.py new file mode 100755 index 0000000..9bdc986 --- /dev/null +++ b/build.py @@ -0,0 +1,262 @@ +#!/usr/bin/python3 + +import os +import re +import subprocess +import shutil +import sys +import platform + +def get_app_target(text): + return re.search(r'(APP_TARGET) +(\"(.*?)\")', text).group(3) + +def get_app_ver(text): + return re.search(r'(APP_VERSION) +(\"(.*?)\")', text).group(3) + +def get_app_name(text): + return re.search(r'(APP_NAME) +(\"(.*?)\")', text).group(3) + +def get_qt_path(): + try: + return str(subprocess.check_output(["qtpaths", "--binaries-dir"]), 'utf-8').strip() + + except: + print("A direct call to 'qtpaths' has failed so automatic retrieval of the QT bin folder is not possible.") + + return input("Please enter the QT bin path (leave blank to cancel the build): ") + +def get_qt_from_cli(): + for arg in sys.argv: + if arg == "-qt_dir": + index = sys.argv.index(arg) + + try: + return sys.argv[index + 1] + + except: + return "" + + return "" + +def get_ver_header(): + current_dir = os.path.dirname(__file__) + + if current_dir == "": + return "src" + os.sep + "common.h" + else: + return current_dir + os.sep + "src" + os.sep + "common.h" + +def get_nearest_subdir(path, sub_name): + dir_list = os.listdir(path) + ret = "" + + for entry in dir_list: + if sub_name in entry: + ret = entry + + break + + return ret + +def cd(): + current_dir = os.path.dirname(__file__) + + if current_dir != "": + os.chdir(current_dir) + +def verbose_copy(src, dst): + print("cpy: " + src + " --> " + dst) + + if os.path.isdir(src): + if os.path.exists(dst) and os.path.isdir(dst): + shutil.rmtree(dst) + + try: + # ignore errors thrown by shutil.copytree() + # it's likely not actually failing to copy + # the directory but still throws errors if + # it fails to apply the same file stats as + # the source. this type of error can be + # ignored. + shutil.copytree(src, dst) + + except: + pass + + elif os.path.exists(src): + shutil.copyfile(src, dst) + + else: + print("wrn: " + src + " does not exists. skipping.") + +def linux_build_app_dir(app_ver, app_name, app_target, qt_bin): + if not os.path.exists("app_dir/platforms"): + os.makedirs("app_dir/platforms") + + if not os.path.exists("app_dir/xcbglintegrations"): + os.makedirs("app_dir/xcbglintegrations") + + if not os.path.exists("app_dir/multimedia"): + os.makedirs("app_dir/multimedia") + + if not os.path.exists("app_dir/platformthemes"): + os.makedirs("app_dir/platformthemes") + + if not os.path.exists("app_dir/lib"): + os.makedirs("app_dir/lib") + + if not os.path.exists("app_dir/icons"): + os.makedirs("app_dir/icons") + + verbose_copy(qt_bin + "/../plugins/platforms", "app_dir/platforms") + verbose_copy(qt_bin + "/../plugins/xcbglintegrations", "app_dir/xcbglintegrations") + verbose_copy(qt_bin + "/../plugins/multimedia", "app_dir/multimedia") + verbose_copy(qt_bin + "/../plugins/platformthemes", "app_dir/platformthemes") + verbose_copy("build/" + app_target, "app_dir/" + app_target) + verbose_copy("icons/main.svg", "app_dir/icons/scalable.svg") + + img_sizes = [8, 16, 22, 24, 28, 32, 36, 42, 48, 64, 72, 96, 128, 192, 256, 512] + + for i in img_sizes: + subprocess.run(["inkscape", "-w", str(i), "-h", str(i), "icons/main.svg", "-o", "app_dir/icons/" + str(i) + ".png"]) + + shutil.copyfile("build/" + app_target, "/tmp/" + app_target) + # copying the executable file from the build folder to + # temp bypasses any -noexe retrictions a linux file + # system may have. there is a chance temp is also + # restricted in this way but that kind of config is + # rare. ldd will not run correctly with -noexe + # enabled. + + lines = str(subprocess.check_output(["ldd", "/tmp/" + app_target]), 'utf-8').split("\n") + + os.remove("/tmp/" + app_target) + + for line in lines: + if " => " in line and "libc" not in line: + #if ("libQt" in line) or ("libicu" in line) or ("libGL.so" in line) or ("libpcre16.so" in line) or ("libpcre.so" in line): + if " (0x0" in line: + start_index = line.index("> ") + 2 + end_index = line.index(" (0x0") + src_file = line[start_index:end_index] + file_name = os.path.basename(src_file) + + verbose_copy(src_file, "app_dir/lib/" + file_name) + + if "/usr/lib/x86_64-linux-gnu/qt6/bin" == qt_bin: + verbose_copy(qt_bin + "/../../libQt6DBus.so.6", "app_dir/lib/libQt6DBus.so.6") + verbose_copy(qt_bin + "/../../libQt6XcbQpa.so.6", "app_dir/lib/libQt6XcbQpa.so.6") + + else: + verbose_copy(qt_bin + "/../lib/libQt6DBus.so.6", "app_dir/lib/libQt6DBus.so.6") + verbose_copy(qt_bin + "/../lib/libQt6XcbQpa.so.6", "app_dir/lib/libQt6XcbQpa.so.6") + verbose_copy(qt_bin + "/../lib/libQt6OpenGL.so.6", "app_dir/lib/libQt6OpenGL.so.6") + + verbose_copy("templates/linux_run_script.sh", "app_dir/" + app_target + ".sh") + verbose_copy("templates/linux_uninstall.sh", "app_dir/uninstall.sh") + verbose_copy("templates/linux_icon.desktop", "app_dir/" + app_target + ".desktop") + + complete(app_ver, app_target) + +def complete(app_ver, app_target): + print("Build complete for version: " + app_ver) + print("You can now run the install.py script to install onto this machine or create an installer.") + +def get_like_distro(): + info = platform.freedesktop_os_release() + ids = [info["ID"]] + + if "ID_LIKE" in info: + # ids are space separated and ordered by precedence + ids.extend(info["ID_LIKE"].split()) + + return ids + +def list_installed_packages(): + like_distro = get_like_distro() + + if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro): + return str(subprocess.check_output(["apt", "list", "--installed"]), 'utf-8') + + elif ("fedora" in like_distro) or ("rhel" in like_distro): + return str(subprocess.check_output(["dnf", "list", "installed"]), 'utf-8') + + elif ("arch" in like_distro): + return str(subprocess.check_output(["pacman", "-Q"]), 'utf-8') + + else: + print("Warning: unable to determine a package manager for this platform.") + + return [] + + +def list_of_words_in_text(list_of_words, text_body): + for word in list_of_words: + if not word in text_body: + return False + + return True + +def platform_setup(): + ins_packages = list_installed_packages() + like_distro = get_like_distro() + dep_pkgs_a = ["pkg-config"] + dep_pkgs_b = ["ffmpeg", "libavcodec-dev", "libavformat-dev", "libavfilter-dev", "libavdevice-dev", "libilmbase-dev", "libvdpau-dev", "libxkbcommon-dev", "libgl-dev", "libxcb-cursor0", "inkscape"] + + if not list_of_words_in_text(dep_pkgs_a, ins_packages) or not list_of_words_in_text(dep_pkgs_b, ins_packages): + if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro): + subprocess.run(["sudo", "apt", "update", "-y"]) + subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_a) + subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_b) + + elif ("fedora" in like_distro) or ("rhel" in like_distro): + subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_a) + subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_b) + + elif ("arch" in like_distro): + subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_a) + subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_b) + +def main(): + platform_setup() + + with open(get_ver_header()) as file: + text = file.read() + + app_target = get_app_target(text) + app_ver = get_app_ver(text) + app_name = get_app_name(text) + qt_bin = get_qt_from_cli() + + if qt_bin == "": + qt_bin = get_qt_path() + + if qt_bin != "": + print("app_target = " + app_target) + print("app_version = " + app_ver) + print("app_name = " + app_name) + print("qt_bin = " + qt_bin) + + cd() + + result = subprocess.run([qt_bin + os.sep + "qmake", "-config", "release"]) + + if result.returncode == 0: + result = subprocess.run(["make"]) + + if result.returncode == 0: + if os.path.exists("app_dir"): + shutil.rmtree("app_dir") + + os.makedirs("app_dir") + + with open("app_dir" + os.sep + "info.txt", "w") as info_file: + info_file.write(app_target + "\n") + info_file.write(app_ver + "\n") + info_file.write(app_name + "\n") + + linux_build_app_dir(app_ver, app_name, app_target, qt_bin) + + +if __name__ == "__main__": + main() diff --git a/icons/check.png b/icons/check.png new file mode 100644 index 0000000..0146337 Binary files /dev/null and b/icons/check.png differ diff --git a/icons/error.png b/icons/error.png new file mode 100644 index 0000000..9d94bca Binary files /dev/null and b/icons/error.png differ diff --git a/icons/main.png b/icons/main.png new file mode 100755 index 0000000..441b2ec Binary files /dev/null and b/icons/main.png differ diff --git a/icons/main.svg b/icons/main.svg new file mode 100755 index 0000000..b6b5e17 --- /dev/null +++ b/icons/main.svg @@ -0,0 +1 @@ +playlist diff --git a/install.py b/install.py new file mode 100755 index 0000000..f313941 --- /dev/null +++ b/install.py @@ -0,0 +1,342 @@ +#!/usr/bin/python3 + +import os +import subprocess +import shutil +import platform +import sys +import zipfile +import binascii +import tempfile + +def cd(): + current_dir = os.path.dirname(__file__) + + if current_dir != "": + os.chdir(current_dir) + +def get_default_installer_path(app_ver, app_name): + if not os.path.exists("installers"): + os.makedirs("installers") + + return "installers/" + app_name + "-" + app_ver + "-" + platform.system() + "-" + platform.machine() + ".run" + +def make_install_dir(path): + try: + if not os.path.exists(path): + os.makedirs(path) + + return True + + except: + print("Failed to create the install directory, please make sure you are runnning this script with admin rights.") + + return False + +def replace_bin(binary, old_bin, new_bin, offs): + while(True): + try: + index = binary.index(old_bin, offs) + binary = binary[:index] + new_bin + binary[index + len(old_bin):] + + except ValueError: + break + + return binary + +def bin_sub_copy_file(src, dst, old_bin, new_bin, offs): + binary = bytearray() + + with open(src, "rb") as rd_file: + binary = rd_file.read() + binary = replace_bin(binary, old_bin, new_bin, offs) + + with open(dst, "wb") as wr_file: + wr_file.write(binary) + +def text_sub_copy_file(src, dst, old_text, new_text, offs): + bin_sub_copy_file(src, dst, old_text.encode("utf-8"), new_text.encode("utf-8"), offs) + +def text_template_deploy(src, dst, install_dir, app_name, app_target): + print("dep: " + dst) + + text_sub_copy_file(src, dst, "$install_dir", install_dir, 0) + text_sub_copy_file(dst, dst, "$app_name", app_name, 0) + text_sub_copy_file(dst, dst, "$app_target", app_target, 0) + +def verbose_copy(src, dst): + print("cpy: " + src + " --> " + dst) + + if os.path.isdir(src): + files = os.listdir(src) + + if not os.path.exists(dst): + os.makedirs(dst) + + for file in files: + tree_src = src + os.path.sep + file + tree_dst = dst + os.path.sep + file + + if os.path.isdir(tree_src): + if not os.path.exists(tree_dst): + os.makedirs(tree_dst) + + verbose_copy(tree_src, tree_dst) + + else: + shutil.copyfile(src, dst) + +def verbose_create_symmlink(src, dst): + print("lnk: " + src + " --> " + dst) + + if os.path.exists(dst): + os.remove(dst) + + os.symlink(src, dst) + +def local_install(app_target, app_name): + install_dir = "/opt/" + app_target + + if os.path.exists(install_dir + "/uninstall.sh"): + subprocess.run([install_dir + "/uninstall.sh"]) + + if make_install_dir(install_dir): + text_template_deploy("app_dir/" + app_target + ".sh", install_dir + "/" + app_target + ".sh", install_dir, app_name, app_target) + text_template_deploy("app_dir/uninstall.sh", install_dir + "/uninstall.sh", install_dir, app_name, app_target) + text_template_deploy("app_dir/" + app_target + ".desktop", "/usr/share/applications/" + app_target + ".desktop", install_dir, app_name, app_target) + + img_sizes = [8, 16, 22, 24, 28, 32, 36, 42, 48, 64, 72, 96, 128, 192, 256, 512] + + for i in img_sizes: + dst_img = "/usr/share/icons/hicolor/" + str(i) + "x" + str(i) + "/apps/" + app_target + ".png" + + verbose_copy("app_dir/icons/" + str(i) + ".png", dst_img) + + subprocess.run(["chmod", "644", dst_img]) + + verbose_copy("app_dir/icons/scalable.svg", "/usr/share/icons/hicolor/scalable/apps/" + app_target + ".svg") + verbose_copy("app_dir/" + app_target, install_dir + "/" + app_target) + verbose_copy("app_dir/lib", install_dir + "/lib") + verbose_copy("app_dir/platforms", install_dir + "/platforms") + verbose_copy("app_dir/xcbglintegrations", install_dir + "/xcbglintegrations") + verbose_copy("app_dir/multimedia", install_dir + "/multimedia") + verbose_copy("app_dir/platformthemes", install_dir + "/platformthemes") + + verbose_create_symmlink(install_dir + "/" + app_target + ".sh", "/usr/bin/" + app_target) + + subprocess.run(["chmod", "644", "/usr/share/icons/hicolor/scalable/apps/" + app_target + ".svg"]) + subprocess.run(["chmod", "755", install_dir + "/" + app_target + ".sh"]) + subprocess.run(["chmod", "755", install_dir + "/" + app_target]) + subprocess.run(["chmod", "755", install_dir + "/uninstall.sh"]) + + print("Installation finished. If you ever need to uninstall this application, run this command with root rights:") + print(" sh " + install_dir + "/uninstall.sh\n") + +def dir_tree(path): + ret = [] + + if os.path.isdir(path): + for entry in os.listdir(path): + full_path = os.path.join(path, entry) + + if os.path.isdir(full_path): + for sub_dir_file in dir_tree(full_path): + ret.append(sub_dir_file) + + else: + ret.append(full_path) + + return ret + +def to_hex(data): + return str(binascii.hexlify(data))[2:-1] + +def from_hex(text_line): + return binascii.unhexlify(text_line) + +def make_install(app_ver, app_name): + path = get_default_installer_path(app_ver, app_name) + + with zipfile.ZipFile("app_dir.zip", "w", compression=zipfile.ZIP_DEFLATED) as zip_file: + print("Compressing app_dir --") + + for file in dir_tree("app_dir"): + print("adding file: " + file) + zip_file.write(file) + + text_sub_copy_file(__file__, path, "main(is_sfx=False)", "main(is_sfx=True)\n\n\n", 10728) + + with open(path, "a") as dst_file, open("app_dir.zip", "rb") as src_file: + print("Packing the compressed app_dir into the sfx script file --") + + dst_file.write("# APP_DIR\n") + + stat = os.stat("app_dir.zip") + + while(True): + buffer = src_file.read(4000000) + + if len(buffer) != 0: + dst_file.write("# " + to_hex(buffer) + "\n") + + print(str(src_file.tell()) + "/" + str(stat.st_size)) + + if len(buffer) < 4000000: + break + + os.remove("app_dir.zip") + + subprocess.run(["chmod", "+x", path]) + + print("Finished packing the app.") + print("Installer: " + path) + +def sfx(): + abs_sfx_path = os.path.abspath(__file__) + mark_found = False + + os.chdir(tempfile.gettempdir()) + + with open(abs_sfx_path) as packed_file, open("app_dir.zip", "wb") as zip_file: + stat = os.stat(abs_sfx_path) + + print("Unpacking the app_dir compressed file from the sfx script.") + + while(True): + line = packed_file.readline() + + if not line: + break + + elif mark_found: + zip_file.write(from_hex(line[2:-1])) + + print(str(packed_file.tell()) + "/" + str(stat.st_size)) + + else: + if line == "# APP_DIR\n": + mark_found = True + + print("Done.") + + if not mark_found: + print("The app_dir mark was not found, unable to continue.") + + else: + with zipfile.ZipFile("app_dir.zip", "r", compression=zipfile.ZIP_DEFLATED) as zip_file: + print("De-compressing app_dir --") + + zip_file.extractall() + + print("Preparing for installation.") + + os.remove("app_dir.zip") + + with open("app_dir" + os.sep + "info.txt") as info_file: + info = info_file.read().split("\n") + + local_install(info[0], info[2]) + shutil.rmtree("app_dir") + +def get_like_distro(): + info = platform.freedesktop_os_release() + ids = [info["ID"]] + + if "ID_LIKE" in info: + # ids are space separated and ordered by precedence + ids.extend(info["ID_LIKE"].split()) + + return ids + +def list_installed_packages(): + like_distro = get_like_distro() + + if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro): + return str(subprocess.check_output(["apt", "list", "--installed"]), 'utf-8') + + elif ("fedora" in like_distro) or ("rhel" in like_distro): + return str(subprocess.check_output(["dnf", "list", "installed"]), 'utf-8') + + elif ("arch" in like_distro): + return str(subprocess.check_output(["pacman", "-Q"]), 'utf-8') + + else: + print("Warning: unable to determine a package manager for this platform.") + + return [] + + +def list_of_words_in_text(list_of_words, text_body): + for word in list_of_words: + if not word in text_body: + return False + + return True + +def platform_setup(): + ins_packages = list_installed_packages() + like_distro = get_like_distro() + dep_pkgs_a = ["pkg-config"] + dep_pkgs_b = ["ffmpeg", "libavcodec-dev", "libavformat-dev", "libavfilter-dev", "libavdevice-dev", "libilmbase-dev", "libvdpau-dev", "libxkbcommon-dev", "libgl-dev", "libxcb-cursor0"] + + if not list_of_words_in_text(dep_pkgs_a, ins_packages) or not list_of_words_in_text(dep_pkgs_b, ins_packages): + if ("ubuntu" in like_distro) or ("debian" in like_distro) or ("linuxmint" in like_distro): + subprocess.run(["sudo", "apt", "update", "-y"]) + subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_a) + subprocess.run(["sudo", "apt", "install", "-y"] + dep_pkgs_b) + + elif ("fedora" in like_distro) or ("rhel" in like_distro): + subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_a) + subprocess.run(["sudo", "dnf", "install", "-y"] + dep_pkgs_b) + + elif ("arch" in like_distro): + subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_a) + subprocess.run(["sudo", "pacman", "-S", "--noconfirm"] + dep_pkgs_b) + +def main(is_sfx): + cd() + + app_target = "" + app_ver = "" + app_name = "" + + if not is_sfx: + with open("app_dir" + os.sep + "info.txt") as info_file: + info = info_file.read().split("\n") + + app_target = info[0] + app_ver = info[1] + app_name = info[2] + + if is_sfx: + sfx() + + elif "--local" in sys.argv: + platform_setup() + local_install(app_target, app_name) + + elif "--installer" in sys.argv: + make_install(app_ver, app_name) + + else: + print("Do you want to install onto this machine or create an installer?") + print("[1] local machine") + print("[2] create installer") + print("[3] exit") + + while(True): + opt = input("select an option: ") + + if opt == "1": + subprocess.run(["sudo", "python3", "install.py", "--local"]) + break + + elif opt == "2": + subprocess.run(["python3", "install.py", "--installer"]) + break + + elif opt == "3": + break + +if __name__ == "__main__": + main(is_sfx=False) diff --git a/src/actions.cpp b/src/actions.cpp new file mode 100644 index 0000000..8254896 --- /dev/null +++ b/src/actions.cpp @@ -0,0 +1,168 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "actions.h" + +Actions::Actions(shared_t *share, QMenuBar *menuBar, QWidget *parent) : QObject(parent) +{ + mainGui = parent; + conf = share; + + fileMenu = new QMenu(tr("File"), mainGui); + editMenu = new QMenu(tr("Edit"), mainGui); + helpMenu = new QMenu(tr("Help"), mainGui); + recents = new QMenu(tr("Recent"), mainGui); + liveSelect = new QMenu(tr("Live"), mainGui); + footageSelect = new QMenu(tr("Footage"), mainGui); + + openFolder = new QAction(tr("Open Folder..."), this); + openFolderConf = new QAction(tr("Open Conf..."), this); + closeFolder = new QAction(tr("Close Folder"), this); + exitApp = new QAction(tr("Exit"), this); + about = new QAction(tr("About ") + QString(APP_NAME) + "...", this); + pref = new QAction(tr("Preferences"), this); + clearRecents = 0; + recentsSpacer = 0; + + rebuildRecents(); + + fileMenu->addAction(openFolder); + fileMenu->addAction(openFolderConf); + fileMenu->addMenu(recents); + fileMenu->addSeparator(); + fileMenu->addAction(closeFolder); + fileMenu->addSeparator(); + fileMenu->addAction(exitApp); + + editMenu->addAction(pref); + helpMenu->addAction(about); + + connect(openFolder, &QAction::triggered, this, &Actions::openFldDialog); + connect(openFolderConf, &QAction::triggered, this, &Actions::openFldConfDialog); + connect(closeFolder, &QAction::triggered, this, &Actions::closeSession); + connect(exitApp, &QAction::triggered, this, &Actions::closeApp); + connect(about, &QAction::triggered, this, &Actions::openAboutDialog); + connect(pref, &QAction::triggered, this, &Actions::openAppConfDialog); + + menuBar->addMenu(fileMenu); + menuBar->addMenu(editMenu); + + live = menuBar->addMenu(liveSelect); + footage = menuBar->addMenu(footageSelect); + + menuBar->addMenu(helpMenu); + + closeFolder->setEnabled(false); +} + +void Actions::rebuildRecents() +{ + if (recentsSpacer) recentsSpacer->deleteLater(); + if (clearRecents) clearRecents->deleteLater(); + + recents->clear(); + + recentsSpacer = recents->addSeparator(); + clearRecents = new QAction(tr("Clear"), this); + + recents->addAction(clearRecents); + + connect(clearRecents, &QAction::triggered, this, &Actions::rmRecents); +} + +void Actions::closeSession() +{ + emit contentClose(); + + closeFolder->setEnabled(false); + footageSelect->clear(); + liveSelect->clear(); + + resetSharedRes(conf); +} + +void Actions::closeApp() +{ + closeFolder->trigger(); + + QCoreApplication::instance()->quit(); +} + +void Actions::openFld(bool preFill) +{ + if (FolderDialog(conf, preFill, mainGui).exec() == QDialog::Accepted) + { + if (closeFolder->isEnabled()) + { + closeFolder->trigger(); + } + + closeFolder->setEnabled(true); + + emit contentOpen(); + } +} + +void Actions::openFldDialog() +{ + openFld(false); +} + +void Actions::openFldConfDialog() +{ + auto path = QFileDialog::getOpenFileName(mainGui, tr("Select a config file"), QDir::homePath(), tr("Config File (*.conf)")); + + if (!path.isEmpty()) + { + openConf(path); + } +} + +void Actions::openAppConfDialog() +{ + PrefDialog(conf, mainGui).exec(); +} + +void Actions::openConf(const QString &path) +{ + if (closeFolder->isEnabled()) + { + closeFolder->trigger(); + } + + rdConf(path, conf); + openFld(true); +} + +void Actions::openAboutDialog() +{ + QMessageBox box(mainGui); + + box.setWindowTitle(tr("About ") + QString(APP_NAME)); + box.setTextFormat(Qt::RichText); + box.setText(QString(APP_NAME) + " v" + QString(APP_VERSION)); + + QString info; + + info.append(tr("

Based on Qt ") + QString(qVersion()) + " " + QSysInfo::buildCpuArchitecture() + "


"); + info.append("

" + QString(APP_NAME) + tr(" is distributed in the hope that it will be useful,
")); + info.append(tr("but WITHOUT ANY WARRANTY; without even the implied warranty of
")); + info.append(tr("MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

")); + + box.setInformativeText(info); + box.exec(); +} + +void Actions::rmRecents() +{ + delFolderContents(conf->recentsPath); +} diff --git a/src/actions.h b/src/actions.h new file mode 100644 index 0000000..f0cc5d3 --- /dev/null +++ b/src/actions.h @@ -0,0 +1,73 @@ +#ifndef ACTIONS_H +#define ACTIONS_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "folder_dialog.h" +#include "main_widget.h" +#include "pref_dialog.h" + +class Actions : public QObject +{ + Q_OBJECT + +private: + + QWidget *mainGui; + shared_t *conf; + + void openFld(bool preFill); + +public slots: + + void openFldDialog(); + void openFldConfDialog(); + void openAboutDialog(); + void openAppConfDialog(); + void closeApp(); + void closeSession(); + void rmRecents(); + +public: + + QMenu *fileMenu; + QMenu *editMenu; + QMenu *helpMenu; + QMenu *recents; + QMenu *liveSelect; + QMenu *footageSelect; + + QAction *openFolder; + QAction *openFolderConf; + QAction *closeFolder; + QAction *exitApp; + QAction *about; + QAction *pref; + QAction *live; + QAction *footage; + QAction *clearRecents; + QAction *recentsSpacer; + + void openConf(const QString &path); + void rebuildRecents(); + + explicit Actions(shared_t *share, QMenuBar *menuBar, QWidget *parent); + +signals: + + void contentOpen(); + void contentClose(); +}; + +#endif // ACTIONS_H diff --git a/src/common.cpp b/src/common.cpp new file mode 100644 index 0000000..a7e48d7 --- /dev/null +++ b/src/common.cpp @@ -0,0 +1,335 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" + +QStringList lsFiles(const QString &path, const QStringList &filters) +{ + QDir dirObj(path); + + dirObj.setFilter(QDir::Files); + dirObj.setSorting(QDir::Name); + + if (!filters.isEmpty()) + { + dirObj.setNameFilters(filters); + } + + return dirObj.entryList(); +} + +QStringList lsFiles(const QString &path) +{ + return lsFiles(path, QStringList()); +} + +QStringList lsVidFiles(const QString &path) +{ + QStringList filters; + + filters << "*.mp4"; filters << "*.ts"; filters << "*.mov"; filters << "*.avi"; + filters << "*.wmv"; filters << "*.flv"; filters << "*.webm"; filters << "*.avchd"; + filters << "*.mkv"; + + return lsFiles(path, filters); +} + +QStringList lsConfFiles(const QString &path) +{ + QStringList filters; + + filters << "*.conf"; + + return lsFiles(path, filters); +} + +QStringList lsDirsInDir(const QString &path) +{ + QDir dirObj(path); + + dirObj.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); + dirObj.setSorting(QDir::Name); + + return dirObj.entryList(); +} + +void rdLine(const QString ¶m, const QString &line, QString *value) +{ + if (line.startsWith(param)) + { + *value = line.mid(param.size()).trimmed(); + } +} + +void rdLine(const QString ¶m, const QString &line, int *value) +{ + if (line.startsWith(param)) + { + *value = line.mid(param.size()).trimmed().toInt(); + } +} + +void rdLine(const QString ¶m, const QString &line, bool *value) +{ + if (line.startsWith(param)) + { + auto val = line.mid(param.size()).trimmed(); + + *value = (val == "y" || val == "Y"); + } +} + +bool delFolderContents(const QString &path) +{ + auto ret = false; + auto files = lsFiles(path); + auto dirs = lsDirsInDir(path); + + for (auto &&file: files) + { + ret = QFile::remove(path + QDir::separator() + file); + } + + for (auto &&dir: dirs) + { + ret = QDir(dir).removeRecursively(); + } + + return ret; +} + +void extCorrection(QString &ext) +{ + if (!ext.startsWith(".")) + { + ext = "." + ext; + } +} + +bool mkPath(const QString &path) +{ + auto ret = true; + + if (!QFileInfo::exists(path)) + { + ret = QDir(path).mkpath(path); + } + + return ret; +} + +void resetSharedRes(shared_t *share) +{ + share->camName.clear(); + share->buffPath.clear(); + share->recPath.clear(); + share->playPath.clear(); + + share->conf = share->appConfPath; + share->retCode = 0; + share->isJM = false; +} + +bool runCmd(const QStringList &args, QWidget *parent) +{ + auto ret = true; + + if (!args.isEmpty()) + { + if (QProcess::execute(args[0], args.mid(1)) != 0) + { + QMessageBox::critical(parent, QObject::tr("Exec Error"), QObject::tr("Command: ") + args.join(' ') + QObject::tr(" returned non-zero. See Help > Logs for details")); + + ret = false; + } + } + + return ret; +} + +bool rdConf(const QString &filePath, shared_t *share) +{ + QFile varFile(filePath); + + if (!varFile.open(QFile::ReadOnly)) + { + share->retCode = ENOENT; + + QTextStream(stderr) << "err: config file - " << filePath << " does not exists or lack read permissions." << Qt::endl; + } + else + { + resetSharedRes(share); + + share->conf = filePath; + + QString line; + + do + { + line = QString::fromUtf8(varFile.readLine()); + + if (!line.startsWith("#")) + { + rdLine("cam_name = ", line, &share->camName); + rdLine("buffer_path = ", line, &share->buffPath); + rdLine("rec_path = ", line, &share->recPath); + rdLine("is_jm = ", line, &share->isJM); + rdLine("play_path = ", line, &share->playPath); + rdLine("max_recents = ", line, &share->maxRecents); + rdLine("max_views_per_page = ", line, &share->maxViewsPerPage); + rdLine("winrect_w = ", line, &share->windowW); + rdLine("winrect_h = ", line, &share->windowH); + rdLine("winrect_x = ", line, &share->windowX); + rdLine("winrect_y = ", line, &share->windowY); + rdLine("plistrect_w = ", line, &share->plistW); + rdLine("plistrect_h = ", line, &share->plistH); + rdLine("plistrect_x = ", line, &share->plistX); + rdLine("plistrect_y = ", line, &share->plistY); + } + + } while(!line.isEmpty()); + + if (share->camName.isEmpty()) + { + share->camName = QFileInfo(share->conf).baseName(); + } + + if (share->buffPath.isEmpty()) + { + share->buffPath = "/var/buffer/" + share->camName; + } + else + { + share->buffPath = QDir::cleanPath(share->buffPath); + } + + if (share->recPath.isEmpty()) + { + share->recPath = "/var/footage/" + share->camName; + } + else + { + share->recPath = QDir::cleanPath(share->recPath); + } + + qInfo() << "--Conf file: " << filePath << " --"; + qInfo() << "cam_name = " << share->camName; + qInfo() << "buffer_path = " << share->buffPath; + qInfo() << "rec_path = " << share->recPath; + qInfo() << "is_jm = " << share->isJM; + qInfo() << "play_path = " << share->playPath; + qInfo() << "max_recents = " << share->maxRecents; + qInfo() << "max_views_per_page = " << share->maxViewsPerPage; + qInfo() << "winrect_w = " << share->windowW; + qInfo() << "winrect_h = " << share->windowH; + qInfo() << "winrect_x = " << share->windowX; + qInfo() << "winrcet_y = " << share->windowY; + qInfo() << "plistrect_w = " << share->plistW; + qInfo() << "plistrect_h = " << share->plistH; + qInfo() << "plistrect_x = " << share->plistX; + qInfo() << "plistrect_y = " << share->plistY; + qInfo() << "-----------------------------------"; + } + + return share->retCode == 0; +} + +void wrConf(const QString &filePath, shared_t *share) +{ + QFile file(filePath); + QString txt; + QTextStream ts(&txt); + + if (file.open(QFile::WriteOnly)) + { + if (filePath == share->appConfPath) + { + ts << "max_recents = " << share->maxRecents << Qt::endl; + ts << "max_views_per_page = " << share->maxViewsPerPage << Qt::endl; + ts << "winrect_w = " << share->windowW << Qt::endl; + ts << "winrect_h = " << share->windowH << Qt::endl; + ts << "winrect_x = " << share->windowX << Qt::endl; + ts << "winrect_y = " << share->windowY << Qt::endl; + ts << "plistrect_w = " << share->plistW << Qt::endl; + ts << "plistrect_h = " << share->plistH << Qt::endl; + ts << "plistrect_x = " << share->plistX << Qt::endl; + ts << "plistrect_y = " << share->plistY << Qt::endl; + } + else + { + if (!share->playPath.isEmpty()) ts << "play_path = " << share->playPath << Qt::endl; + + if (share->isJM) ts << "is_jm = y" << Qt::endl; + else ts << "is_jm = n" << Qt::endl; + } + + file.write(txt.toUtf8()); + } + + file.close(); +} + +void createOrUpdateRecent(shared_t *share) +{ + if (share->conf == share->appConfPath) + { + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(share->playPath.toUtf8()); + + if (share->isJM) + { + hasher.addData("FF"); + } + + wrConf(share->recentsPath + QDir::separator() + hasher.result().toHex() + ".conf", share); + } + else + { + wrConf(share->conf, share); + } +} + +void buildGrid(QGridLayout *mainLayout, int parentWidth, const QList &widList, int cellWidth) +{ + auto columns = 0; + + if (widList.size() == 2) + { + columns = 1; + } + else + { + columns = parentWidth / cellWidth; + } + + for (auto &&wid : widList) + { + mainLayout->removeWidget(wid); + } + + if (columns != 0) + { + for (auto row = 0, i = 0; i < widList.size(); ++row) + { + for (auto col = 0; (col < columns) && (i < widList.size()); ++col, ++i) + { + mainLayout->addWidget(widList[i], row, col); + } + } + } + else + { + qInfo() << "buildGrid() << calculated a zero column grid. This may cause a blank layout."; + } +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..ff08ec0 --- /dev/null +++ b/src/common.h @@ -0,0 +1,125 @@ +#ifndef COMMON_H +#define COMMON_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define APP_VERSION "1.0.0" +#define APP_NAME "JustVideo" +#define APP_TARGET "jvideo" + +#define PULL_RATE 700 + +class Actions; +class VidWidget; + +struct shared_t +{ + QString conf; + QString buffPath; + QString recPath; + QString camName; + QString playPath; + QString recentsPath; + QString appDataPath; + QString appConfPath; + bool isJM; + int windowW; + int windowH; + int windowX; + int windowY; + int plistW; + int plistH; + int plistX; + int plistY; + int maxViewsPerPage; + int maxRecents; + int retCode; +}; + +QStringList lsFiles(const QString &path, const QStringList &filters); +QStringList lsFiles(const QString &path); +QStringList lsVidFiles(const QString &path); +QStringList lsConfFiles(const QString &path); +QStringList lsDirsInDir(const QString &path); +bool rdConf(const QString &filePath, shared_t *share); +bool mkPath(const QString &path); +bool delFolderContents(const QString &path); +bool runCmd(const QStringList &args, QWidget *parent); +void createOrUpdateRecent(shared_t *share); +void wrConf(const QString &filePath, shared_t *share); +void rdLine(const QString ¶m, const QString &line, QString *value); +void rdLine(const QString ¶m, const QString &line, int *value); +void rdLine(const QString ¶m, const QString &line, bool *value); +void extCorrection(QString &ext); +void resetSharedRes(shared_t *share); +void buildGrid(QGridLayout *mainLayout, int parentWidth, const QList &widList, int cellWidth); + +#endif // COMMON_H diff --git a/src/folder_dialog.cpp b/src/folder_dialog.cpp new file mode 100644 index 0000000..815e4ff --- /dev/null +++ b/src/folder_dialog.cpp @@ -0,0 +1,159 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "folder_dialog.h" + +FolderDialog::FolderDialog(shared_t *conf, bool preFill, QWidget *parent) : QDialog(parent) +{ + shared = conf; + + auto mainLayout = new QVBoxLayout(this); + auto formWidget = new QWidget(this); + auto formLayout = new QFormLayout(formWidget); + auto btnWidget = new QWidget(this); + auto btnLayout = new QHBoxLayout(btnWidget); + auto okBtn = new QPushButton(tr("Ok"), this); + auto cancelBtn = new QPushButton(tr("Cancel"), this); + auto browseBtn = new QToolButton(this); + + folder = new QLineEdit(this); + jmFolder = new QCheckBox(this); + + browseBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + browseBtn->setText("..."); + + formLayout->addRow(new QLabel(tr("Folder:"), this), sideBySide(folder, browseBtn)); + formLayout->addRow(new QLabel(tr("JustMotion:"), this), jmFolder); + + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); + + mainLayout->addWidget(formWidget, 0, Qt::AlignHCenter); + mainLayout->addWidget(btnWidget, 0, Qt::AlignHCenter); + + connect(okBtn, &QPushButton::clicked, this, &FolderDialog::run); + connect(cancelBtn, &QPushButton::clicked, this, &FolderDialog::reject); + connect(browseBtn, &QToolButton::clicked, this, &FolderDialog::browse); + + setContentsMargins(0,0,0,0); + + if (preFill) + { + QTimer::singleShot(500, this, &FolderDialog::autoRun); + } +} + +void FolderDialog::autoRun() +{ + rd(); run(); +} + +void FolderDialog::setWidIntoLayout(QWidget *widget, QHBoxLayout *lay, int wid) +{ + if (widget == nullptr) + { + auto blank = new QWidget(this); + + if (wid != 0) + { + blank->setFixedWidth(wid); + } + + lay->addWidget(blank); + } + else + { + if (wid != 0) + { + widget->setFixedWidth(wid); + } + + lay->addWidget(widget); + } +} + +QWidget *FolderDialog::sideBySide(QWidget *left, QWidget *right, int lWid, int rWid) +{ + auto ret = new QWidget(this); + auto lay = new QHBoxLayout(ret); + + if (left != nullptr) + { + left->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + } + + setWidIntoLayout(left, lay, lWid); + setWidIntoLayout(right, lay, rWid); + + lay->setSpacing(0); + + return ret; +} + +void FolderDialog::rd() +{ + folder->setText(shared->playPath); + jmFolder->setChecked(shared->isJM); +} + +void FolderDialog::wr() +{ + shared->playPath = folder->text(); + shared->isJM = jmFolder->isChecked(); +} + +void FolderDialog::errMsg(const QString &shortMsg, const QString &longMsg) +{ + QMessageBox box(this); + + box.setIcon(QMessageBox::Critical); + box.setText(shortMsg); + + if (!longMsg.isEmpty()) + { + box.setDetailedText(longMsg); + } + + box.exec(); + + setDisabled(false); +} + +void FolderDialog::browse() +{ + auto path = QFileDialog::getExistingDirectory(this, tr("Select a directory"), QDir::homePath(), QFileDialog::ShowDirsOnly); + + if (!path.isEmpty()) + { + folder->setText(path); + } +} + +void FolderDialog::run() +{ + wr(); + + createOrUpdateRecent(shared); + + if (!QFileInfo::exists(shared->playPath)) + { + errMsg(tr("Folder Non-existant"), tr("Play path '") + shared->playPath + tr("' does not exists")); + } + else if (!QFileInfo(shared->playPath).isDir()) + { + errMsg(tr("Non-directory Error"), tr("Play path '") + shared->playPath + tr("' is not a directory")); + } + else + { + accept(); + } +} diff --git a/src/folder_dialog.h b/src/folder_dialog.h new file mode 100644 index 0000000..0dfd945 --- /dev/null +++ b/src/folder_dialog.h @@ -0,0 +1,45 @@ +#ifndef FOLDER_DIALOG_H +#define FOLDER_DIALOG_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" + +class FolderDialog : public QDialog +{ + Q_OBJECT + +private: + + shared_t *shared; + QLineEdit *folder; + QCheckBox *jmFolder; + + void rd(); + void wr(); + void errMsg(const QString &shortMsg, const QString &longMsg); + void setWidIntoLayout(QWidget *widget, QHBoxLayout *lay, int wid); + QWidget *sideBySide(QWidget *left, QWidget *right, int lWid = 0, int rWid = 0); + +private slots: + + void run(); + void browse(); + void autoRun(); + +public: + + explicit FolderDialog(shared_t *conf, bool preFill, QWidget *parent = nullptr); +}; + +#endif // FOLDER_DIALOG_H diff --git a/src/img_loader.cpp b/src/img_loader.cpp new file mode 100644 index 0000000..62ee684 --- /dev/null +++ b/src/img_loader.cpp @@ -0,0 +1,226 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "img_loader.h" + +ImgLoader::ImgLoader(QObject *parent) : QObject(nullptr) +{ + timer = nullptr; + player = nullptr; + vidRunning = false; + seek = true; + + auto thr = new QThread(nullptr); + + connect(parent, &QObject::destroyed, this, &ImgLoader::deleteLater); + connect(this, &ImgLoader::destroyed, thr, &QThread::quit); + connect(this, &ImgLoader::loop, this, &ImgLoader::loadNextImg); + connect(thr, &QThread::started, this, &ImgLoader::loadNextImg); + connect(thr, &QThread::finished, thr, &QThread::deleteLater); + + moveToThread(thr); + + thr->start(); +} + +void ImgLoader::resetPlayer() +{ + // QAVPLAYER has an issue with rapid play-stop usage. It will seg fault if + // I attempt to use the same object in rapid play-stop hence this function + // that will recreate the object on every play-stop. garbage collection + // issue? QAVStream::stream() returns null. + + if (player != nullptr) + { + player->deleteLater(); + } + + seek = true; + player = new QAVPlayer(this); + + connect(player, &QAVPlayer::videoFrame, this, &ImgLoader::rdVideoFrame); + connect(player, &QAVPlayer::stopped, this, &ImgLoader::playerDone); + connect(this, &ImgLoader::nextVid, this, &ImgLoader::loadNextImgFromVid); +} + +void ImgLoader::playerDone(qint64 pos) +{ + Q_UNUSED(pos); + + auto queuePath = QString(IMG_FROM_VID) + player->source(); + auto label = vidQueue[queuePath]; + + if (label == nullptr) + { + qDebug() << "nullptr returned reading from video extraction queue. skipping: " << queuePath; + } + else + { + vidQueue.remove(queuePath); + + QVideoFrame qframe(rawFrame); + QPixmap pix; + + auto img = qframe.toImage(); + + if (img.isNull()) + { + qDebug() << "Failed to convert QImage from raw frame: " << queuePath; + } + else if (!pix.convertFromImage(img)) + { + qDebug() << "Failed to convert QPixmap from QImage: " << queuePath; + } + else + { + label->setPixmap(pix.scaled(250, 180, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + } + } + + resetPlayer(); + + emit nextVid(); +} + +void ImgLoader::rdVideoFrame(const QAVVideoFrame &frame) +{ + if (seek) + { + player->seek(player->duration() * 0.10); seek = false; + } + else + { + rawFrame = frame; + + player->stop(); + } +} + +void ImgLoader::loadNextImg() +{ + if (player == nullptr) + { + resetPlayer(); + } + + if (timer == nullptr) + { + timer = new QTimer(this); + + timer->setInterval(3000); + timer->setSingleShot(true); + + connect(timer, &QTimer::timeout, this, &ImgLoader::loadNextImg); + } + + timer->start(); + + mutex.lock(); + + if (!imgQueue.isEmpty()) + { + auto keys = imgQueue.keys(); + auto imgPath = keys.first(); + auto imgBtn = imgQueue.take(imgPath); + + qInfo() << "Loading thumbnail: " << imgPath << " Queue size: " << imgQueue.size(); + + QPixmap pix; + + if (imgPath.startsWith(IMG_FROM_VID)) + { + vidQueue.insert(imgPath, imgBtn); + + qInfo() << "'" << IMG_FROM_VID << "' tag detected in the thumbnail path, queueing up for capture from video file. Queue size: " << vidQueue.size(); + } + else if (pix.load(imgPath)) + { + imgBtn->setPixmap(pix.scaled(250, 180, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + } + else + { + imgBtn->setPixmap(QPixmap(":/icons/video-250.png")); + + qInfo() << "Failed to load a thumbnail from: " << imgPath << " the format might not be compatible. loading default image."; + } + } + + mutex.unlock(); + + if (!imgQueue.isEmpty()) + { + emit loop(); + } + else if (!vidQueue.isEmpty() && !vidRunning) + { + emit nextVid(); + } +} + +void ImgLoader::loadNextImgFromVid() +{ + mutex.lock(); + + if (!vidQueue.isEmpty()) + { + vidRunning = true; + + auto keys = vidQueue.keys(); + auto imgPath = keys.first(); + auto vidPath = imgPath.mid(QString(IMG_FROM_VID).size()); + + if (!vidProced.contains(vidPath)) + { + qDebug() << "Start video file: " << vidPath << " to extract image from video frame."; + + vidProced.append(vidPath); + + player->setSource(vidPath); + player->play(); + } + } + else + { + vidRunning = false; + } + + mutex.unlock(); +} + +void ImgLoader::addImg(const QString &path, QLabel *label) +{ + mutex.lock(); + + imgQueue.insert(path, label); + + mutex.unlock(); +} + +void ImgLoader::rmImg(const QString &path) +{ + mutex.lock(); + + vidQueue.remove(path); + imgQueue.remove(path); + + mutex.unlock(); +} + +void ImgLoader::clear() +{ + mutex.lock(); + + vidQueue.clear(); + imgQueue.clear(); + + mutex.unlock(); +} diff --git a/src/img_loader.h b/src/img_loader.h new file mode 100644 index 0000000..e2f6833 --- /dev/null +++ b/src/img_loader.h @@ -0,0 +1,59 @@ +#ifndef IMG_LOADER_H +#define IMG_LOADER_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" + +#define IMG_FROM_VID "#IMG-FROM-VID" + +class ImgLoader : public QObject +{ + Q_OBJECT + +private: + + QTimer *timer; + QMutex mutex; + QAVPlayer *player; + QMap imgQueue; + QMap vidQueue; + QList vidProced; + QAVVideoFrame rawFrame; + bool vidRunning; + bool seek; + + void resetPlayer(); + +private slots: + + void loadNextImg(); + void loadNextImgFromVid(); + void rdVideoFrame(const QAVVideoFrame &frame); + void playerDone(qint64 pos); + +public: + + void addImg(const QString &path, QLabel *label); + void rmImg(const QString &path); + void clear(); + + explicit ImgLoader(QObject *parent); + +signals: + + void loop(); + void nextVid(); +}; + +#endif // IMG_LOADER_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..35d23b4 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,87 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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.RPOSE. See the +// GNU General Public License for more details. + +#include "common.h" +#include "actions.h" +#include "main_widget.h" +#include "recents.h" + +int main(int argc, char** argv) +{ + QApplication app(argc, argv); + + QApplication::setApplicationName(APP_NAME); + QApplication::setApplicationVersion(APP_VERSION); + + MainGui mainGui; + QMenuBar menuBar; + shared_t shared; + + auto rec = app.primaryScreen()->geometry(); + + shared.appDataPath = QDir::homePath() + QDir::separator() + "." + QString(APP_TARGET); + shared.recentsPath = shared.appDataPath + QDir::separator() + "recent"; + shared.appConfPath = shared.appDataPath + QDir::separator() + "app.conf"; + shared.windowH = (rec.height() / 4) * 3; + shared.windowW = rec.width() / 2; + shared.maxRecents = 20; + shared.maxViewsPerPage = 4; + shared.windowX = rec.x(); + shared.windowY = rec.y(); + shared.plistX = 100; + shared.plistY = 100; + shared.plistW = 550; + shared.plistH = shared.windowH; + shared.retCode = 0; + + mkPath(shared.appDataPath); + mkPath(shared.recentsPath); + + if (!QFileInfo::exists(shared.appConfPath)) + { + wrConf(shared.appConfPath, &shared); + } + + rdConf(shared.appConfPath, &shared); + + Actions actions(&shared, &menuBar, &mainGui); + MainWidget mainWidget(&shared, &menuBar, &mainGui, &actions); + + new RecentsSync(&shared, &actions, &actions); + + mainWidget.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + mainWidget.setContentsMargins(0,0,0,0); + + mainGui.setContentsMargins(0,0,0,0); + mainGui.setCentralWidget(&mainWidget); + mainGui.setMenuBar(&menuBar); + mainGui.setWindowTitle(APP_NAME); + mainGui.setMinimumHeight(600); + mainGui.setMinimumWidth(800); + mainGui.setGeometry(shared.windowX, shared.windowY, shared.windowW, shared.windowH); + mainGui.show(); + + auto ret = app.exec(); + + rec = mainGui.geometry(); + + shared.windowH = rec.height(); + shared.windowW = rec.width(); + shared.windowX = rec.x(); + shared.windowY = rec.y(); + + resetSharedRes(&shared); + wrConf(shared.appConfPath, &shared); + + return ret; +} diff --git a/src/main_widget.cpp b/src/main_widget.cpp new file mode 100644 index 0000000..1cdd0e9 --- /dev/null +++ b/src/main_widget.cpp @@ -0,0 +1,164 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "main_widget.h" + +MainGui::MainGui(QWidget *parent) : QMainWindow(parent) +{ + layout()->setSpacing(0); +} + +void MainGui::closeEvent(QCloseEvent *event) +{ + emit killSes(); + + QMainWindow::closeEvent(event); +} + +MainWidget::MainWidget(shared_t *share, QMenuBar *mainMenuBar, MainGui *parent, Actions *acts) : QStackedWidget(parent) +{ + shared = share; + actions = acts; + menuBar = mainMenuBar; + mainGui = parent; + homepage = new QWidget(this); + + actions->live->setVisible(false); + actions->footage->setVisible(false); + + addWidget(homepage); + setCurrentWidget(homepage); + + parentWidget()->setWindowTitle(QString(APP_NAME)); + + connect(actions, &Actions::contentClose, this, &MainWidget::reset); + connect(actions, &Actions::contentOpen, this, &MainWidget::open); + + connect(parent, &MainGui::killSes, acts->closeFolder, &QAction::trigger); +} + +void MainWidget::reset() +{ + actions->live->setVisible(false); + actions->footage->setVisible(false); + + setCurrentWidget(homepage); + + parentWidget()->setWindowTitle(QString(APP_NAME)); +} + +void MainWidget::open() +{ + parentWidget()->setWindowTitle(shared->playPath + " - " + QString(APP_NAME)); + + if (shared->isJM) + { + actions->live->setVisible(true); + actions->footage->setVisible(true); + + auto mntLoc = QDir::cleanPath(shared->playPath); + auto confPath = mntLoc + QDir::separator() + "etc" + QDir::separator() + "jmotion"; + auto confFiles = lsFiles(confPath); + auto confs = QList(); + auto pageConfs = QList(); + + for (auto &&confFile : confFiles) + { + shared_t conf; + + if (rdConf(confPath + QDir::separator() + confFile, &conf)) + { + confs.append(conf); + } + } + + if (confs.isEmpty()) + { + QMessageBox::critical(this, tr("JustMotion Config Error"), tr("Did not find any config files in: '") + mntLoc + "' check the folder for any conf files in the etc/jmotion subfolder."); + + actions->closeFolder->trigger(); + } + else + { + auto page = 1; + + for (auto &&conf : confs) + { + pageConfs.append(conf); + + if (pageConfs.size() == shared->maxViewsPerPage) + { + mwMultiViewBuild(pageConfs, page++); pageConfs.clear(); + } + } + + if (!pageConfs.isEmpty()) + { + mwMultiViewBuild(pageConfs, page); + } + + for (auto &&conf : confs) + { + mwSingleLiveViewBuild(QDir::cleanPath(mntLoc + QDir::separator() + conf.buffPath + QDir::separator() + "live"), conf.camName); + } + + for (auto &&conf : confs) + { + mwSingleFootageViewBuild(QDir::cleanPath(mntLoc + QDir::separator() + conf.recPath), conf.camName); + } + } + } + else + { + actions->live->setVisible(false); + actions->footage->setVisible(false); + + playPathBuild(); + } +} + +void MainWidget::setView(QWidget *widget) +{ + if (currentWidget() == homepage) + { + setCurrentWidget(widget); + } +} + +void MainWidget::addVidWidget(VidWidget *vidWidget) +{ + QCoreApplication::instance()->installEventFilter(vidWidget); + + connect(vidWidget, &VidWidget::mainWindowsVis, mainGui, &MainGui::setVisible); + + setView(vidWidget); +} + +void MainWidget::playPathBuild() +{ + addVidWidget(new VidWidget(shared, "", shared->playPath, false, true, nullptr, actions->closeFolder, this)); +} + +void MainWidget::mwSingleLiveViewBuild(const QString &path, const QString &camName) +{ + addVidWidget(new VidWidget(shared, camName, path, false, false, actions->liveSelect, actions->closeFolder, this)); +} + +void MainWidget::mwSingleFootageViewBuild(const QString &path, const QString &camName) +{ + addVidWidget(new VidWidget(shared, camName, path, false, true, actions->footageSelect, actions->closeFolder, this)); +} + +void MainWidget::mwMultiViewBuild(QList confs, int page) +{ + setView(new VidGrid(shared, confs, shared->playPath, page, actions->liveSelect, actions->closeFolder, this)); +} diff --git a/src/main_widget.h b/src/main_widget.h new file mode 100644 index 0000000..139fccb --- /dev/null +++ b/src/main_widget.h @@ -0,0 +1,67 @@ +#ifndef MAIN_WIDGET_H +#define MAIN_WIDGET_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "vid_widget.h" +#include "vid_grid.h" +#include "actions.h" + +class MainGui : public QMainWindow +{ + Q_OBJECT + +private: + + void closeEvent(QCloseEvent *event); + +public: + + explicit MainGui(QWidget *parent = nullptr); + +signals: + + void killSes(); +}; + +class MainWidget : public QStackedWidget +{ + Q_OBJECT + +private: + + shared_t *shared; + QMenuBar *menuBar; + MainGui *mainGui; + Actions *actions; + QWidget *homepage; + + void mwMultiViewBuild(QList confs, int page); + void mwSingleLiveViewBuild(const QString &path, const QString &camName); + void mwSingleFootageViewBuild(const QString &path, const QString &camName); + void playPathBuild(); + void setView(QWidget *widget); + void addVidWidget(VidWidget *vidWidget); + +private slots: + + void open(); + void reset(); + +public: + + explicit MainWidget(shared_t *share, QMenuBar *mainMenuBar, MainGui *parent, Actions *acts); +}; + +#endif // MAIN_WIDGET_H diff --git a/src/play_control.cpp b/src/play_control.cpp new file mode 100644 index 0000000..8161330 --- /dev/null +++ b/src/play_control.cpp @@ -0,0 +1,146 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "play_control.h" + +PlayControl::PlayControl(PlaylistBE *backend, bool pls, Player *media, QVideoWidget *parent) : QDialog(parent) +{ + auto controlsWid = new QWidget(this); + auto mainLayout = new QVBoxLayout(this); + auto controlsLayout = new QHBoxLayout(controlsWid); + + setModal(true); + + vidName = new QLabel(this); + txtPos = new QLabel(media->getSeekPos(), this); + playBtn = new QToolButton(this); + pauseBtn = new QToolButton(this); + nextBtn = new QToolButton(this); + prevBtn = new QToolButton(this); + plsBtn = new QToolButton(this); + fsBtn = new QToolButton(this); + backBtn = new QToolButton(this); + forwardBtn = new QToolButton(this); + hideTimer = new QTimer(this); + parentVidWid = parent; + + playBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + pauseBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + nextBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + prevBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + plsBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + fsBtn->setToolButtonStyle(Qt::ToolButtonTextOnly); + + playBtn->setText(" >"); + pauseBtn->setText("||"); + nextBtn->setText("->"); + prevBtn->setText("<-"); + plsBtn->setText("List"); + fsBtn->setText("[ ]"); + forwardBtn->setText("->(30)"); + backBtn->setText("(10)<-"); + + hideTimer->setInterval(5000); + hideTimer->setSingleShot(true); + plsBtn->setVisible(pls); + fsBtn->setCheckable(true); + fsBtn->setChecked(isFullScreen()); + + mainLayout->addWidget(vidName); + mainLayout->addWidget(controlsWid); + + auto fnt = vidName->font(); + + fnt.setFamily("Courier"); + fnt.setBold(true); + + vidName->setFont(fnt); + playBtn->setFont(fnt); + pauseBtn->setFont(fnt); + nextBtn->setFont(fnt); + prevBtn->setFont(fnt); + plsBtn->setFont(fnt); + fsBtn->setFont(fnt); + forwardBtn->setFont(fnt); + backBtn->setFont(fnt); + + controlsLayout->addWidget(plsBtn); + controlsLayout->addWidget(prevBtn); + controlsLayout->addWidget(playBtn); + controlsLayout->addWidget(pauseBtn); + controlsLayout->addWidget(nextBtn); + controlsLayout->addWidget(fsBtn); + controlsLayout->addWidget(backBtn); + controlsLayout->addWidget(txtPos); + controlsLayout->addWidget(forwardBtn); + + connect(playBtn, &QToolButton::clicked, backend, &PlaylistBE::play); + connect(pauseBtn, &QToolButton::clicked, backend, &PlaylistBE::pause); + connect(nextBtn, &QToolButton::clicked, backend, &PlaylistBE::next); + connect(prevBtn, &QToolButton::clicked, backend, &PlaylistBE::prev); + + connect(hideTimer, &QTimer::timeout, this, &PlayControl::accept); + connect(plsBtn, &QToolButton::clicked, this, &PlayControl::playlist); + connect(plsBtn, &QToolButton::clicked, this, &PlayControl::accept); + connect(media, &Player::stateChanged, this, &PlayControl::stateChanged); + connect(media, &Player::sourceChanged, this, &PlayControl::vidSrcChanged); + + connect(forwardBtn, &QToolButton::clicked, media, &Player::forward); + connect(backBtn, &QToolButton::clicked, media, &Player::back); + + connect(fsBtn, &QToolButton::clicked, parent, &QVideoWidget::setFullScreen); + + connect(media, &Player::seekPos, txtPos, &QLabel::setText); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + + stateChanged(media->state()); + vidSrcChanged(media->source()); +} + +void PlayControl::leaveEvent(QEvent *event) +{ + Q_UNUSED(event); + + accept(); +} + +void PlayControl::showEvent(QShowEvent *event) +{ + Q_UNUSED(event); + + hideTimer->start(); +} + +void PlayControl::stateChanged(QAVPlayer::State newState) +{ + if (newState == QAVPlayer::PlayingState) + { + playBtn->setVisible(false); + pauseBtn->setVisible(true); + } + else + { + playBtn->setVisible(true); + pauseBtn->setVisible(false); + } +} + +void PlayControl::vidSrcChanged(const QString &path) +{ + vidName->setText(QFileInfo(path).baseName()); +} + +void PlayControl::resetHide() +{ + hideTimer->start(); +} diff --git a/src/play_control.h b/src/play_control.h new file mode 100644 index 0000000..2981410 --- /dev/null +++ b/src/play_control.h @@ -0,0 +1,60 @@ +#ifndef PLAY_CONTROL_H +#define PLAY_CONTROL_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "player.h" +#include "playlist_backend.h" + +class PlayControl : public QDialog +{ + Q_OBJECT + +private: + + QToolButton *playBtn; + QToolButton *pauseBtn; + QToolButton *nextBtn; + QToolButton *prevBtn; + QToolButton *plsBtn; + QToolButton *fsBtn; + QToolButton *forwardBtn; + QToolButton *backBtn; + QVideoWidget *parentVidWid; + QTimer *hideTimer; + QLabel *vidName; + QLabel *txtPos; + + void showEvent(QShowEvent *event); + void leaveEvent(QEvent *event); + +private slots: + + void stateChanged(QAVPlayer::State newState); + void vidSrcChanged(const QString &path); + +public: + + explicit PlayControl(PlaylistBE *backend, bool pls, Player *media, QVideoWidget *parent); + +public slots: + + void resetHide(); + +signals: + + void playlist(); +}; + +#endif // PLAY_CONTROL_H diff --git a/src/player.cpp b/src/player.cpp new file mode 100644 index 0000000..70c9b24 --- /dev/null +++ b/src/player.cpp @@ -0,0 +1,140 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "player.h" + +AudioOut::AudioOut(QObject *parent) : QAVAudioOutput(parent) {} + +void AudioOut::playFrame(const QAVAudioFrame &frame) +{ + play(frame); +} + +FrameFilter::FrameFilter(QObject *parent) : QObject(parent) {} + +Player::Player(QVideoSink *videoSink, bool muted, QWidget *parent) : QAVPlayer(nullptr) +{ + viewWid = parent; + vidSink = videoSink; + audOutput = nullptr; + timer = nullptr; + curPos = 0; + + auto thr = new QThread(nullptr); + + if (!muted) + { + connect(thr, &QThread::started, this, &Player::setupAudio); + } + + connect(thr, &QThread::finished, this, &Player::deleteLater); + connect(thr, &QThread::started, this, &Player::initTimer); + connect(this, &Player::videoFrame, this, &Player::setVideoFrame); + + moveToThread(thr); + + thr->start(); +} + +Player::~Player() +{ + blockSignals(true); +} + +void Player::kill() +{ + thread()->quit(); +} + +void Player::setupAudio() +{ + audOutput = new AudioOut(this); + + connect(this, &Player::audioFrame, audOutput, &AudioOut::playFrame); +} + +void Player::initTimer() +{ + timer = new QTimer(this); + + connect(timer, &QTimer::timeout, this, &Player::timeout); + + timer->start(PULL_RATE); +} + +QString Player::posDurToTxt(qint64 pos, qint64 dur) +{ + auto posTObj = QTime::fromMSecsSinceStartOfDay(pos); + auto durTObj = QTime::fromMSecsSinceStartOfDay(dur); + + return posTObj.toString("HH:mm:ss") + "/" + durTObj.toString("HH:mm:ss"); +} + +QString Player::getSeekPos() +{ + return posDurToTxt(position(), duration()); +} + +void Player::tooglePlayPause() +{ + if (state() == QAVPlayer::PlayingState) + { + pause(); + } + else + { + play(); + } +} + +void Player::setVideoFrame(const QAVVideoFrame &frame) +{ + if (viewWid->isVisible() && vidSink != nullptr) + { + vidSink->setVideoFrame(frame); + } + else + { + stop(); + } +} + +void Player::timeout() +{ + curPos = position(); + + emit seekPos(getSeekPos()); +} + +void Player::addFrameFilter(FrameFilter *filter) +{ + frameFilters.append(filter); +} + +void Player::forward() +{ + seek(curPos + 30000); +} + +void Player::back() +{ + seek(curPos - 10000); +} + +void Player::setFile(const QString &path) +{ + stop(); + setSource(path); + play(); +} + + diff --git a/src/player.h b/src/player.h new file mode 100644 index 0000000..9c1048c --- /dev/null +++ b/src/player.h @@ -0,0 +1,86 @@ +#ifndef PLAYER_H +#define PLAYER_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" + +class AudioOut : public QAVAudioOutput +{ + Q_OBJECT + +public slots: + + void playFrame(const QAVAudioFrame &frame); + +public: + + explicit AudioOut(QObject *parent); +}; + +class FrameFilter : public QObject +{ + Q_OBJECT + +public: + + virtual void procFrame(QVideoFrame *frame, quint64 tPos, quint64 dur) {Q_UNUSED(frame); Q_UNUSED(tPos); Q_UNUSED(dur);} + + explicit FrameFilter(QObject *parent); +}; + +class Player : public QAVPlayer +{ + Q_OBJECT + +private: + + QList frameFilters; + AudioOut *audOutput; + QWidget *viewWid; + QVideoSink *vidSink; + QTimer *timer; + quint64 curPos; + + QString posDurToTxt(qint64 pos, qint64 dur); + +private slots: + + void setVideoFrame(const QAVVideoFrame &frame); + void timeout(); + void initTimer(); + void setupAudio(); + +public slots: + + void setFile(const QString &path); + void tooglePlayPause(); + void kill(); + void forward(); + void back(); + +public: + + void addFrameFilter(FrameFilter *filter); + QString getSeekPos(); + + explicit Player(QVideoSink *videoSink, bool muted, QWidget *parent); + + ~Player(); + +signals: + + void seekPos(const QString &txt); +}; + +#endif // PLAYER_H diff --git a/src/playlist_backend.cpp b/src/playlist_backend.cpp new file mode 100644 index 0000000..42b9eb2 --- /dev/null +++ b/src/playlist_backend.cpp @@ -0,0 +1,239 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "playlist_backend.h" + +PlaylistBE::PlaylistBE(shared_t *share, const QString &path, Player *media, bool frontPlay, QObject *parent) : QObject(nullptr) +{ + Q_UNUSED(parent); + + shared = share; + player = media; + vidDir = QDir::cleanPath(path) + QDir::separator(); + buffer = true; + startOnMoreMedia = false; + vidClipBuffer = 3; + index = 0; + live = frontPlay; + + auto thr = new QThread(nullptr); + + connect(this, &PlaylistBE::playFile, media, &Player::setFile); + connect(this, &PlaylistBE::endOfList, media, &Player::stop); + connect(this, &PlaylistBE::noMedia, media, &Player::stop); + connect(this, &PlaylistBE::buffering, media, &Player::stop); + connect(this, &PlaylistBE::mPlay, media, &Player::play); + connect(this, &PlaylistBE::mStop, media, &Player::stop); + connect(this, &PlaylistBE::mPause, media, &Player::pause); + + connect(thr, &QThread::finished, this, &PlaylistBE::deleteLater); + connect(thr, &QThread::started, this, &PlaylistBE::init); + + connect(media, &Player::mediaStatusChanged, this, &PlaylistBE::mediaStatusChanged); + connect(media, &Player::stateChanged, this, &PlaylistBE::stateChanged); + + moveToThread(thr); + + thr->start(); +} + +void PlaylistBE::kill() +{ + thread()->terminate(); +} + +void PlaylistBE::init() +{ + timer = new QTimer(this); + + timer->setInterval(1000); + timer->start(); + + connect(timer, &QTimer::timeout, this, &PlaylistBE::scan); +} + +void PlaylistBE::buffOrEnd() +{ + if (live) + { + buffer = true; + + emit buffering(); + } + else + { + emit endOfList(); + } +} + +QString PlaylistBE::seek(char direction) +{ + auto ret = QString(); + auto newInd = index; + auto max = fileList.size() - 1; + + if (direction == '+') newInd = index + 1; + else newInd = index - 1; + + if (fileList.isEmpty()) + { + emit noMedia(); + + if (!live) + { + startOnMoreMedia = true; + } + } + else if (newInd < 0) + { + ret = vidDir + fileList[0]; + } + else if (newInd <= max) + { + index = newInd; + ret = vidDir + fileList[index]; + } + else + { + buffOrEnd(); + } + + return ret; +} + +void PlaylistBE::list() +{ + for (auto i = 0; i < fileList.size(); ++i) + { + emit newFile(vidDir + fileList[i]); + } +} + +void PlaylistBE::next() +{ + emit playFile(seek('+')); +} + +void PlaylistBE::prev() +{ + emit playFile(seek('-')); +} + +void PlaylistBE::start() +{ + if (!fileList.isEmpty()) + { + index = 0; + + emit playFile(vidDir + fileList[index]); + } + else + { + startOnMoreMedia = true; emit noMedia(); + } +} + +void PlaylistBE::front() +{ + if (fileList.size() >= vidClipBuffer) + { + index = fileList.size() - vidClipBuffer; + buffer = false; + + emit playFile(vidDir + fileList[index]); + } + else + { + buffOrEnd(); + } +} + +void PlaylistBE::play() +{ + if (player->state() == QAVPlayer::PausedState) + { + emit mPlay(); + } + else if (player->source().isEmpty()) + { + next(); + } +} + +void PlaylistBE::pause() +{ + emit mPause(); +} + +void PlaylistBE::stop() +{ + emit mStop(); +} + +void PlaylistBE::scan() +{ + for (auto &&file : fileList) + { + if (!QFileInfo::exists(vidDir + file)) + { + emit fileDel(vidDir + file); + + fileList.removeOne(file); + + index--; + } + } + + auto newVids = lsVidFiles(vidDir); + + for (auto i = 0; i < newVids.size(); ++i) + { + if (!fileList.contains(newVids[i])) + { + emit newFile(vidDir + newVids[i]); + + fileList.append(newVids[i]); + } + } + + if (!fileList.isEmpty()) + { + if (startOnMoreMedia) + { + startOnMoreMedia = false; + + start(); + } + else if (buffer && live) + { + buffer = false; + + next(); + } + } +} + +void PlaylistBE::mediaStatusChanged(QAVPlayer::MediaStatus status) +{ + if (status == QAVPlayer::EndOfMedia || status == QAVPlayer::InvalidMedia) + { + next(); + } +} + +void PlaylistBE::stateChanged(QAVPlayer::State newState) +{ + if (newState == QAVPlayer::PlayingState) + { + emit playbackResumed(); + } +} diff --git a/src/playlist_backend.h b/src/playlist_backend.h new file mode 100644 index 0000000..69fe31f --- /dev/null +++ b/src/playlist_backend.h @@ -0,0 +1,77 @@ +#ifndef PLAYLIST_BACKEND_H +#define PLAYLIST_BACKEND_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "player.h" + +class PlaylistBE : public QObject +{ + Q_OBJECT + +private: + + QStringList fileList; + QString vidDir; + QTimer *timer; + shared_t *shared; + Player *player; + bool startOnMoreMedia; + bool buffer; + bool live; + int vidClipBuffer; + int sliderPos; + int index; + + QString seek(char direction); + void buffOrEnd(); + +private slots: + + void scan(); + void init(); + void mediaStatusChanged(QAVPlayer::MediaStatus status); + void stateChanged(QAVPlayer::State newState); + +public slots: + + void next(); + void front(); + void prev(); + void list(); + void start(); + void play(); + void pause(); + void stop(); + void kill(); + +public: + + explicit PlaylistBE(shared_t *share, const QString &path, Player *media, bool frontPlay, QObject *parent = nullptr); + +signals: + + void fileDel(const QString &path); + void newFile(const QString &path); + void playFile(const QString &path); + void playbackResumed(); + void buffering(); + void noMedia(); + void endOfList(); + void mPlay(); + void mPause(); + void mStop(); +}; + +#endif // PLAYLIST_BACKEND_H diff --git a/src/playlist_widget.cpp b/src/playlist_widget.cpp new file mode 100644 index 0000000..01b237f --- /dev/null +++ b/src/playlist_widget.cpp @@ -0,0 +1,198 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "playlist_widget.h" + +PlaylistItem::PlaylistItem(const QString &filePath, ImgLoader *img, QWidget *parent) : QLabel(parent) +{ + imgLoader = img; + vidFile = filePath; + popMsg = new QDialog(this, Qt::Window | Qt::FramelessWindowHint | Qt::Tool); + popMsgText = new QLabel(popMsg); + + popMsg->hide(); + popMsg->setModal(false); + popMsg->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + auto popLayout = new QVBoxLayout(popMsg); + + popLayout->addWidget(popMsgText, 0, Qt::AlignCenter); + + setCursor(Qt::PointingHandCursor); + setFixedWidth(200); + setFixedHeight(180); + + genImgAndText(); + setMouseTracking(true); +} + +bool PlaylistItem::findImg(QString &pathWithoutExt) +{ + const QStringList imgExts = {".jpg", ".png", ".jpeg", ".bmp", ".gif"}; + + for (auto &&ext : imgExts) + { + if (QFileInfo::exists(pathWithoutExt + ext)) + { + pathWithoutExt += ext; return true; + } + } + + return false; +} + +void PlaylistItem::genImgAndText() +{ + auto info = QFileInfo(vidFile); + auto name = info.baseName(); + auto dir = info.path(); + auto imgA = dir + QDir::separator() + name; + auto imgB = dir + QDir::separator() + ".." + QDir::separator() + "img" + QDir::separator() + name; + + if (findImg(imgA)) imgFile = imgA; + else if (findImg(imgB)) imgFile = imgB; + else imgFile = QString(IMG_FROM_VID) + vidFile; + + popMsgText->setText(name); + imgLoader->addImg(imgFile, this); +} + +void PlaylistItem::leaveEvent(QEvent *event) +{ + Q_UNUSED(event); + + popMsg->hide(); +} + +void PlaylistItem::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event); + + dispText(); +} + +void PlaylistItem::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + + emit playItemSelected(vidFile); +} + +void PlaylistItem::rmVid(const QString &path) +{ + if (path == vidFile) + { + emit aboutToDelete(this); + + deleteLater(); + } +} + +void PlaylistItem::dispText() +{ + auto pnt = mapToGlobal(QPointF(0, 180)); + + popMsg->move(pnt.x(), pnt.y()); + popMsg->show(); +} + +PlaylistWidget::PlaylistWidget(shared_t *share, PlaylistBE *backend, Player *media, QAction *closeAction, QWidget *parent) : QScrollArea(nullptr) +{ + Q_UNUSED(parent); + + auto listWidget = new QWidget(this); + + player = media; + bkend = backend; + shared = share; + imgLoader = new ImgLoader(this); + listLayout = new QGridLayout(listWidget); + + listWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + listWidget->setContentsMargins(0,0,0,0); + + connect(backend, &PlaylistBE::newFile, this, &PlaylistWidget::addVid); + connect(closeAction, &QAction::triggered, this, &PlaylistWidget::closeWid); + + setWindowFlag(Qt::Tool, true); + setWidget(listWidget); + setWidgetResizable(true); + setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + setFrameStyle(QFrame::NoFrame); + + setGeometry(share->plistX, share->plistY, share->plistW, share->plistH); +} + +PlaylistWidget::~PlaylistWidget() +{ + auto curRect = geometry(); + + shared->plistX = curRect.x(); + shared->plistY = curRect.y(); + shared->plistW = curRect.width(); + shared->plistH = curRect.height(); +} + +void PlaylistWidget::closeWid() +{ + imgLoader->blockSignals(true); + imgLoader->clear(); + + deleteLater(); +} + +void PlaylistWidget::rmVid(const QString &path) +{ + fileList.removeOne(path); +} + +void PlaylistWidget::reGrid() +{ + buildGrid(listLayout, width(), widList, 200); +} + +void PlaylistWidget::rmItem(const QWidget *item) +{ + widList.removeOne(item); reGrid(); +} + +void PlaylistWidget::addVid(const QString &path) +{ + if (!fileList.contains(path)) + { + auto playItem = new PlaylistItem(path, imgLoader, this); + + connect(playItem, &PlaylistItem::playItemSelected, player, &Player::setFile); + connect(playItem, &PlaylistItem::aboutToDelete, this, &PlaylistWidget::rmItem); + connect(bkend, &PlaylistBE::fileDel, playItem, &PlaylistItem::rmVid); + + fileList.append(path); + widList.append(playItem); + + reGrid(); + } +} + +void PlaylistWidget::showEvent(QShowEvent *event) +{ + reGrid(); QScrollArea::showEvent(event); +} + +void PlaylistWidget::resizeEvent(QResizeEvent *event) +{ + reGrid(); QScrollArea::resizeEvent(event); +} + +void PlaylistWidget::closeEvent(QCloseEvent *event) +{ + Q_UNUSED(event); hide(); emit closed(false); +} diff --git a/src/playlist_widget.h b/src/playlist_widget.h new file mode 100644 index 0000000..52b612a --- /dev/null +++ b/src/playlist_widget.h @@ -0,0 +1,94 @@ +#ifndef PLAYLIST_WIDGET_H +#define PLAYLIST_WIDGET_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "img_loader.h" +#include "player.h" +#include "playlist_backend.h" + +class PlaylistItem : public QLabel +{ + Q_OBJECT + +private: + + ImgLoader *imgLoader; + QLabel *image; + QLabel *title; + QDialog *popMsg; + QLabel *popMsgText; + QString vidFile; + QString imgFile; + + void mouseReleaseEvent(QMouseEvent *event); + void leaveEvent(QEvent *event); + void enterEvent(QEnterEvent *event); + void genImgAndText(); + void dispText(); + bool findImg(QString &pathWithoutExt); + +public slots: + + void rmVid(const QString &path); + +public: + + explicit PlaylistItem(const QString &filePath, ImgLoader *img, QWidget *parent); + +signals: + + void playItemSelected(const QString &fileUrl); + void aboutToDelete(const QWidget *item); +}; + +class PlaylistWidget : public QScrollArea +{ + Q_OBJECT + +private: + + shared_t *shared; + Player *player; + QGridLayout *listLayout; + ImgLoader *imgLoader; + PlaylistBE *bkend; + QStringList fileList; + QList widList; + + void reGrid(); + void showEvent(QShowEvent *event); + void closeEvent(QCloseEvent *event); + void resizeEvent(QResizeEvent *event); + +private slots: + + void addVid(const QString &path); + void rmVid(const QString &path); + void rmItem(const QWidget *item); + void closeWid(); + +public: + + explicit PlaylistWidget(shared_t *share, PlaylistBE *backend, Player *media, QAction *closeAction, QWidget *parent); + + ~PlaylistWidget(); + +signals: + + void list(); + void closed(bool checked); +}; + +#endif // PLAYLIST_WIDGET_H diff --git a/src/pref_dialog.cpp b/src/pref_dialog.cpp new file mode 100644 index 0000000..06e0d58 --- /dev/null +++ b/src/pref_dialog.cpp @@ -0,0 +1,66 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "pref_dialog.h" + +PrefDialog::PrefDialog(shared_t *conf, QWidget *parent) : QDialog(parent) +{ + shared = conf; + + auto mainLayout = new QVBoxLayout(this); + auto formWidget = new QWidget(this); + auto formLayout = new QFormLayout(formWidget); + auto btnWidget = new QWidget(this); + auto btnLayout = new QHBoxLayout(btnWidget); + auto okBtn = new QPushButton(this); + auto cancelBtn = new QPushButton(this); + + maxRecents = new QSpinBox(this); + maxViewsPerPage = new QSpinBox(this); + + formLayout->addRow(new QLabel(tr("Max Recents:"), this), maxRecents); + formLayout->addRow(new QLabel(tr("Max Views Per Page:"), this), maxViewsPerPage); + + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); + + mainLayout->addWidget(formWidget, 0, Qt::AlignHCenter); + mainLayout->addWidget(btnWidget, 0, Qt::AlignHCenter); + + okBtn->setText("Ok"); + cancelBtn->setText("Cancel"); + + connect(okBtn, &QPushButton::clicked, this, &PrefDialog::preAccept); + connect(cancelBtn, &QPushButton::clicked, this, &PrefDialog::reject); + + rd(); +} + +void PrefDialog::rd() +{ + maxRecents->setValue(shared->maxRecents); + maxViewsPerPage->setValue(shared->maxViewsPerPage); +} + +void PrefDialog::wr() +{ + shared->maxRecents = maxRecents->value(); + shared->maxViewsPerPage = maxViewsPerPage->value(); + + wrConf(shared->appConfPath, shared); +} + +void PrefDialog::preAccept() +{ + wr(); accept(); +} + diff --git a/src/pref_dialog.h b/src/pref_dialog.h new file mode 100644 index 0000000..ad68997 --- /dev/null +++ b/src/pref_dialog.h @@ -0,0 +1,40 @@ +#ifndef PREF_DIALOG_H +#define PREF_DIALOG_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" + +class PrefDialog : public QDialog +{ + Q_OBJECT + +private: + + shared_t *shared; + QSpinBox *maxRecents; + QSpinBox *maxViewsPerPage; + + void rd(); + void wr(); + +private slots: + + void preAccept(); + +public: + + explicit PrefDialog(shared_t *conf, QWidget *parent = nullptr); +}; + +#endif // PREF_DIALOG_H diff --git a/src/recents.cpp b/src/recents.cpp new file mode 100644 index 0000000..40a262f --- /dev/null +++ b/src/recents.cpp @@ -0,0 +1,68 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "recents.h" + +RecentItem::RecentItem(const QString &path, Actions *acts, QObject *parent) : QObject(parent) +{ + confPath = path; + actions = acts; + itemAction = new QAction(genItemName(), this); + + actions->recents->insertAction(actions->recentsSpacer, itemAction); + + connect(itemAction, &QAction::triggered, this, &RecentItem::triggered); +} + +QString RecentItem::genItemName() +{ + QString ret = "Null"; + shared_t conf; + + if (rdConf(confPath, &conf)) + { + ret = conf.playPath; + } + + return ret; +} + +void RecentItem::triggered() +{ + actions->openConf(confPath); +} + +RecentsSync::RecentsSync(shared_t *sharedRes, Actions *acts, QObject *parent) : QObject(parent) +{ + actions = acts; + share = sharedRes; + + mon.addPath(share->recentsPath); + + connect(&mon, &QFileSystemWatcher::directoryChanged, this, &RecentsSync::scan); + + scan(); +} + +void RecentsSync::scan() +{ + emit sync(); + + actions->rebuildRecents(); + + for (auto &&file: lsConfFiles(share->recentsPath)) + { + auto item = new RecentItem(share->recentsPath + QDir::separator() + file, actions, this); + + connect(this, &RecentsSync::sync, item, &RecentItem::deleteLater); + } +} diff --git a/src/recents.h b/src/recents.h new file mode 100644 index 0000000..ebd18c5 --- /dev/null +++ b/src/recents.h @@ -0,0 +1,63 @@ +#ifndef RECENTS_H +#define RECENTS_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "actions.h" + +class RecentItem : public QObject +{ + Q_OBJECT + +private: + + QString confPath; + QAction *itemAction; + Actions *actions; + + QString genItemName(); + +private slots: + + void triggered(); + +public: + + explicit RecentItem(const QString &path, Actions *acts, QObject *parent = nullptr); +}; + +class RecentsSync : public QObject +{ + Q_OBJECT + +private: + + QFileSystemWatcher mon; + shared_t *share; + Actions *actions; + +private slots: + + void scan(); + +public: + + explicit RecentsSync(shared_t *sharedRes, Actions *acts, QObject *parent = nullptr); + +signals: + + void sync(); +}; + +#endif // RECENTS_H diff --git a/src/vid_grid.cpp b/src/vid_grid.cpp new file mode 100644 index 0000000..9c49472 --- /dev/null +++ b/src/vid_grid.cpp @@ -0,0 +1,91 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "vid_grid.h" + +VidGrid::VidGrid(shared_t *share, QList confs, const QString &mntLoc, int pageNum, QMenu *camSelect, QAction *closeAction, QStackedWidget *parent) : QWidget(parent) +{ + mainGui = parent; + widCount = 0; + viewIndex = parent->addWidget(this); + mainLayout = new QGridLayout(this); + menuOption = camSelect->addAction(tr("Grid - Page") + QString::number(pageNum), this, &VidGrid::viewSelected); + + mainLayout->setSpacing(0); + + for (auto &&conf: confs) + { + auto vid = new VidWidget(share, conf.camName, QDir::cleanPath(mntLoc + QDir::separator() + conf.buffPath + QDir::separator() + "live"), true, false, nullptr, closeAction, parent); + + connect(this, &VidGrid::stop, vid, &VidWidget::stop); + connect(this, &VidGrid::front, vid, &VidWidget::front); + + connect(vid, &VidWidget::destroyed, this, &VidGrid::widCountMinusOne); + + widList.append(vid); + + widCount++; + } + + connect(closeAction, &QAction::triggered, this, &VidGrid::closeWid); + + setContentsMargins(0,0,0,0); + connect(parent, &QStackedWidget::currentChanged, this, &VidGrid::viewChanged); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + buildGrid(mainLayout, parent->width(), widList, 500); +} + +void VidGrid::widCountMinusOne() +{ + widCount--; + + if (widCount == 0) + { + deleteLater(); + } +} + +void VidGrid::closeWid() +{ + widList.clear(); +} + +void VidGrid::viewSelected() +{ + mainGui->setCurrentWidget(this); +} + +void VidGrid::viewChanged(int index) +{ + if (!widList.isEmpty()) + { + if (viewIndex == index) + { + emit front(); + + menuOption->setIcon(QIcon("://icons/check.png")); + } + else + { + emit stop(); + + menuOption->setIcon(QIcon()); + } + } +} + +void VidGrid::resizeEvent(QResizeEvent *event) +{ + buildGrid(mainLayout, mainGui->width(), widList, 500); + + QWidget::resizeEvent(event); +} diff --git a/src/vid_grid.h b/src/vid_grid.h new file mode 100644 index 0000000..42b14ea --- /dev/null +++ b/src/vid_grid.h @@ -0,0 +1,51 @@ +#ifndef VID_GRID_H +#define VID_GRID_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "vid_widget.h" + +class VidGrid : public QWidget +{ + Q_OBJECT + +private: + + QList widList; + QGridLayout *mainLayout; + QStackedWidget *mainGui; + QAction *menuOption; + int widCount; + int viewIndex; + + void resizeEvent(QResizeEvent *event); + +private slots: + + void viewSelected(); + void closeWid(); + void widCountMinusOne(); + void viewChanged(int index); + +public: + + explicit VidGrid(shared_t *share, QList confs, const QString &mntLoc, int pageNum, QMenu *camSelect, QAction *closeAction, QStackedWidget *parent); + +signals: + + void stop(); + void front(); +}; + +#endif // VID_GRID_H diff --git a/src/vid_widget.cpp b/src/vid_widget.cpp new file mode 100644 index 0000000..f5a7673 --- /dev/null +++ b/src/vid_widget.cpp @@ -0,0 +1,263 @@ +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "vid_widget.h" + +VidWidget::VidWidget(shared_t *share, const QString &camName, const QString &playpath, bool multiLive, bool footage, QMenu *camSelect, QAction *closeAction, QStackedWidget *parent) : QVideoWidget(parent) +{ + mainGui = parent; + shared = share; + live = multiLive || !footage; + inGrid = multiLive; + menuOption = nullptr; + playlistWid = nullptr; + ctrlOpen = false; + objCount = 2; + + popMsg = new QDialog(this, Qt::Window | Qt::FramelessWindowHint | Qt::Tool); + popMsgText = new QLabel(popMsg); + player = new Player(videoSink(), multiLive, this); + playlistBE = new PlaylistBE(share, playpath, player, live, this); + + if (multiLive) + { + //setCursor(Qt::PointingHandCursor); + } + else + { + if (camSelect != nullptr) + { + menuOption = camSelect->addAction(camName, this, &VidWidget::viewSelected); + } + + viewIndex = parent->addWidget(this); + + if (footage) + { + playlistWid = new PlaylistWidget(share, playlistBE, player, closeAction, this); + + playlistWid->hide(); + } + + connect(parent, &QStackedWidget::currentChanged, this, &VidWidget::viewChanged); + + connect(this, &VidWidget::back, player, &Player::back); + connect(this, &VidWidget::forward, player, &Player::forward); + connect(this, &VidWidget::togglePlay, player, &Player::tooglePlayPause); + + installEventFilter(this); + } + + popMsg->hide(); + popMsg->setModal(false); + popMsg->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + auto popLayout = new QVBoxLayout(popMsg); + + popLayout->addWidget(popMsgText, 0, Qt::AlignCenter); + + connect(this, &VidWidget::start, playlistBE, &PlaylistBE::start); + connect(this, &VidWidget::front, playlistBE, &PlaylistBE::front); + connect(this, &VidWidget::stop, player, &Player::stop); + connect(this, &VidWidget::fullScreenChanged, this, &VidWidget::hideMain); + + connect(playlistBE, &PlaylistBE::buffering, this, &VidWidget::buffering); + connect(playlistBE, &PlaylistBE::endOfList, this, &VidWidget::endOfList); + connect(playlistBE, &PlaylistBE::playbackResumed, this, &VidWidget::resumed); + + connect(closeAction, &QAction::triggered, player, &Player::kill); + connect(closeAction, &QAction::triggered, playlistBE, &PlaylistBE::kill); + + connect(player, &Player::destroyed, this, &VidWidget::objKilled); + connect(playlistBE, &PlaylistBE::destroyed, this, &VidWidget::objKilled); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setContentsMargins(0,0,0,0); +} + +QPoint VidWidget::getCtrlPopPnt() +{ + return mapToGlobal(QPoint(40, height() - 100)); +} + +void VidWidget::keyPressEvent(QKeyEvent *event) +{ + if (isVisible() && !inGrid) + { + if (event->key() == Qt::Key_Left) + { + emit back(); + } + else if (event->key() == Qt::Key_Right) + { + emit forward(); + } + else if (event->key() == Qt::Key_Space) + { + emit togglePlay(); + } + else if (event->key() == Qt::Key_Escape) + { + setFullScreen(false); + } + } +} + +void VidWidget::popCtrl(const QPointF &pnt) +{ + if (ctrlOpen) + { + emit extendControls(); + } + else + { + auto controlsWid = new PlayControl(playlistBE, playlistWid != nullptr, player, this); + + if (playlistWid != nullptr) + { + connect(controlsWid, &PlayControl::playlist, playlistWid, &PlaylistWidget::show); + } + + connect(controlsWid, &PlayControl::accepted, this, &VidWidget::controlsClosed); + + connect(this, &VidWidget::closeControls, controlsWid, &PlayControl::accept); + connect(this, &VidWidget::extendControls, controlsWid, &PlayControl::resetHide); + + controlsWid->move(pnt.x(), pnt.y()); + controlsWid->show(); + + ctrlOpen = true; + } +} + +bool VidWidget::eventFilter(QObject *obj, QEvent *event) +{ + Q_UNUSED(obj); + + if (isVisible() && !inGrid) + { + if (event->type() == QEvent::MouseMove) + { + auto pnt = getCtrlPopPnt(); + auto actRect = QRect(pnt, QSize(300, 100)); + auto curPnt = QCursor::pos(); + + if (actRect.contains(curPnt)) + { + popCtrl(pnt); + } + } + } + + return false; +} + +void VidWidget::controlsClosed() +{ + ctrlOpen = false; +} + +void VidWidget::hideMain(bool fsState) +{ + if (fsState) + { + emit closeControls(); + } + + emit mainWindowsVis(!fsState); +} + +void VidWidget::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event) + + emit togglePlay(); +} + +void VidWidget::objKilled() +{ + objCount--; + + if (objCount == 0) + { + mainGui->removeWidget(this); + + deleteLater(); + } +} + +void VidWidget::viewSelected() +{ + mainGui->setCurrentWidget(this); +} + +void VidWidget::viewChanged(int index) +{ + if (viewIndex == index) + { + if (live) emit front(); + else emit start(); + + if (menuOption != nullptr) + { + menuOption->setIcon(QIcon("://icons/check.png")); + } + } + else + { + emit stop(); + + if (menuOption != nullptr) + { + menuOption->setIcon(QIcon()); + } + + if (playlistWid != nullptr) + { + playlistWid->hide(); + } + + popMsg->hide(); + } +} + +void VidWidget::dispMsg(const QString &txt) +{ + if (isVisible()) + { + auto pnt = mapToGlobal(QPointF(10, 10)); + + popMsgText->setText(txt); + popMsg->move(pnt.x(), pnt.y()); + popMsg->show(); + } +} + +void VidWidget::resumed() +{ + popMsg->hide(); +} + +void VidWidget::buffering() +{ + dispMsg(tr("Buffering")); +} + +void VidWidget::endOfList() +{ + dispMsg(tr("End-Of-List")); +} + +void VidWidget::noMedia() +{ + dispMsg(tr("No-Media")); +} diff --git a/src/vid_widget.h b/src/vid_widget.h new file mode 100644 index 0000000..657af0b --- /dev/null +++ b/src/vid_widget.h @@ -0,0 +1,80 @@ +#ifndef VID_WIDGET_H +#define VID_WIDGET_H + +// This file is part of JustVideo. + +// JustVideo 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. + +// JustVideo 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. + +#include "common.h" +#include "playlist_widget.h" +#include "playlist_backend.h" +#include "play_control.h" +#include "player.h" + +class VidWidget : public QVideoWidget +{ + Q_OBJECT + +private: + + shared_t *shared; + QAction *menuOption; + QStackedWidget *mainGui; + PlaylistBE *playlistBE; + PlaylistWidget *playlistWid; + Player *player; + QLabel *txtPos; + QLabel *vidName; + QLabel *popMsgText; + QDialog *popMsg; + bool inGrid; + bool live; + bool ctrlOpen; + int viewIndex; + int objCount; + + QPoint getCtrlPopPnt(); + void dispMsg(const QString &txt); + void mouseReleaseEvent(QMouseEvent *event); + void keyPressEvent(QKeyEvent *event); + bool eventFilter(QObject *obj, QEvent *event); + +private slots: + + void viewSelected(); + void viewChanged(int index); + void buffering(); + void endOfList(); + void noMedia(); + void resumed(); + void objKilled(); + void controlsClosed(); + void popCtrl(const QPointF &pnt); + void hideMain(bool fsState); + +public: + + explicit VidWidget(shared_t *share, const QString &camName, const QString &playpath, bool multiLive, bool footage, QMenu *camSelect, QAction *closeAction, QStackedWidget *parent); + +signals: + + void start(); + void stop(); + void front(); + void back(); + void forward(); + void togglePlay(); + void closeControls(); + void extendControls(); + void mainWindowsVis(bool); +}; + +#endif // VID_WIDGET_H diff --git a/templates/linux_icon.desktop b/templates/linux_icon.desktop new file mode 100644 index 0000000..784c944 --- /dev/null +++ b/templates/linux_icon.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=$app_name +GenericName=Binge Friendly Video Player +Comment=Play all videos inside a folder +Exec=$app_target +Icon=$app_target +Terminal=false +Type=Application +Categories=AudioVideo;Video;Player;TV; + diff --git a/templates/linux_run_script.sh b/templates/linux_run_script.sh new file mode 100644 index 0000000..8c63cfd --- /dev/null +++ b/templates/linux_run_script.sh @@ -0,0 +1,5 @@ +#!/bin/sh +export QTDIR=$install_dir +export QT_PLUGIN_PATH=$install_dir +export LD_LIBRARY_PATH=$install_dir/lib +$install_dir/$app_target $1 $2 $3 diff --git a/templates/linux_uninstall.sh b/templates/linux_uninstall.sh new file mode 100644 index 0000000..4e0c959 --- /dev/null +++ b/templates/linux_uninstall.sh @@ -0,0 +1,22 @@ +#!/bin/sh +rm -v /usr/bin/$app_target +rm -rv $install_dir +rm -v /usr/share/icons/hicolor/8x8/apps/$app_target.png +rm -v /usr/share/icons/hicolor/16x16/apps/$app_target.png +rm -v /usr/share/icons/hicolor/22x22/apps/$app_target.png +rm -v /usr/share/icons/hicolor/24x24/apps/$app_target.png +rm -v /usr/share/icons/hicolor/28x28/apps/$app_target.png +rm -v /usr/share/icons/hicolor/32x32/apps/$app_target.png +rm -v /usr/share/icons/hicolor/36x36/apps/$app_target.png +rm -v /usr/share/icons/hicolor/42x42/apps/$app_target.png +rm -v /usr/share/icons/hicolor/48x48/apps/$app_target.png +rm -v /usr/share/icons/hicolor/64x64/apps/$app_target.png +rm -v /usr/share/icons/hicolor/72x72/apps/$app_target.png +rm -v /usr/share/icons/hicolor/96x96/apps/$app_target.png +rm -v /usr/share/icons/hicolor/128x128/apps/$app_target.png +rm -v /usr/share/icons/hicolor/192x192/apps/$app_target.png +rm -v /usr/share/icons/hicolor/256x256/apps/$app_target.png +rm -v /usr/share/icons/hicolor/512x512/apps/$app_target.png +rm -v /usr/share/icons/hicolor/scalable/apps/$app_target.svg +rm -v /usr/share/applications/$app_target.desktop +echo "Uninstallation Complete"