From ef90c77e8a47616a62e19df51fffe04d06610684 Mon Sep 17 00:00:00 2001 From: zii Date: Tue, 6 Aug 2024 08:28:43 -0400 Subject: [PATCH] v1.0 -first ccommit for stable version of JustVideo --- .gitignore | 57 + JustVideo.pro | 56 + LICENSE.md | 621 ++++++++ QtAVPlayer/LICENSE | 21 + QtAVPlayer/README.md | 218 +++ .../extract_frames/extract_frames.pro | 13 + QtAVPlayer/examples/extract_frames/main.cpp | 30 + QtAVPlayer/examples/qml_video/main.cpp | 204 +++ QtAVPlayer/examples/qml_video/main.qml | 80 + QtAVPlayer/examples/qml_video/main_qt6.qml | 80 + QtAVPlayer/examples/qml_video/qml.qrc | 6 + QtAVPlayer/examples/qml_video/qml_qt6.qrc | 6 + QtAVPlayer/examples/qml_video/qml_video.pro | 25 + QtAVPlayer/examples/widget_video/main.cpp | 136 ++ .../examples/widget_video/widget_video.pro | 14 + QtAVPlayer/src/QtAVPlayer/QtAVPlayer.pri | 131 ++ .../QtAVPlayer/qavandroidsurfacetexture.cpp | 74 + .../QtAVPlayer/qavandroidsurfacetexture_p.h | 61 + QtAVPlayer/src/QtAVPlayer/qavaudiocodec.cpp | 49 + QtAVPlayer/src/QtAVPlayer/qavaudiocodec_p.h | 39 + QtAVPlayer/src/QtAVPlayer/qavaudiofilter.cpp | 155 ++ QtAVPlayer/src/QtAVPlayer/qavaudiofilter_p.h | 53 + QtAVPlayer/src/QtAVPlayer/qavaudioformat.h | 61 + QtAVPlayer/src/QtAVPlayer/qavaudioframe.cpp | 216 +++ QtAVPlayer/src/QtAVPlayer/qavaudioframe.h | 41 + .../src/QtAVPlayer/qavaudioinputfilter.cpp | 127 ++ .../src/QtAVPlayer/qavaudioinputfilter_p.h | 45 + QtAVPlayer/src/QtAVPlayer/qavaudiooutput.cpp | 305 ++++ QtAVPlayer/src/QtAVPlayer/qavaudiooutput.h | 47 + .../src/QtAVPlayer/qavaudiooutputfilter.cpp | 43 + .../src/QtAVPlayer/qavaudiooutputfilter_p.h | 38 + QtAVPlayer/src/QtAVPlayer/qavcodec.cpp | 99 ++ QtAVPlayer/src/QtAVPlayer/qavcodec_p.h | 62 + QtAVPlayer/src/QtAVPlayer/qavcodec_p_p.h | 41 + QtAVPlayer/src/QtAVPlayer/qavdemuxer.cpp | 876 +++++++++++ QtAVPlayer/src/QtAVPlayer/qavdemuxer_p.h | 114 ++ QtAVPlayer/src/QtAVPlayer/qavfilter.cpp | 31 + QtAVPlayer/src/QtAVPlayer/qavfilter_p.h | 54 + QtAVPlayer/src/QtAVPlayer/qavfilter_p_p.h | 48 + QtAVPlayer/src/QtAVPlayer/qavfiltergraph.cpp | 186 +++ QtAVPlayer/src/QtAVPlayer/qavfiltergraph_p.h | 64 + QtAVPlayer/src/QtAVPlayer/qavfilters.cpp | 220 +++ QtAVPlayer/src/QtAVPlayer/qavfilters_p.h | 65 + QtAVPlayer/src/QtAVPlayer/qavframe.cpp | 121 ++ QtAVPlayer/src/QtAVPlayer/qavframe.h | 41 + QtAVPlayer/src/QtAVPlayer/qavframe_p.h | 48 + QtAVPlayer/src/QtAVPlayer/qavframecodec.cpp | 46 + QtAVPlayer/src/QtAVPlayer/qavframecodec_p.h | 41 + .../src/QtAVPlayer/qavhwdevice_d3d11.cpp | 225 +++ .../src/QtAVPlayer/qavhwdevice_d3d11_p.h | 44 + .../src/QtAVPlayer/qavhwdevice_mediacodec.cpp | 115 ++ .../src/QtAVPlayer/qavhwdevice_mediacodec_p.h | 47 + QtAVPlayer/src/QtAVPlayer/qavhwdevice_p.h | 51 + .../QtAVPlayer/qavhwdevice_vaapi_drm_egl.cpp | 163 ++ .../QtAVPlayer/qavhwdevice_vaapi_drm_egl_p.h | 46 + .../QtAVPlayer/qavhwdevice_vaapi_x11_glx.cpp | 180 +++ .../QtAVPlayer/qavhwdevice_vaapi_x11_glx_p.h | 46 + .../src/QtAVPlayer/qavhwdevice_vdpau.cpp | 235 +++ .../src/QtAVPlayer/qavhwdevice_vdpau_p.h | 42 + .../QtAVPlayer/qavhwdevice_videotoolbox.mm | 112 ++ .../QtAVPlayer/qavhwdevice_videotoolbox_p.h | 46 + QtAVPlayer/src/QtAVPlayer/qavinoutfilter.cpp | 64 + QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p.h | 50 + .../src/QtAVPlayer/qavinoutfilter_p_p.h | 41 + QtAVPlayer/src/QtAVPlayer/qaviodevice.cpp | 173 ++ QtAVPlayer/src/QtAVPlayer/qaviodevice.h | 41 + QtAVPlayer/src/QtAVPlayer/qavpacket.cpp | 108 ++ QtAVPlayer/src/QtAVPlayer/qavpacket_p.h | 58 + QtAVPlayer/src/QtAVPlayer/qavpacketqueue_p.h | 275 ++++ QtAVPlayer/src/QtAVPlayer/qavplayer.cpp | 1399 +++++++++++++++++ QtAVPlayer/src/QtAVPlayer/qavplayer.h | 162 ++ QtAVPlayer/src/QtAVPlayer/qavstream.cpp | 285 ++++ QtAVPlayer/src/QtAVPlayer/qavstream.h | 83 + QtAVPlayer/src/QtAVPlayer/qavstreamframe.cpp | 81 + QtAVPlayer/src/QtAVPlayer/qavstreamframe.h | 45 + QtAVPlayer/src/QtAVPlayer/qavstreamframe_p.h | 41 + .../src/QtAVPlayer/qavsubtitlecodec.cpp | 60 + .../src/QtAVPlayer/qavsubtitlecodec_p.h | 43 + .../src/QtAVPlayer/qavsubtitleframe.cpp | 83 + QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.h | 35 + .../src/QtAVPlayer/qavvideobuffer_cpu.cpp | 38 + .../src/QtAVPlayer/qavvideobuffer_cpu_p.h | 38 + .../src/QtAVPlayer/qavvideobuffer_gpu.cpp | 34 + .../src/QtAVPlayer/qavvideobuffer_gpu_p.h | 42 + QtAVPlayer/src/QtAVPlayer/qavvideobuffer_p.h | 45 + QtAVPlayer/src/QtAVPlayer/qavvideocodec.cpp | 147 ++ QtAVPlayer/src/QtAVPlayer/qavvideocodec_p.h | 44 + QtAVPlayer/src/QtAVPlayer/qavvideofilter.cpp | 149 ++ QtAVPlayer/src/QtAVPlayer/qavvideofilter_p.h | 52 + QtAVPlayer/src/QtAVPlayer/qavvideoframe.cpp | 475 ++++++ QtAVPlayer/src/QtAVPlayer/qavvideoframe.h | 80 + .../src/QtAVPlayer/qavvideoinputfilter.cpp | 116 ++ .../src/QtAVPlayer/qavvideoinputfilter_p.h | 46 + .../src/QtAVPlayer/qavvideooutputfilter.cpp | 42 + .../src/QtAVPlayer/qavvideooutputfilter_p.h | 38 + QtAVPlayer/src/QtAVPlayer/qtavplayerglobal.h | 13 + README.md | 35 + build.py | 262 +++ icons/check.png | Bin 0 -> 15714 bytes icons/error.png | Bin 0 -> 50788 bytes icons/main.png | Bin 0 -> 2587 bytes icons/main.svg | 1 + install.py | 342 ++++ src/actions.cpp | 168 ++ src/actions.h | 73 + src/common.cpp | 335 ++++ src/common.h | 125 ++ src/folder_dialog.cpp | 159 ++ src/folder_dialog.h | 45 + src/img_loader.cpp | 226 +++ src/img_loader.h | 59 + src/main.cpp | 87 + src/main_widget.cpp | 164 ++ src/main_widget.h | 67 + src/play_control.cpp | 146 ++ src/play_control.h | 60 + src/player.cpp | 140 ++ src/player.h | 86 + src/playlist_backend.cpp | 239 +++ src/playlist_backend.h | 77 + src/playlist_widget.cpp | 198 +++ src/playlist_widget.h | 94 ++ src/pref_dialog.cpp | 66 + src/pref_dialog.h | 40 + src/recents.cpp | 68 + src/recents.h | 63 + src/vid_grid.cpp | 91 ++ src/vid_grid.h | 51 + src/vid_widget.cpp | 263 ++++ src/vid_widget.h | 80 + templates/linux_icon.desktop | 10 + templates/linux_run_script.sh | 5 + templates/linux_uninstall.sh | 22 + 133 files changed, 15034 insertions(+) create mode 100644 .gitignore create mode 100644 JustVideo.pro create mode 100644 LICENSE.md create mode 100644 QtAVPlayer/LICENSE create mode 100644 QtAVPlayer/README.md create mode 100644 QtAVPlayer/examples/extract_frames/extract_frames.pro create mode 100644 QtAVPlayer/examples/extract_frames/main.cpp create mode 100644 QtAVPlayer/examples/qml_video/main.cpp create mode 100644 QtAVPlayer/examples/qml_video/main.qml create mode 100644 QtAVPlayer/examples/qml_video/main_qt6.qml create mode 100644 QtAVPlayer/examples/qml_video/qml.qrc create mode 100644 QtAVPlayer/examples/qml_video/qml_qt6.qrc create mode 100644 QtAVPlayer/examples/qml_video/qml_video.pro create mode 100644 QtAVPlayer/examples/widget_video/main.cpp create mode 100644 QtAVPlayer/examples/widget_video/widget_video.pro create mode 100644 QtAVPlayer/src/QtAVPlayer/QtAVPlayer.pri create mode 100644 QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavandroidsurfacetexture_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiocodec.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiocodec_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiofilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiofilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudioformat.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudioframe.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudioframe.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudioinputfilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiooutput.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiooutput.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavaudiooutputfilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavcodec.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavcodec_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavcodec_p_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavdemuxer.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavdemuxer_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfilter_p_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfiltergraph.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfiltergraph_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfilters.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavfilters_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavframe.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavframe.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavframe_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavframecodec.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavframecodec_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_d3d11_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_mediacodec_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_drm_egl_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_vaapi_x11_glx_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_vdpau_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox.mm create mode 100644 QtAVPlayer/src/QtAVPlayer/qavhwdevice_videotoolbox_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavinoutfilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavinoutfilter_p_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qaviodevice.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qaviodevice.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavpacket.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavpacket_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavpacketqueue_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavplayer.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavplayer.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavstream.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavstream.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavstreamframe.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavstreamframe.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavstreamframe_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavsubtitlecodec_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavsubtitleframe.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideobuffer_cpu_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideobuffer_gpu_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideobuffer_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideocodec.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideocodec_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideofilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideofilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideoframe.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideoframe.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideoinputfilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter.cpp create mode 100644 QtAVPlayer/src/QtAVPlayer/qavvideooutputfilter_p.h create mode 100644 QtAVPlayer/src/QtAVPlayer/qtavplayerglobal.h create mode 100644 README.md create mode 100755 build.py create mode 100644 icons/check.png create mode 100644 icons/error.png create mode 100755 icons/main.png create mode 100755 icons/main.svg create mode 100755 install.py create mode 100644 src/actions.cpp create mode 100644 src/actions.h create mode 100644 src/common.cpp create mode 100644 src/common.h create mode 100644 src/folder_dialog.cpp create mode 100644 src/folder_dialog.h create mode 100644 src/img_loader.cpp create mode 100644 src/img_loader.h create mode 100644 src/main.cpp create mode 100644 src/main_widget.cpp create mode 100644 src/main_widget.h create mode 100644 src/play_control.cpp create mode 100644 src/play_control.h create mode 100644 src/player.cpp create mode 100644 src/player.h create mode 100644 src/playlist_backend.cpp create mode 100644 src/playlist_backend.h create mode 100644 src/playlist_widget.cpp create mode 100644 src/playlist_widget.h create mode 100644 src/pref_dialog.cpp create mode 100644 src/pref_dialog.h create mode 100644 src/recents.cpp create mode 100644 src/recents.h create mode 100644 src/vid_grid.cpp create mode 100644 src/vid_grid.h create mode 100644 src/vid_widget.cpp create mode 100644 src/vid_widget.h create mode 100644 templates/linux_icon.desktop create mode 100644 templates/linux_run_script.sh create mode 100644 templates/linux_uninstall.sh 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 0000000000000000000000000000000000000000..0146337b81c61c58c096ae68137842810ff172e8 GIT binary patch literal 15714 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 LnpGb1y83?rQrjq= literal 0 HcmV?d00001 diff --git a/icons/main.png b/icons/main.png new file mode 100755 index 0000000000000000000000000000000000000000..441b2ec829efe528cb2f6c23a550a9d52132f30b GIT binary patch literal 2587 zcmdT_eNWgb3Kh@@%7|IUKEMmgh}aZ*-;SOB>%a1J&$;LQJ-_$&e7w(n z?!CDyqr%-?XSf1@dt^lD+W;H{Ilyy9i%s#Z9W8u$#D-)5x2gCh;FHp)5qB*zRJuk{ zdbf98;Tp=**~s;-z3dv%qvSv1w`%o|u51g>Ym=;Ut&YoGPlUx+roOUN<@;R4sZ)-l z=|htrQ9d~qYgwkr)Kt#Cgi`#EuYl{^=pF9uM8~BCB?~Z+l=5+alu=k2N6C^Kp%YFzNbaaBm>pwdDKXmyq_oeji z#qq3^XSEJOGOJtZ_>iLdsIFUXjnB)UPRb;diPgl|q|2ovjQw*av#BG2vqz!0swR5A zD`M=QGW6OmE$hNPRXA}JG}jgw&wU}ts$y!Ytm^cMMtN!!z^kSW)I*vQ2Yq@q%AUrN zEbLYe6z|PiBZz(8qVuMLGHx#K zwP}_$x>YN8tAFdSKiARMGz9rQZMNW=*D*_4=3{Bl0PSW77`CusXB*$l4 zvRKliyMKme8>AsYRwPcp`sgyL0(on z+)%}M(F&;3*IsO3n6Kyz2VcvE69 zWsWisN}oHAL5!6?TZKc#Sm@J!`|y^TX8f?YZGb*mg~74` z`nVq!w^`}RKrC*v&}DiImRaahJ@#5=rb_~`_~rn8P>;dt0s4R*d#$$88a}GakWzmr z?gpyMe;<3I1l8qb8!pUx24Xw^8sAy%wueD=rgE2v-zGMmu>F39X{u&I8o;!T1BSdZ zs%wZEt#o9A=9HtXZj~VD2pUsUvYYJrB*uD+vFjY!r@wp!V8=0$A{>qTbrUIza#i`r zk@r?lGTKOjF(V54$Kt4u)=^1%VQV4@$`L*!tQ0itLa_#;q{Gu6Ilk$t5(W?;%2gR% z(5E_g;)Y=hpGbHuTNbkMu!O1s4pgkhB$x`5a?m z#>}CH_KSLtz9(BJL9fI5+)5$btyddv_K)sS-*-~3?RA^?0ZnzqH4nknW9hzg@{!=Cg)Y#yi>c8I zjxcAXI#t~IuBnFExq(tY_=fP;vSGSk=GqDWM)p13Edsl~CGo!nY z!D-gJ-ipBmHmv?<3>w(?>Th6x9$Wo47!k literal 0 HcmV?d00001 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"