├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE.GPL ├── LICENSE.LGPL ├── README.rst ├── do_release.sh ├── mpv.py ├── pyproject.toml └── tests ├── __init__.py ├── sub_test.srt ├── test.webm └── test_mpv.py /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests' 2 | 3 | 4 | on: 5 | push: 6 | branches: [ '**' ] 7 | pull_request: 8 | branches: [ '**' ] 9 | 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | name: 'Linux - Python' 20 | strategy: 21 | matrix: 22 | python-version: [ '3.13' ] 23 | fail-fast: false 24 | env: 25 | PY_MPV_SKIP_TESTS: >- 26 | test_wait_for_property_event_overflow 27 | PY_MPV_TEST_VO: 'null' 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: 'Install Python' 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install uv 35 | uses: astral-sh/setup-uv@v5 36 | with: 37 | enable-cache: false 38 | - name: 'Install Dependencies' 39 | run: sudo apt install -y libmpv2 40 | - name: 'Setup Test Environment' 41 | run: uv sync --extra test 42 | - name: 'Run Python Tests' 43 | run: uv run -m pytest --reruns 3 --reruns-delay 3 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | mpv.egg-info 3 | __pycache__ 4 | *.swo 5 | *.swp 6 | -------------------------------------------------------------------------------- /LICENSE.GPL: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /LICENSE.LGPL: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. vim: tw=120 sw=4 et 2 | 3 | python-mpv 4 | ========== 5 | 6 | python-mpv is a ctypes-based python interface to the mpv media player. It gives you more or less full control of all 7 | features of the player, just as the lua interface does. 8 | 9 | Installation 10 | ------------ 11 | 12 | .. code:: bash 13 | 14 | pip install mpv 15 | 16 | 17 | ...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file. 18 | 19 | Requirements 20 | ~~~~~~~~~~~~ 21 | 22 | libmpv 23 | ...... 24 | ``libmpv.so`` either locally (in your current working directory) or somewhere in your system library search path. This 25 | module is somewhat lenient as far as ``libmpv`` versions are concerned but since ``libmpv`` is changing quite frequently 26 | you'll only get all the newest features when using an up-to-date version of this module. The unit tests for this module 27 | do some basic automatic version compatibility checks. If you discover anything missing here, please open an `issue`_ or 28 | submit a `pull request`_ on github. 29 | 30 | On Windows you can place libmpv anywhere in your ``%PATH%`` (e.g. next to ``python.exe``) or next to this module's 31 | ``mpv.py``. Before falling back to looking in the mpv module's directory, python-mpv uses the DLL search order built 32 | into ctypes, which is different to the one Windows uses internally. You can modify `%PATH%` before importing python-mpv 33 | to modify where python-mpv looks for the DLL. Consult `this stackoverflow post `__ 34 | for details. 35 | 36 | Python >= 3.9 37 | ............. 38 | We only support python stable releases from the last couple of years. We only test the current stable python release. If you find a compatibility issue with an older python version that still has upstream support (that is less than about four years old), feel free to open an issue_ and we'll have a look. 39 | 40 | .. _`issue`: https://github.com/jaseg/python-mpv/issues 41 | .. _`pull request`: https://github.com/jaseg/python-mpv/pulls 42 | 43 | Supported Platforms 44 | ................... 45 | 46 | **Linux**, **Windows** and **OSX** all seem to work mostly fine. For some notes on the installation on Windows see 47 | `this comment`__. Shared library handling is quite bad on windows, so expect some pain there. On OSX there seems to be 48 | some bug int the event logic. See `issue 36`_ and `issue 61`_ for details. Creating a pyQT window and having mpv draw 49 | into it seems to be a workaround (about 10loc), but in case you want this fixed please weigh in on the issue tracker 50 | since right now there is not many OSX users. 51 | 52 | .. __: https://github.com/jaseg/python-mpv/issues/60#issuecomment-352719773 53 | .. _`issue 61`: https://github.com/jaseg/python-mpv/issues/61 54 | .. _`issue 36`: https://github.com/jaseg/python-mpv/issues/36 55 | 56 | Usage 57 | ----- 58 | 59 | .. code:: python 60 | 61 | import mpv 62 | player = mpv.MPV(ytdl=True) 63 | player.play('https://youtu.be/DOmdB7D-pUU') 64 | player.wait_for_playback() 65 | 66 | python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands `_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source `__ for things you can do. Additional controls and status information are exposed through `MPV properties `_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``. 67 | 68 | Threading 69 | ~~~~~~~~~ 70 | 71 | The ``mpv`` module starts one thread for event handling, since MPV sends events that must be processed quickly. The 72 | event queue has a fixed maximum size and some operations can cause a large number of events to be sent. 73 | 74 | If you want to handle threading yourself, you can pass ``start_event_thread=False`` to the ``MPV`` constructor and 75 | manually call the ``MPV`` object's ``_loop`` function. If you have some strong need to not use threads and use some 76 | external event loop (such as asyncio) instead you can do that, too with some work. The API of the backend C ``libmpv`` 77 | has a function for producing a sort of event file descriptor for a handle. You can use that to produce a file descriptor 78 | that can be passed to an event loop to tell it to wake up the python-mpv event handler on every incoming event. 79 | 80 | All API functions are thread-safe. If one is not, please file an issue on github. 81 | 82 | Advanced Usage 83 | ~~~~~~~~~~~~~~ 84 | 85 | Logging, Properties, Python Key Bindings, Screenshots and youtube-dl 86 | .................................................................... 87 | 88 | .. code:: python 89 | 90 | #!/usr/bin/env python3 91 | import mpv 92 | 93 | def my_log(loglevel, component, message): 94 | print('[{}] {}: {}'.format(loglevel, component, message)) 95 | 96 | player = mpv.MPV(log_handler=my_log, ytdl=True, input_default_bindings=True, input_vo_keyboard=True) 97 | 98 | # Property access, these can be changed at runtime 99 | @player.property_observer('time-pos') 100 | def time_observer(_name, value): 101 | # Here, _value is either None if nothing is playing or a float containing 102 | # fractional seconds since the beginning of the file. 103 | print('Now playing at {:.2f}s'.format(value)) 104 | 105 | player.fullscreen = True 106 | player.loop_playlist = 'inf' 107 | # Option access, in general these require the core to reinitialize 108 | player['vo'] = 'gpu' 109 | 110 | @player.on_key_press('q') 111 | def my_q_binding(): 112 | print('THERE IS NO ESCAPE') 113 | 114 | @player.on_key_press('s') 115 | def my_s_binding(): 116 | pillow_img = player.screenshot_raw() 117 | pillow_img.save('screenshot.png') 118 | 119 | player.play('https://youtu.be/DLzxrzFCyOs') 120 | player.wait_for_playback() 121 | 122 | del player 123 | 124 | Skipping silence using libav filters 125 | .................................... 126 | 127 | The following code uses the libav silencedetect filter to skip silence at the beginning of a file. It works by loading 128 | the filter, then parsing its output from mpv's log. Thanks to Sean DeNigris on github (#202) for the original code! 129 | 130 | .. code:: python 131 | 132 | #!/usr/bin/env python3 133 | import sys 134 | import mpv 135 | 136 | p = mpv.MPV() 137 | p.play(sys.argv[1]) 138 | 139 | def skip_silence(): 140 | p.set_loglevel('debug') 141 | p.af = 'lavfi=[silencedetect=n=-20dB:d=1]' 142 | p.speed = 100 143 | def check(evt): 144 | toks = evt['event']['text'].split() 145 | if 'silence_end:' in toks: 146 | return float(toks[2]) 147 | p.time_pos = p.wait_for_event('log_message', cond=check) 148 | p.speed = 1 149 | p.af = '' 150 | 151 | skip_silence() 152 | p.wait_for_playback() 153 | 154 | Video overlays 155 | .............. 156 | 157 | .. code:: python 158 | 159 | #!/usr/bin/env python3 160 | import time 161 | from PIL import Image, ImageDraw, ImageFont 162 | import mpv 163 | 164 | player = mpv.MPV() 165 | 166 | player.loop = True 167 | player.play('test.webm') 168 | player.wait_until_playing() 169 | 170 | font = ImageFont.truetype('DejaVuSans.ttf', 40) 171 | 172 | while not player.core_idle: 173 | 174 | time.sleep(0.5) 175 | overlay = player.create_image_overlay() 176 | 177 | for pos in range(0, 500, 5): 178 | ts = player.time_pos 179 | if ts is None: 180 | break 181 | 182 | img = Image.new('RGBA', (400, 150), (255, 255, 255, 0)) 183 | d = ImageDraw.Draw(img) 184 | d.text((10, 10), 'Hello World', font=font, fill=(0, 255, 255, 128)) 185 | d.text((10, 60), f't={ts:.3f}', font=font, fill=(255, 0, 255, 255)) 186 | 187 | overlay.update(img, pos=(2*pos, pos)) 188 | time.sleep(0.05) 189 | 190 | overlay.remove() 191 | 192 | 193 | Playlist handling 194 | ................. 195 | 196 | .. code:: python 197 | 198 | #!/usr/bin/env python3 199 | import mpv 200 | 201 | player = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True) 202 | 203 | player.playlist_append('https://youtu.be/PHIGke6Yzh8') 204 | player.playlist_append('https://youtu.be/Ji9qSuQapFY') 205 | player.playlist_append('https://youtu.be/6f78_Tf4Tdk') 206 | 207 | player.playlist_pos = 0 208 | 209 | while True: 210 | # To modify the playlist, use player.playlist_{append,clear,move,remove}. player.playlist is read-only 211 | print(player.playlist) 212 | player.wait_for_playback() 213 | 214 | Directly feeding mpv data from python 215 | ..................................... 216 | 217 | .. code:: python 218 | 219 | #!/usr/bin/env python3 220 | import mpv 221 | 222 | player = mpv.MPV() 223 | @player.python_stream('foo') 224 | def reader(): 225 | with open('test.webm', 'rb') as f: 226 | while True: 227 | yield f.read(1024*1024) 228 | 229 | player.play('python://foo') 230 | player.wait_for_playback() 231 | 232 | Using external subtitles 233 | ........................ 234 | 235 | The easiest way to load custom subtitles from a file is to pass the ``--sub-file`` option to the ``loadfile`` call: 236 | 237 | .. code:: python 238 | 239 | #!/usr/bin/env python3 240 | import mpv 241 | 242 | player = mpv.MPV() 243 | player.loadfile('test.webm', sub_file='test.srt') 244 | player.wait_for_playback() 245 | 246 | Note that you can also pass many other options to ``loadfile``. See the mpv docs for details. 247 | 248 | If you want to add subtitle files or streams at runtime, you can use the ``sub-add`` command. ``sub-add`` can only be 249 | called once the player is done loading the file and starts playing. An easy way to wait for this is to wait for the 250 | ``core-idle`` property. 251 | 252 | .. code:: python 253 | 254 | #!/usr/bin/env python3 255 | import mpv 256 | 257 | player = mpv.MPV() 258 | player.play('test.webm') 259 | player.wait_until_playing() 260 | player.sub_add('test.srt') 261 | player.wait_for_playback() 262 | 263 | Using MPV's built-in GUI 264 | ........................ 265 | 266 | python-mpv is using mpv via libmpv. libmpv is meant for embedding into other applications and by default disables most 267 | GUI features such as the OSD or keyboard input. To enable the built-in GUI, use the following options when initializing 268 | the MPV instance. See `Issue 102`_ for more details 269 | 270 | .. _`issue 102`: https://github.com/jaseg/python-mpv/issues/61 271 | 272 | .. code:: python 273 | 274 | # Enable the on-screen controller and keyboard shortcuts 275 | player = mpv.MPV(input_default_bindings=True, input_vo_keyboard=True, osc=True) 276 | 277 | # Alternative version using the old "floating box" style on-screen controller 278 | player = mpv.MPV(player_operation_mode='pseudo-gui', 279 | script_opts='osc-layout=box,osc-seekbarstyle=bar,osc-deadzonesize=0,osc-minmousemove=3', 280 | input_default_bindings=True, 281 | input_vo_keyboard=True, 282 | osc=True) 283 | 284 | PyQT embedding 285 | .............. 286 | 287 | .. code:: python 288 | 289 | #!/usr/bin/env python3 290 | import mpv 291 | import sys 292 | 293 | from PyQt5.QtWidgets import * 294 | from PyQt5.QtCore import * 295 | 296 | class Test(QMainWindow): 297 | def __init__(self, parent=None): 298 | super().__init__(parent) 299 | self.container = QWidget(self) 300 | self.setCentralWidget(self.container) 301 | self.container.setAttribute(Qt.WA_DontCreateNativeAncestors) 302 | self.container.setAttribute(Qt.WA_NativeWindow) 303 | player = mpv.MPV(wid=str(int(self.container.winId())), 304 | vo='x11', # You may not need this 305 | log_handler=print, 306 | loglevel='debug') 307 | player.play('test.webm') 308 | 309 | app = QApplication(sys.argv) 310 | 311 | # This is necessary since PyQT stomps over the locale settings needed by libmpv. 312 | # This needs to happen after importing PyQT before creating the first mpv.MPV instance. 313 | import locale 314 | locale.setlocale(locale.LC_NUMERIC, 'C') 315 | win = Test() 316 | win.show() 317 | sys.exit(app.exec_()) 318 | 319 | PyGObject embedding 320 | ................... 321 | 322 | .. code:: python 323 | 324 | #!/usr/bin/env python3 325 | import gi 326 | 327 | import mpv 328 | 329 | gi.require_version('Gtk', '3.0') 330 | from gi.repository import Gtk 331 | 332 | 333 | class MainClass(Gtk.Window): 334 | 335 | def __init__(self): 336 | super(MainClass, self).__init__() 337 | self.set_default_size(600, 400) 338 | self.connect("destroy", self.on_destroy) 339 | 340 | widget = Gtk.Frame() 341 | self.add(widget) 342 | self.show_all() 343 | 344 | # Must be created >after< the widget is shown, else property 'window' will be None 345 | self.mpv = mpv.MPV(wid=str(widget.get_property("window").get_xid())) 346 | self.mpv.play("test.webm") 347 | 348 | def on_destroy(self, widget, data=None): 349 | self.mpv.terminate() 350 | Gtk.main_quit() 351 | 352 | 353 | if __name__ == '__main__': 354 | # This is necessary since like Qt, Gtk stomps over the locale settings needed by libmpv. 355 | # Like with Qt, this needs to happen after importing Gtk but before creating the first mpv.MPV instance. 356 | import locale 357 | locale.setlocale(locale.LC_NUMERIC, 'C') 358 | 359 | application = MainClass() 360 | Gtk.main() 361 | 362 | Using OpenGL from PyGObject 363 | ........................... 364 | 365 | Just like it is possible to render into a GTK widget through X11 windows, it `also is possible to render into a GTK 366 | widget using OpenGL `__ through this python API. 367 | 368 | Using OpenGL from PyQt5/QML 369 | ........................... 370 | 371 | Robozman_ has mangaed to `make mpv render into a PyQt5/QML widget using OpenGL 372 | `__ through this python API. 373 | 374 | Using mpv inside imgui inside OpenGL via GLFW 375 | ............................................. 376 | 377 | dfaker_ has written a demo (`link `__) that uses mpv to render video into an `imgui `__ UI running on an OpenGL context inside `GLFW `__. Check out their demo to see how to integrate with imgui/OpenGL and how to access properties and manage the lifecycle of an MPV instance. 378 | 379 | Running tests 380 | ------------- 381 | 382 | Use pytest to run tests. 383 | 384 | Coding Conventions 385 | ------------------ 386 | 387 | The general aim is `PEP 8`_, with liberal application of the "consistency" section. 120 cells line width. Four spaces. 388 | No tabs. Probably don't bother making pure-formatting PRs except if you think it *really* helps readability or it 389 | *really* irks you if you don't. 390 | 391 | License 392 | ------- 393 | 394 | python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or later. 395 | For details, see `the mpv copyright page`_. 396 | 397 | .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ 398 | .. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/main/mpv.py 399 | .. _cosven: https://github.com/cosven 400 | .. _Robozman: https://gitlab.com/robozman 401 | .. _dfaker: https://github.com/dfaker 402 | .. _`the mpv copyright page`: https://github.com/mpv-player/mpv/blob/master/Copyright 403 | 404 | -------------------------------------------------------------------------------- /do_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ $# -eq 1 ] || exit 2 4 | 5 | VER="$1" 6 | 7 | echo "$VER" | grep '^[0-9]\+\.[0-9]\+\.[0-9]\+$' || { 8 | echo "Call this script as ./do_release.sh [version] where version has format 1.2.3, without \"v\" prefix." 9 | exit 2 10 | } 11 | 12 | echo "Creating version $VER" 13 | 14 | if [ -n "$(git diff --name-only --cached)" ]; then 15 | echo "Stash or commit staged changes first" 16 | exit 2 17 | fi 18 | 19 | sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1v"$VER"\\2/" pyproject.toml 20 | sed -i "s/^\\(\\s*__version__\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" mpv.py 21 | git add pyproject.toml mpv.py 22 | git commit -m "Version $VER" --no-edit 23 | git -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER" 24 | git push --tags origin 25 | -------------------------------------------------------------------------------- /mpv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: ts=4 sw=4 et 3 | # 4 | # Python MPV library module 5 | # Copyright (C) 2017-2024 Sebastian Götte 6 | # 7 | # python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or 8 | # later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright 9 | # 10 | # You may copy, modify, and redistribute this file under the terms of the GNU General Public License version 2 (or, at 11 | # your option, any later version), or the GNU Lesser General Public License as published by the Free Software 12 | # Foundation; either version 2.1 of the License, or (at your option) any later version. 13 | # 14 | # This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 15 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU 16 | # Lesser General Public License for more details. 17 | # 18 | # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. 19 | 20 | __version__ = '1.0.8' 21 | 22 | from ctypes import * 23 | import ctypes.util 24 | import threading 25 | import queue 26 | import os 27 | import os.path 28 | import sys 29 | from warnings import warn 30 | from functools import partial, wraps 31 | from contextlib import contextmanager 32 | from concurrent.futures import Future, InvalidStateError 33 | import collections 34 | import re 35 | import traceback 36 | 37 | if os.name == 'nt': 38 | # Note: mpv-2.dll with API version 2 corresponds to mpv v0.35.0. Most things should work with the fallback, too. 39 | names = ['mpv-2.dll', 'libmpv-2.dll', 'mpv-1.dll'] 40 | for name in names: 41 | dll = ctypes.util.find_library(name) 42 | if dll: 43 | break 44 | else: 45 | for name in names: 46 | dll = os.path.join(os.path.dirname(__file__), name) 47 | if os.path.isfile(dll): 48 | break 49 | else: 50 | raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') 51 | 52 | try: 53 | # flags argument: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR 54 | # cf. https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa 55 | backend = CDLL(dll, 0x00001000 | 0x00000100) 56 | except Exception as e: 57 | if not os.path.isabs(dll): # can only be find_library, not the "look next to mpv.py" thing 58 | raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it. It looks like find_library found mpv.dll under a relative path entry in %PATH%. Please make sure all paths in %PATH% are absolute. Instead of trying to load mpv.dll from the current working directory, put it somewhere next to your script and add that path to %PATH% using os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]') from e 59 | else: 60 | raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it.') from e 61 | fs_enc = 'utf-8' 62 | 63 | else: 64 | import locale 65 | lc, enc = locale.getlocale(locale.LC_NUMERIC) 66 | # libmpv requires LC_NUMERIC to be set to "C". Since messing with global variables everyone else relies upon is 67 | # still better than segfaulting, we are setting LC_NUMERIC to "C". 68 | locale.setlocale(locale.LC_NUMERIC, 'C') 69 | 70 | sofile = ctypes.util.find_library('mpv') 71 | if sofile is None: 72 | raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult the documentation for ctypes.util.find_library which this script uses to look up the library filename.") 73 | backend = CDLL(sofile) 74 | fs_enc = sys.getfilesystemencoding() 75 | 76 | 77 | class ShutdownError(SystemError): 78 | pass 79 | 80 | class EventOverflowError(SystemError): 81 | pass 82 | 83 | class MpvHandle(c_void_p): 84 | pass 85 | 86 | class MpvRenderCtxHandle(c_void_p): 87 | pass 88 | 89 | class PropertyUnavailableError(AttributeError): 90 | pass 91 | 92 | class ErrorCode(object): 93 | """For documentation on these, see mpv's libmpv/client.h.""" 94 | SUCCESS = 0 95 | EVENT_QUEUE_FULL = -1 96 | NOMEM = -2 97 | UNINITIALIZED = -3 98 | INVALID_PARAMETER = -4 99 | OPTION_NOT_FOUND = -5 100 | OPTION_FORMAT = -6 101 | OPTION_ERROR = -7 102 | PROPERTY_NOT_FOUND = -8 103 | PROPERTY_FORMAT = -9 104 | PROPERTY_UNAVAILABLE = -10 105 | PROPERTY_ERROR = -11 106 | COMMAND = -12 107 | LOADING_FAILED = -13 108 | AO_INIT_FAILED = -14 109 | VO_INIT_FAILED = -15 110 | NOTHING_TO_PLAY = -16 111 | UNKNOWN_FORMAT = -17 112 | UNSUPPORTED = -18 113 | NOT_IMPLEMENTED = -19 114 | GENERIC = -20 115 | 116 | EXCEPTION_DICT = { 117 | 0: None, 118 | -1: lambda *a: MemoryError('mpv event queue full', *a), 119 | -2: lambda *a: MemoryError('mpv cannot allocate memory', *a), 120 | -3: lambda *a: ValueError('Uninitialized mpv handle used', *a), 121 | -4: lambda *a: ValueError('Invalid value for mpv parameter', *a), 122 | -5: lambda *a: AttributeError('mpv option does not exist', *a), 123 | -6: lambda *a: TypeError('Tried to set mpv option using wrong format', *a), 124 | -7: lambda *a: ValueError('Invalid value for mpv option', *a), 125 | -8: lambda *a: AttributeError('mpv property does not exist', *a), 126 | # Currently (mpv 0.18.1) there is a bug causing a PROPERTY_FORMAT error to be returned instead of 127 | # INVALID_PARAMETER when setting a property-mapped option to an invalid value. 128 | -9: lambda *a: TypeError('Tried to get/set mpv property using wrong format, or passed invalid value', *a), 129 | -10: lambda *a: PropertyUnavailableError('mpv property is not available', *a), 130 | -11: lambda *a: RuntimeError('Generic error getting or setting mpv property', *a), 131 | -12: lambda *a: SystemError('Error running mpv command', *a), 132 | -14: lambda *a: RuntimeError('Initializing the audio output failed', *a), 133 | -15: lambda *a: RuntimeError('Initializing the video output failed'), 134 | -16: lambda *a: RuntimeError('There was no audio or video data to play. This also happens if the file ' 135 | 'was recognized, but did not contain any audio or video streams, or no ' 136 | 'streams were selected.'), 137 | -17: lambda *a: RuntimeError('When trying to load the file, the file format could not be determined, ' 138 | 'or the file was too broken to open it'), 139 | -18: lambda *a: ValueError('Generic error for signaling that certain system requirements are not fulfilled'), 140 | -19: lambda *a: NotImplementedError('The API function which was called is a stub only'), 141 | -20: lambda *a: RuntimeError('Unspecified error') } 142 | 143 | @staticmethod 144 | def human_readable(ec): 145 | return _mpv_error_string(ec).decode('utf-8') 146 | 147 | @staticmethod 148 | def default_error_handler(ec, *args): 149 | return ValueError(ErrorCode.human_readable(ec), ec, *args) 150 | 151 | @classmethod 152 | def exception_for_ec(kls, ec, *args): 153 | ec = 0 if ec > 0 else ec 154 | ex = kls.EXCEPTION_DICT.get(ec, kls.default_error_handler) 155 | if ex: 156 | return ex(ec, *args) 157 | 158 | @classmethod 159 | def raise_for_ec(kls, ec, func, *args): 160 | ex = kls.exception_for_ec(ec, *args) 161 | if ex: 162 | raise ex 163 | 164 | MpvGlGetProcAddressFn = CFUNCTYPE(c_void_p, c_void_p, c_char_p) 165 | class MpvOpenGLInitParams(Structure): 166 | _fields_ = [('get_proc_address', MpvGlGetProcAddressFn), 167 | ('get_proc_address_ctx', c_void_p), 168 | ('extra_exts', c_void_p)] 169 | 170 | def __init__(self, get_proc_address): 171 | self.get_proc_address = get_proc_address 172 | self.get_proc_address_ctx = None 173 | self.extra_exts = None 174 | 175 | class MpvOpenGLFBO(Structure): 176 | _fields_ = [('fbo', c_int), 177 | ('w', c_int), 178 | ('h', c_int), 179 | ('internal_format', c_int)] 180 | 181 | def __init__(self, w, h, fbo=0, internal_format=0): 182 | self.w, self.h = w, h 183 | self.fbo = fbo 184 | self.internal_format = internal_format 185 | 186 | class MpvRenderFrameInfo(Structure): 187 | _fields_ = [('flags', c_int64), 188 | ('target_time', c_int64)] 189 | 190 | def as_dict(self): 191 | return {'flags': self.flags, 192 | 'target_time': self.target_time} 193 | 194 | class MpvOpenGLDRMParams(Structure): 195 | _fields_ = [('fd', c_int), 196 | ('crtc_id', c_int), 197 | ('connector_id', c_int), 198 | ('atomic_request_ptr', c_void_p), 199 | ('render_fd', c_int)] 200 | 201 | class MpvOpenGLDRMDrawSurfaceSize(Structure): 202 | _fields_ = [('width', c_int), ('height', c_int)] 203 | 204 | class MpvOpenGLDRMParamsV2(Structure): 205 | _fields_ = [('fd', c_int), 206 | ('crtc_id', c_int), 207 | ('connector_id', c_int), 208 | ('atomic_request_ptr', c_void_p), 209 | ('render_fd', c_int)] 210 | 211 | def __init__(self, crtc_id, connector_id, atomic_request_ptr, fd=-1, render_fd=-1): 212 | self.crtc_id, self.connector_id = crtc_id, connector_id 213 | self.atomic_request_ptr = atomic_request_ptr 214 | self.fd, self.render_fd = fd, render_fd 215 | 216 | 217 | class MpvRenderParam(Structure): 218 | _fields_ = [('type_id', c_int), 219 | ('data', c_void_p)] 220 | 221 | # maps human-readable type name to (type_id, argtype) tuple. 222 | # The type IDs come from libmpv/render.h 223 | TYPES = {"invalid" :(0, None), 224 | "api_type" :(1, str), 225 | "opengl_init_params" :(2, MpvOpenGLInitParams), 226 | "opengl_fbo" :(3, MpvOpenGLFBO), 227 | "flip_y" :(4, bool), 228 | "depth" :(5, int), 229 | "icc_profile" :(6, bytes), 230 | "ambient_light" :(7, int), 231 | "x11_display" :(8, c_void_p), 232 | "wl_display" :(9, c_void_p), 233 | "advanced_control" :(10, bool), 234 | "next_frame_info" :(11, MpvRenderFrameInfo), 235 | "block_for_target_time" :(12, bool), 236 | "skip_rendering" :(13, bool), 237 | "drm_display" :(14, MpvOpenGLDRMParams), 238 | "drm_draw_surface_size" :(15, MpvOpenGLDRMDrawSurfaceSize), 239 | "drm_display_v2" :(16, MpvOpenGLDRMParamsV2)} 240 | 241 | def __init__(self, name, value=None): 242 | if name not in self.TYPES: 243 | raise ValueError('unknown render param type "{}"'.format(name)) 244 | self.type_id, cons = self.TYPES[name] 245 | if cons is None: 246 | self.value = None 247 | self.data = c_void_p() 248 | elif cons is str: 249 | self.value = value 250 | self.data = cast(c_char_p(value.encode('utf-8')), c_void_p) 251 | elif cons is bytes: 252 | self.value = MpvByteArray(value) 253 | self.data = cast(pointer(self.value), c_void_p) 254 | elif cons is bool: 255 | self.value = c_int(int(bool(value))) 256 | self.data = cast(pointer(self.value), c_void_p) 257 | elif cons is c_void_p: 258 | self.value = value 259 | self.data = cast(self.value, c_void_p) 260 | else: 261 | self.value = cons(**value) 262 | self.data = cast(pointer(self.value), c_void_p) 263 | 264 | def kwargs_to_render_param_array(kwargs): 265 | t = MpvRenderParam * (len(kwargs)+1) 266 | return t(*kwargs.items(), ('invalid', None)) 267 | 268 | class MpvFormat(c_int): 269 | NONE = 0 270 | STRING = 1 271 | OSD_STRING = 2 272 | FLAG = 3 273 | INT64 = 4 274 | DOUBLE = 5 275 | NODE = 6 276 | NODE_ARRAY = 7 277 | NODE_MAP = 8 278 | BYTE_ARRAY = 9 279 | 280 | def __eq__(self, other): 281 | return self is other or self.value == other or self.value == int(other) 282 | 283 | def __repr__(self): 284 | return ['NONE', 'STRING', 'OSD_STRING', 'FLAG', 'INT64', 'DOUBLE', 'NODE', 'NODE_ARRAY', 'NODE_MAP', 285 | 'BYTE_ARRAY'][self.value] 286 | 287 | def __hash__(self): 288 | return self.value 289 | 290 | 291 | class MpvEventID(c_int): 292 | NONE = 0 293 | SHUTDOWN = 1 294 | LOG_MESSAGE = 2 295 | GET_PROPERTY_REPLY = 3 296 | SET_PROPERTY_REPLY = 4 297 | COMMAND_REPLY = 5 298 | START_FILE = 6 299 | END_FILE = 7 300 | FILE_LOADED = 8 301 | CLIENT_MESSAGE = 16 302 | VIDEO_RECONFIG = 17 303 | AUDIO_RECONFIG = 18 304 | SEEK = 20 305 | PLAYBACK_RESTART = 21 306 | PROPERTY_CHANGE = 22 307 | QUEUE_OVERFLOW = 24 308 | HOOK = 25 309 | 310 | ANY = ( SHUTDOWN, LOG_MESSAGE, GET_PROPERTY_REPLY, SET_PROPERTY_REPLY, COMMAND_REPLY, START_FILE, END_FILE, 311 | FILE_LOADED, CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE) 312 | 313 | def __repr__(self): 314 | return f'' 315 | 316 | @classmethod 317 | def from_str(kls, s): 318 | return getattr(kls, s.upper().replace('-', '_')) 319 | 320 | 321 | identity_decoder = lambda b: b 322 | strict_decoder = lambda b: b.decode('utf-8') 323 | def lazy_decoder(b): 324 | try: 325 | return b.decode('utf-8') 326 | except UnicodeDecodeError: 327 | return b 328 | 329 | class MpvNodeList(Structure): 330 | def array_value(self, decoder=identity_decoder): 331 | return [ self.values[i].node_value(decoder) for i in range(self.num) ] 332 | 333 | def dict_value(self, decoder=identity_decoder): 334 | return { self.keys[i].decode('utf-8'): 335 | self.values[i].node_value(decoder) for i in range(self.num) } 336 | 337 | class MpvByteArray(Structure): 338 | _fields_ = [('data', c_void_p), 339 | ('size', c_size_t)] 340 | 341 | def __init__(self, value): 342 | self._value = value 343 | self.data = cast(c_char_p(value), c_void_p) 344 | self.size = len(value) 345 | 346 | def bytes_value(self): 347 | return cast(self.data, POINTER(c_char))[:self.size] 348 | 349 | class MpvNode(Structure): 350 | def node_value(self, decoder=identity_decoder): 351 | return MpvNode.node_cast_value(self.val, self.format.value, decoder) 352 | 353 | @staticmethod 354 | def node_cast_value(v, fmt=MpvFormat.NODE, decoder=identity_decoder): 355 | if fmt == MpvFormat.NONE: 356 | return None 357 | elif fmt == MpvFormat.STRING: 358 | return decoder(v.string) 359 | elif fmt == MpvFormat.OSD_STRING: 360 | return v.string.decode('utf-8') 361 | elif fmt == MpvFormat.FLAG: 362 | return bool(v.flag) 363 | elif fmt == MpvFormat.INT64: 364 | return v.int64 365 | elif fmt == MpvFormat.DOUBLE: 366 | return v.double 367 | else: 368 | if not v.node: # Check for null pointer 369 | return None 370 | if fmt == MpvFormat.NODE: 371 | return v.node.contents.node_value(decoder) 372 | elif fmt == MpvFormat.NODE_ARRAY: 373 | return v.list.contents.array_value(decoder) 374 | elif fmt == MpvFormat.NODE_MAP: 375 | return v.map.contents.dict_value(decoder) 376 | elif fmt == MpvFormat.BYTE_ARRAY: 377 | return v.byte_array.contents.bytes_value() 378 | else: 379 | raise TypeError('Unknown MPV node format {}. Please submit a bug report.'.format(fmt)) 380 | 381 | class MpvNodeUnion(Union): 382 | _fields_ = [('string', c_char_p), 383 | ('flag', c_int), 384 | ('int64', c_int64), 385 | ('double', c_double), 386 | ('node', POINTER(MpvNode)), 387 | ('list', POINTER(MpvNodeList)), 388 | ('map', POINTER(MpvNodeList)), 389 | ('byte_array', POINTER(MpvByteArray))] 390 | 391 | MpvNode._fields_ = [('val', MpvNodeUnion), 392 | ('format', MpvFormat)] 393 | 394 | MpvNodeList._fields_ = [('num', c_int), 395 | ('values', POINTER(MpvNode)), 396 | ('keys', POINTER(c_char_p))] 397 | 398 | class MpvEvent(Structure): 399 | _fields_ = [('event_id', MpvEventID), 400 | ('error', c_int), 401 | ('reply_userdata', c_ulonglong), 402 | ('_data', c_void_p)] 403 | 404 | @property 405 | def data(self): 406 | dtype = { 407 | MpvEventID.GET_PROPERTY_REPLY: MpvEventProperty, 408 | MpvEventID.PROPERTY_CHANGE: MpvEventProperty, 409 | MpvEventID.LOG_MESSAGE: MpvEventLogMessage, 410 | MpvEventID.CLIENT_MESSAGE: MpvEventClientMessage, 411 | MpvEventID.START_FILE: MpvEventStartFile, 412 | MpvEventID.END_FILE: MpvEventEndFile, 413 | MpvEventID.HOOK: MpvEventHook, 414 | MpvEventID.COMMAND_REPLY: MpvEventCommand, 415 | }.get(self.event_id.value) 416 | return cast(self._data, POINTER(dtype)).contents if dtype else None 417 | 418 | def as_dict(self, decoder=identity_decoder): 419 | out = cast(create_string_buffer(sizeof(MpvNode)), POINTER(MpvNode)) 420 | _mpv_event_to_node(out, pointer(self)) 421 | rv = out.contents.node_value(decoder=decoder) 422 | _mpv_free_node_contents(out) 423 | return rv 424 | 425 | def __str__(self): 426 | d = self.data 427 | return f'<{type(d).__name__} ({self.event_id.value}) err={self.error} p={self.reply_userdata:016x} d={self.as_dict()}>' 428 | 429 | class MpvEventProperty(Structure): 430 | _fields_ = [('_name', c_char_p), 431 | ('format', MpvFormat), 432 | ('data', MpvNodeUnion)] 433 | 434 | @property 435 | def name(self): 436 | return self._name.decode("utf-8") 437 | 438 | @property 439 | def value(self): 440 | return MpvNode.node_cast_value(self.data, self.format.value, decoder=lazy_decoder) 441 | 442 | class MpvEventLogMessage(Structure): 443 | _fields_ = [('_prefix', c_char_p), 444 | ('_level', c_char_p), 445 | ('_text', c_char_p)] 446 | 447 | @property 448 | def prefix(self): 449 | return self._prefix.decode("utf-8") 450 | 451 | @property 452 | def level(self): 453 | return self._level.decode("utf-8") 454 | 455 | @property 456 | def text(self): 457 | return lazy_decoder(self._text) 458 | 459 | class MpvEventEndFile(Structure): 460 | _fields_ = [ 461 | ('reason', c_int), 462 | ('error', c_int), 463 | ('playlist_entry_id', c_ulonglong), 464 | ('playlist_insert_id', c_ulonglong), 465 | ('playlist_insert_num_entries', c_int), 466 | ] 467 | 468 | EOF = 0 469 | RESTARTED = 1 470 | ABORTED = 2 471 | QUIT = 3 472 | ERROR = 4 473 | REDIRECT = 5 474 | 475 | class MpvEventStartFile(Structure): 476 | _fields_ = [('playlist_entry_id', c_ulonglong),] 477 | 478 | class MpvEventClientMessage(Structure): 479 | _fields_ = [('_num_args', c_int), 480 | ('_args', POINTER(c_char_p))] 481 | 482 | @property 483 | def args(self): 484 | return [ self._args[i] for i in range(self._num_args) ] 485 | 486 | class MpvEventCommand(Structure): 487 | _fields_ = [('_result', MpvNode)] 488 | 489 | def unpack(self, decoder=identity_decoder): 490 | return self._result.node_value(decoder=decoder) 491 | 492 | @property 493 | def result(self): 494 | return self.unpack() 495 | 496 | class MpvEventHook(Structure): 497 | _fields_ = [('_name', c_char_p), 498 | ('id', c_ulonglong),] 499 | 500 | 501 | @property 502 | def name(self): 503 | return self._name.decode("utf-8") 504 | 505 | StreamReadFn = CFUNCTYPE(c_int64, c_void_p, POINTER(c_char), c_uint64) 506 | StreamSeekFn = CFUNCTYPE(c_int64, c_void_p, c_int64) 507 | StreamSizeFn = CFUNCTYPE(c_int64, c_void_p) 508 | StreamCloseFn = CFUNCTYPE(None, c_void_p) 509 | StreamCancelFn = CFUNCTYPE(None, c_void_p) 510 | 511 | class StreamCallbackInfo(Structure): 512 | _fields_ = [('cookie', c_void_p), 513 | ('read', StreamReadFn), 514 | ('seek', StreamSeekFn), 515 | ('size', StreamSizeFn), 516 | ('close', StreamCloseFn), 517 | ('cancel', StreamCancelFn)] 518 | 519 | StreamOpenFn = CFUNCTYPE(c_int, c_void_p, c_char_p, POINTER(StreamCallbackInfo)) 520 | 521 | WakeupCallback = CFUNCTYPE(None, c_void_p) 522 | 523 | RenderUpdateFn = CFUNCTYPE(None, c_void_p) 524 | 525 | def _handle_func(name, args, restype, errcheck, ctx=MpvHandle, deprecated=False): 526 | func = getattr(backend, name) 527 | func.argtypes = [ctx] + args if ctx else args 528 | if restype is not None: 529 | func.restype = restype 530 | if errcheck is not None: 531 | func.errcheck = errcheck 532 | if deprecated: 533 | @wraps(func) 534 | def wrapper(*args, **kwargs): 535 | if not wrapper.warned: # Only warn on first invocation to prevent spamming 536 | warn("Backend C api has been deprecated: " + name, DeprecationWarning, stacklevel=2) 537 | wrapper.warned = True 538 | return func(*args, **kwargs) 539 | wrapper.warned = False 540 | 541 | globals()['_'+name] = wrapper 542 | else: 543 | globals()['_'+name] = func 544 | 545 | def bytes_free_errcheck(res, func, *args): 546 | notnull_errcheck(res, func, *args) 547 | rv = cast(res, c_void_p).value 548 | _mpv_free(res) 549 | return rv 550 | 551 | def notnull_errcheck(res, func, *args): 552 | if res is None: 553 | raise RuntimeError('Underspecified error in MPV when calling {} with args {!r}: NULL pointer returned.'\ 554 | 'Please consult your local debugger.'.format(func.__name__, args)) 555 | return res 556 | 557 | ec_errcheck = ErrorCode.raise_for_ec 558 | 559 | backend.mpv_client_api_version.restype = c_ulong 560 | def _mpv_client_api_version(): 561 | ver = backend.mpv_client_api_version() 562 | return ver>>16, ver&0xFFFF 563 | 564 | MPV_VERSION = _mpv_client_api_version() 565 | if MPV_VERSION < (1, 108): 566 | ver = '.'.join(str(num) for num in MPV_VERSION) 567 | raise RuntimeError(f"python-mpv requires libmpv with an API version of 1.108 or higher (libmpv >= 0.33), but you have an older version ({ver}).") 568 | 569 | backend.mpv_free.argtypes = [c_void_p] 570 | _mpv_free = backend.mpv_free 571 | 572 | backend.mpv_free_node_contents.argtypes = [c_void_p] 573 | _mpv_free_node_contents = backend.mpv_free_node_contents 574 | 575 | backend.mpv_create.restype = MpvHandle 576 | _mpv_create = backend.mpv_create 577 | 578 | _handle_func('mpv_create_client', [c_char_p], MpvHandle, notnull_errcheck) 579 | _handle_func('mpv_create_weak_client', [c_char_p], MpvHandle, notnull_errcheck) 580 | _handle_func('mpv_client_name', [], c_char_p, errcheck=None) 581 | _handle_func('mpv_initialize', [], c_int, ec_errcheck) 582 | _handle_func('mpv_destroy', [], None, errcheck=None) 583 | _handle_func('mpv_terminate_destroy', [], None, errcheck=None) 584 | _handle_func('mpv_load_config_file', [c_char_p], c_int, ec_errcheck) 585 | _handle_func('mpv_get_time_us', [], c_ulonglong, errcheck=None) 586 | 587 | _handle_func('mpv_set_option', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck) 588 | _handle_func('mpv_set_option_string', [c_char_p, c_char_p], c_int, ec_errcheck) 589 | 590 | _handle_func('mpv_command', [POINTER(c_char_p)], c_int, ec_errcheck) 591 | _handle_func('mpv_command_string', [c_char_p, c_char_p], c_int, ec_errcheck) 592 | _handle_func('mpv_command_async', [c_ulonglong, POINTER(c_char_p)], c_int, ec_errcheck) 593 | _handle_func('mpv_command_node', [POINTER(MpvNode), POINTER(MpvNode)], c_int, ec_errcheck) 594 | _handle_func('mpv_command_node_async', [c_ulonglong, POINTER(MpvNode)], c_int, ec_errcheck) 595 | _handle_func('mpv_abort_async_command', [c_ulonglong], None, errcheck=None) 596 | 597 | _handle_func('mpv_set_property', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck) 598 | _handle_func('mpv_set_property_string', [c_char_p, c_char_p], c_int, ec_errcheck) 599 | _handle_func('mpv_set_property_async', [c_ulonglong, c_char_p, MpvFormat,c_void_p],c_int, ec_errcheck) 600 | _handle_func('mpv_get_property', [c_char_p, MpvFormat, c_void_p], c_int, ec_errcheck) 601 | _handle_func('mpv_get_property_string', [c_char_p], c_void_p, bytes_free_errcheck) 602 | _handle_func('mpv_get_property_osd_string', [c_char_p], c_void_p, bytes_free_errcheck) 603 | _handle_func('mpv_get_property_async', [c_ulonglong, c_char_p, MpvFormat], c_int, ec_errcheck) 604 | _handle_func('mpv_observe_property', [c_ulonglong, c_char_p, MpvFormat], c_int, ec_errcheck) 605 | _handle_func('mpv_unobserve_property', [c_ulonglong], c_int, ec_errcheck) 606 | 607 | _handle_func('mpv_event_name', [c_int], c_char_p, errcheck=None, ctx=None) 608 | _handle_func('mpv_event_to_node', [POINTER(MpvNode), POINTER(MpvEvent)], c_int, ec_errcheck, ctx=None) 609 | _handle_func('mpv_error_string', [c_int], c_char_p, errcheck=None, ctx=None) 610 | 611 | _handle_func('mpv_request_event', [MpvEventID, c_int], c_int, ec_errcheck) 612 | _handle_func('mpv_request_log_messages', [c_char_p], c_int, ec_errcheck) 613 | _handle_func('mpv_wait_event', [c_double], POINTER(MpvEvent), errcheck=None) 614 | _handle_func('mpv_wakeup', [], None, errcheck=None) 615 | _handle_func('mpv_set_wakeup_callback', [WakeupCallback, c_void_p], None, errcheck=None) 616 | 617 | _handle_func('mpv_stream_cb_add_ro', [c_char_p, c_void_p, StreamOpenFn], c_int, ec_errcheck) 618 | 619 | _handle_func('mpv_render_context_create', [MpvRenderCtxHandle, MpvHandle, POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=None) 620 | _handle_func('mpv_render_context_set_parameter', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle) 621 | _handle_func('mpv_render_context_get_info', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle) 622 | _handle_func('mpv_render_context_set_update_callback', [RenderUpdateFn, c_void_p], None, errcheck=None, ctx=MpvRenderCtxHandle) 623 | _handle_func('mpv_render_context_update', [], c_int64, errcheck=None, ctx=MpvRenderCtxHandle) 624 | _handle_func('mpv_render_context_render', [POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=MpvRenderCtxHandle) 625 | _handle_func('mpv_render_context_report_swap', [], None, errcheck=None, ctx=MpvRenderCtxHandle) 626 | _handle_func('mpv_render_context_free', [], None, errcheck=None, ctx=MpvRenderCtxHandle) 627 | 628 | 629 | def _mpv_coax_proptype(value, proptype=str): 630 | """Intelligently coax the given python value into something that can be understood as a proptype property.""" 631 | if type(value) is bytes: 632 | return value; 633 | elif type(value) is bool: 634 | return b'yes' if value else b'no' 635 | elif proptype in (str, int, float): 636 | return str(proptype(value)).encode('utf-8') 637 | else: 638 | raise TypeError('Cannot coax value of type {} into property type {}'.format(type(value), proptype)) 639 | 640 | def _make_node_str_list(l): 641 | """Take a list of python objects and make a MPV string node array from it. 642 | 643 | As an example, the python list ``l = [ "foo", 23, false ]`` will result in the following MPV node object:: 644 | 645 | struct mpv_node { 646 | .format = MPV_NODE_ARRAY, 647 | .u.list = *(struct mpv_node_array){ 648 | .num = len(l), 649 | .keys = NULL, 650 | .values = struct mpv_node[len(l)] { 651 | { .format = MPV_NODE_STRING, .u.string = l[0] }, 652 | { .format = MPV_NODE_STRING, .u.string = l[1] }, 653 | ... 654 | } 655 | } 656 | } 657 | """ 658 | char_ps = [ c_char_p(_mpv_coax_proptype(e, str)) for e in l ] 659 | node_list = MpvNodeList( 660 | num=len(l), 661 | keys=None, 662 | values=( MpvNode * len(l))( *[ MpvNode( 663 | format=MpvFormat.STRING, 664 | val=MpvNodeUnion(string=p)) 665 | for p in char_ps ])) 666 | node = MpvNode( 667 | format=MpvFormat.NODE_ARRAY, 668 | val=MpvNodeUnion(list=pointer(node_list))) 669 | return char_ps, node_list, node, cast(pointer(node), c_void_p) 670 | 671 | def _make_node_str_map(d): 672 | """Take a dict of python objects and make a MPV string node map from it. """ 673 | char_ps = [ (c_char_p(k.encode('utf-8')), c_char_p(_mpv_coax_proptype(v, str))) for k, v in d.items() ] 674 | node_list = MpvNodeList( 675 | num=len(d), 676 | keys=( c_char_p * len(d))( *[k for k, v in char_ps] ), 677 | values=( MpvNode * len(d))( *[ MpvNode( 678 | format=MpvFormat.STRING, 679 | val=MpvNodeUnion(string=v)) 680 | for k, v in char_ps ])) 681 | node = MpvNode( 682 | format=MpvFormat.NODE_MAP, 683 | val=MpvNodeUnion(map=pointer(node_list))) 684 | return char_ps, node_list, node, cast(pointer(node), c_void_p) 685 | 686 | 687 | def _event_generator(handle): 688 | while True: 689 | event = _mpv_wait_event(handle, -1).contents 690 | if event.event_id.value == MpvEventID.NONE: 691 | raise StopIteration() 692 | yield event 693 | 694 | 695 | def _create_null_term_cmd_arg_array(name, args): 696 | args = [name.encode('utf-8')] + [(arg if type(arg) is bytes else str(arg).encode('utf-8')) 697 | for arg in args if arg is not None] + [None] 698 | return (c_char_p * len(args))(*args) 699 | 700 | 701 | _py_to_mpv = lambda name: name.replace('_', '-') 702 | _mpv_to_py = lambda name: name.replace('-', '_') 703 | 704 | _drop_nones = lambda *args: [ arg for arg in args if arg is not None ] 705 | 706 | class _Proxy: 707 | def __init__(self, mpv): 708 | super().__setattr__('mpv', mpv) 709 | 710 | class _PropertyProxy(_Proxy): 711 | def __dir__(self): 712 | return super().__dir__() + [ name.replace('-', '_') for name in self.mpv.property_list ] 713 | 714 | class _FileLocalProxy(_Proxy): 715 | def __getitem__(self, name): 716 | return self.mpv.__getitem__(name, file_local=True) 717 | 718 | def __setitem__(self, name, value): 719 | return self.mpv.__setitem__(name, value, file_local=True) 720 | 721 | def __iter__(self): 722 | return iter(self.mpv) 723 | 724 | class _OSDPropertyProxy(_PropertyProxy): 725 | def __getattr__(self, name): 726 | return self.mpv._get_property(_py_to_mpv(name), fmt=MpvFormat.OSD_STRING) 727 | 728 | def __setattr__(self, _name, _value): 729 | raise AttributeError('OSD properties are read-only. Please use the regular property API for writing.') 730 | 731 | class _DecoderPropertyProxy(_PropertyProxy): 732 | def __init__(self, mpv, decoder): 733 | super().__init__(mpv) 734 | super().__setattr__('_decoder', decoder) 735 | 736 | def __getattr__(self, name): 737 | return self.mpv._get_property(_py_to_mpv(name), decoder=self._decoder) 738 | 739 | def __setattr__(self, name, value): 740 | setattr(self.mpv, _py_to_mpv(name), value) 741 | 742 | class GeneratorStream: 743 | """Transform a python generator into an mpv-compatible stream object. The total size of the file can be indicated to 744 | mpv using the size argument to __init__. Seeking is not supported. 745 | """ 746 | 747 | def __init__(self, generator_fun, size=None): 748 | self._generator_fun = generator_fun 749 | self.size = size 750 | 751 | def seek(self, offset): 752 | self._read_iter = iter(self._generator_fun()) 753 | self._read_chunk = b'' 754 | return 0 # We only support seeking to the first byte atm 755 | # implementation in case seeking to arbitrary offsets would be necessary 756 | # while offset > 0: 757 | # offset -= len(self.read(offset)) 758 | # return offset 759 | 760 | def read(self, size): 761 | if not self._read_chunk: 762 | try: 763 | self._read_chunk += next(self._read_iter) 764 | except StopIteration: 765 | return b'' 766 | rv, self._read_chunk = self._read_chunk[:size], self._read_chunk[size:] 767 | return rv 768 | 769 | def close(self): 770 | self._read_iter = iter([]) # make next read() call return EOF 771 | 772 | def cancel(self): 773 | self._read_iter = iter([]) # make next read() call return EOF 774 | 775 | 776 | class ImageOverlay: 777 | def __init__(self, m, overlay_id, img=None, pos=(0, 0)): 778 | self.m = m 779 | self.overlay_id = overlay_id 780 | self.pos = pos 781 | self._size = None 782 | if img is not None: 783 | self.update(img) 784 | 785 | def update(self, img=None, pos=None): 786 | from PIL import Image 787 | if img is not None: 788 | self.img = img 789 | img = self.img 790 | 791 | w, h = img.size 792 | stride = w*4 793 | 794 | if pos is not None: 795 | self.pos = pos 796 | x, y = self.pos 797 | 798 | # Pre-multiply alpha channel 799 | bg = Image.new('RGBA', (w, h), (0, 0, 0, 0)) 800 | out = Image.alpha_composite(bg, img) 801 | 802 | # Copy image to ctypes buffer 803 | if img.size != self._size: 804 | self._buf = create_string_buffer(w*h*4) 805 | self._size = img.size 806 | 807 | ctypes.memmove(self._buf, out.tobytes('raw', 'BGRA'), w*h*4) 808 | source = '&' + str(addressof(self._buf)) 809 | 810 | self.m.overlay_add(self.overlay_id, x, y, source, 0, 'bgra', w, h, stride) 811 | 812 | def remove(self): 813 | self.m.remove_overlay(self.overlay_id) 814 | 815 | 816 | class FileOverlay: 817 | def __init__(self, m, overlay_id, filename=None, size=None, stride=None, pos=(0,0)): 818 | self.m = m 819 | self.overlay_id = overlay_id 820 | self.pos = pos 821 | self.size = size 822 | self.stride = stride 823 | if filename is not None: 824 | self.update(filename) 825 | 826 | def update(self, filename=None, size=None, stride=None, pos=None): 827 | if filename is not None: 828 | self.filename = filename 829 | 830 | if pos is not None: 831 | self.pos = pos 832 | 833 | if size is not None: 834 | self.size = size 835 | 836 | if stride is not None: 837 | self.stride = stride 838 | 839 | x, y = self.pos 840 | w, h = self.size 841 | stride = self.stride or 4*w 842 | 843 | self.m.overlay_add(self, self.overlay_id, x, y, self.filename, 0, 'bgra', w, h, stride) 844 | 845 | def remove(self): 846 | self.m.remove_overlay(self.overlay_id) 847 | 848 | 849 | class MPV(object): 850 | """See man mpv(1) for the details of the implemented commands. All mpv properties can be accessed as 851 | ``my_mpv.some_property`` and all mpv options can be accessed as ``my_mpv['some-option']``. 852 | 853 | By default, properties are returned as decoded ``str`` and an error is thrown if the value does not contain valid 854 | utf-8. To get a decoded ``str`` if possibly but ``bytes`` instead of an error if not, use 855 | ``my_mpv.lazy.some_property``. To always get raw ``bytes``, use ``my_mpv.raw.some_property``. To access a 856 | property's decoded OSD value, use ``my_mpv.osd.some_property``. 857 | 858 | To get API information on an option, use ``my_mpv.option_info('option-name')``. To get API information on a 859 | property, use ``my_mpv.properties['property-name']``. Take care to use mpv's dashed-names instead of the 860 | underscore_names exposed on the python object. 861 | 862 | To make your program not barf hard the first time its used on a weird file system **always** access properties 863 | containing file names or file tags through ``MPV.raw``. """ 864 | 865 | def __init__(self, *extra_mpv_flags, log_handler=None, start_event_thread=True, loglevel=None, **extra_mpv_opts): 866 | """Create an MPV instance. 867 | 868 | Extra arguments and extra keyword arguments will be passed to mpv as options. 869 | """ 870 | 871 | self.handle = _mpv_create() 872 | self._event_thread = None 873 | self._core_shutdown = False 874 | 875 | _mpv_set_option_string(self.handle, b'audio-display', b'no') 876 | istr = lambda o: ('yes' if o else 'no') if type(o) is bool else str(o) 877 | try: 878 | for flag in extra_mpv_flags: 879 | _mpv_set_option_string(self.handle, flag.encode('utf-8'), b'') 880 | for k,v in extra_mpv_opts.items(): 881 | _mpv_set_option_string(self.handle, k.replace('_', '-').encode('utf-8'), istr(v).encode('utf-8')) 882 | finally: 883 | _mpv_initialize(self.handle) 884 | 885 | self.osd = _OSDPropertyProxy(self) 886 | self.file_local = _FileLocalProxy(self) 887 | self.raw = _DecoderPropertyProxy(self, identity_decoder) 888 | self.strict = _DecoderPropertyProxy(self, strict_decoder) 889 | self.lazy = _DecoderPropertyProxy(self, lazy_decoder) 890 | 891 | self._event_callbacks = [] 892 | self._command_reply_callbacks = {} 893 | self._event_handler_lock = threading.Lock() 894 | self._property_handlers = collections.defaultdict(lambda: []) 895 | self._quit_handlers = set() 896 | self._message_handlers = {} 897 | self._key_binding_handlers = {} 898 | self._event_handle = _mpv_create_client(self.handle, b'py_event_handler') 899 | self._log_handler = log_handler 900 | self._stream_protocol_cbs = {} 901 | self._stream_protocol_frontends = collections.defaultdict(lambda: {}) 902 | self.register_stream_protocol('python', self._python_stream_open) 903 | self._python_streams = {} 904 | self._python_stream_catchall = None 905 | self._exception_futures = set() 906 | self.overlay_ids = set() 907 | self.overlays = {} 908 | if loglevel is not None or log_handler is not None: 909 | self.set_loglevel(loglevel or 'terminal-default') 910 | if start_event_thread: 911 | self._event_thread = threading.Thread(target=self._loop, name='MPVEventHandlerThread') 912 | self._event_thread.daemon = True 913 | self._event_thread.start() 914 | else: 915 | self._event_thread = None 916 | if (m := re.search(r'(\d+)\.(\d+)\.(\d+)', self.mpv_version)): 917 | self.mpv_version_tuple = tuple(map(int, m.groups())) 918 | 919 | @contextmanager 920 | def _enqueue_exceptions(self): 921 | try: 922 | yield 923 | except Exception as e: 924 | for fut in self._exception_futures: 925 | try: 926 | fut.set_exception(e) 927 | break 928 | except InvalidStateError: 929 | pass 930 | else: 931 | warn(f'Unhandled exception on python-mpv event loop: {e}\n{traceback.format_exc()}', RuntimeWarning) 932 | 933 | def _loop(self): 934 | for event in _event_generator(self._event_handle): 935 | try: 936 | eid = event.event_id.value 937 | 938 | with self._event_handler_lock: 939 | if eid == MpvEventID.SHUTDOWN: 940 | self._core_shutdown = True 941 | 942 | for callback in self._event_callbacks: 943 | with self._enqueue_exceptions(): 944 | callback(event) 945 | 946 | if eid == MpvEventID.PROPERTY_CHANGE: 947 | pc = event.data 948 | name, value, _fmt = pc.name, pc.value, pc.format 949 | for handler in self._property_handlers[name]: 950 | with self._enqueue_exceptions(): 951 | handler(name, value) 952 | 953 | if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None: 954 | ev = event.data 955 | with self._enqueue_exceptions(): 956 | self._log_handler(ev.level, ev.prefix, ev.text) 957 | 958 | if eid == MpvEventID.CLIENT_MESSAGE: 959 | # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16} 960 | target, *args = event.data.args 961 | target = target.decode("utf-8") 962 | if target in self._message_handlers: 963 | with self._enqueue_exceptions(): 964 | self._message_handlers[target](*args) 965 | 966 | if eid == MpvEventID.COMMAND_REPLY: 967 | key = event.reply_userdata 968 | callback = self._command_reply_callbacks.pop(key, None) 969 | if callback: 970 | with self._enqueue_exceptions(): 971 | callback(ErrorCode.exception_for_ec(event.error), event.data) 972 | 973 | if eid == MpvEventID.QUEUE_OVERFLOW: 974 | # cache list, since error handlers will unregister themselves 975 | for cb in list(self._command_reply_callbacks.values()): 976 | with self._enqueue_exceptions(): 977 | cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None) 978 | 979 | if eid == MpvEventID.SHUTDOWN: 980 | _mpv_destroy(self._event_handle) 981 | for cb in list(self._command_reply_callbacks.values()): 982 | with self._enqueue_exceptions(): 983 | cb(ShutdownError('libmpv core has been shutdown'), None) 984 | return 985 | 986 | except Exception as e: 987 | warn(f'Unhandled {e} inside python-mpv event loop!\n{traceback.format_exc()}', RuntimeWarning) 988 | 989 | @property 990 | def core_shutdown(self): 991 | """Property indicating whether the core has been shut down. Possible causes for this are e.g. the `quit` command 992 | or a user closing the mpv window.""" 993 | return self._core_shutdown 994 | 995 | def check_core_alive(self): 996 | """ This method can be used as a sanity check to tests whether the core is still alive at the time it is 997 | called.""" 998 | if self._core_shutdown: 999 | raise ShutdownError('libmpv core has been shutdown') 1000 | 1001 | def wait_until_paused(self, timeout=None, catch_errors=True): 1002 | """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while 1003 | waiting.""" 1004 | self.wait_for_property('core-idle', timeout=timeout, catch_errors=catch_errors) 1005 | 1006 | def wait_for_playback(self, timeout=None, catch_errors=True): 1007 | """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while 1008 | waiting. 1009 | """ 1010 | self.wait_for_event('end_file', timeout=timeout, catch_errors=catch_errors) 1011 | 1012 | def wait_until_playing(self, timeout=None, catch_errors=True): 1013 | """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while 1014 | waiting.""" 1015 | self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout, catch_errors=catch_errors) 1016 | 1017 | def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True): 1018 | """Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for 1019 | properties such as ``idle_active`` indicating the player is done with regular playback and just idling around. 1020 | Raises a ShutdownError when the core is shutdown while waiting. 1021 | """ 1022 | with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout, catch_errors=catch_errors) as result: 1023 | pass 1024 | return result.result() 1025 | 1026 | def wait_for_shutdown(self, timeout=None, catch_errors=True): 1027 | '''Wait for core to shutdown (e.g. through quit() or terminate()).''' 1028 | try: 1029 | self.wait_for_event(None, timeout=timeout, catch_errors=catch_errors) 1030 | except ShutdownError: 1031 | return 1032 | 1033 | def _set_error_handler(self, future): 1034 | @self.event_callback('shutdown', 'queue-overflow') 1035 | def shutdown_handler(event): 1036 | nonlocal future 1037 | try: 1038 | if event.event_id.value == MpvEventID.SHUTDOWN: 1039 | future.set_exception(ShutdownError('libmpv core has been shutdown')) 1040 | else: 1041 | future.set_exception(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough')) 1042 | except InvalidStateError: 1043 | pass 1044 | return shutdown_handler.unregister_mpv_events 1045 | 1046 | @contextmanager 1047 | def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True): 1048 | """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See 1049 | prepare_and_wait_for_event for usage. 1050 | Raises a ShutdownError when the core is shutdown while waiting. Re-raises any errors inside ``cond``. 1051 | """ 1052 | result = Future() 1053 | 1054 | def observer(name, val): 1055 | try: 1056 | rv = cond(val) 1057 | if rv: 1058 | result.set_result(rv) 1059 | 1060 | except InvalidStateError: 1061 | pass 1062 | 1063 | except Exception as e: 1064 | try: 1065 | result.set_exception(e) 1066 | except: 1067 | pass 1068 | 1069 | try: 1070 | result.set_running_or_notify_cancel() 1071 | 1072 | self.observe_property(name, observer) 1073 | err_unregister = self._set_error_handler(result) 1074 | if catch_errors: 1075 | self._exception_futures.add(result) 1076 | 1077 | yield result 1078 | 1079 | if level_sensitive: 1080 | rv = cond(getattr(self, name.replace('-', '_'))) 1081 | if rv: 1082 | result.set_result(rv) 1083 | return 1084 | 1085 | self.check_core_alive() 1086 | result.result(timeout) 1087 | 1088 | except InvalidStateError: 1089 | pass 1090 | 1091 | finally: 1092 | err_unregister() 1093 | self.unobserve_property(name, observer) 1094 | self._exception_futures.discard(result) 1095 | 1096 | def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True): 1097 | """Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError 1098 | if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error 1099 | inside ``cond``. 1100 | """ 1101 | with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout, catch_errors=catch_errors) as result: 1102 | pass 1103 | return result.result() 1104 | 1105 | @contextmanager 1106 | def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True): 1107 | """Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given, 1108 | waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens 1109 | when 'shutdown' is in event_types. Re-raises any error inside ``cond``. 1110 | 1111 | Compared to wait_for_event this handles the case where a thread waits for an event it itself causes in a 1112 | thread-safe way. An example from the testsuite is: 1113 | 1114 | with self.m.prepare_and_wait_for_event('client_message'): 1115 | self.m.keypress(key) 1116 | 1117 | Using just wait_for_event it would be impossible to ensure the event is caught since it may already have been 1118 | handled in the interval between keypress(...) running and a subsequent wait_for_event(...) call. 1119 | """ 1120 | result = Future() 1121 | 1122 | @self.event_callback(*event_types) 1123 | def target_handler(evt): 1124 | try: 1125 | rv = cond(evt) 1126 | if rv: 1127 | result.set_result(rv) 1128 | except Exception as e: 1129 | try: 1130 | result.set_exception(e) 1131 | except InvalidStateError: 1132 | pass 1133 | except InvalidStateError: 1134 | pass 1135 | 1136 | err_unregister = self._set_error_handler(result) 1137 | 1138 | try: 1139 | result.set_running_or_notify_cancel() 1140 | if catch_errors: 1141 | self._exception_futures.add(result) 1142 | 1143 | yield result 1144 | 1145 | self.check_core_alive() 1146 | result.result(timeout) 1147 | 1148 | finally: 1149 | err_unregister() 1150 | target_handler.unregister_mpv_events() 1151 | self._exception_futures.discard(result) 1152 | 1153 | def __del__(self): 1154 | if self.handle: 1155 | self.terminate() 1156 | 1157 | def terminate(self): 1158 | """Properly terminates this player instance. Preferably use this instead of relying on python's garbage 1159 | collector to cause this to be called from the object's destructor. 1160 | 1161 | This method will detach the main libmpv handle and wait for mpv to shut down and the event thread to finish. 1162 | """ 1163 | self.handle, handle = None, self.handle 1164 | if threading.current_thread() is self._event_thread: 1165 | raise UserWarning('terminate() should not be called from event thread (e.g. from a callback function). If ' 1166 | 'you want to terminate mpv from here, please call quit() instead, then sync the main thread ' 1167 | 'against the event thread using e.g. wait_for_shutdown(), then terminate() from the main thread. ' 1168 | 'This call has been transformed into a call to quit().') 1169 | self.quit() 1170 | else: 1171 | _mpv_terminate_destroy(handle) 1172 | if self._event_thread: 1173 | self._event_thread.join() 1174 | 1175 | def set_loglevel(self, level): 1176 | """Set MPV's log level. This adjusts which output will be sent to this object's log handlers. If you just want 1177 | mpv's regular terminal output, you don't need to adjust this but just need to pass a log handler to the MPV 1178 | constructur such as ``MPV(log_handler=print)``. 1179 | 1180 | Valid log levels are "no", "fatal", "error", "warn", "info", "v" "debug" and "trace". For details see your mpv's 1181 | client.h header file. 1182 | """ 1183 | _mpv_request_log_messages(self._event_handle, level.encode('utf-8')) 1184 | 1185 | def string_command(self, name, *args): 1186 | """Execute a raw command.""" 1187 | args = _create_null_term_cmd_arg_array(name, args) 1188 | _mpv_command(self.handle, args) 1189 | 1190 | def command_async(self, name, *args, callback=None, decoder=lazy_decoder, **kwargs): 1191 | """Same as mpv_command, but run the command asynchronously. If you provide a callback, that callback will be 1192 | called after completion or on error. This method returns a future that evaluates to the result of the callback 1193 | (if given), and the result of the libmpv call otherwise. 1194 | 1195 | Usage example: 1196 | 1197 | future = player.command_async(...) 1198 | try: 1199 | print('The result was', future.result()) 1200 | except Exception as e: 1201 | print('mpv returned an error:', e) 1202 | """ 1203 | 1204 | future = Future() 1205 | future.set_running_or_notify_cancel() 1206 | 1207 | if callback is None: 1208 | def callback(error, result): 1209 | if error: 1210 | raise error 1211 | return result 1212 | 1213 | def wrapper(error, result): 1214 | try: 1215 | result = result.unpack(decoder) 1216 | future.set_result(callback(error, result)) 1217 | except Exception as e: 1218 | try: 1219 | future.set_exception(e) 1220 | except InvalidStateError: 1221 | pass 1222 | 1223 | def abort(): 1224 | _mpv_abort_async_command(self._event_handle, id(future)) 1225 | del self._command_reply_callbacks[id(future)] 1226 | future.cancel = abort 1227 | 1228 | self._command_reply_callbacks[id(future)] = wrapper 1229 | 1230 | if kwargs: 1231 | if args: 1232 | raise ValueError('Can only call mpv commands either using positional or using named arguments, not a mix of both.') 1233 | kwargs['name'] = name 1234 | _1, _2, _3, pointer = _make_node_str_map(kwargs) 1235 | else: 1236 | _1, _2, _3, pointer = _make_node_str_list([name, *args]) 1237 | 1238 | ppointer = cast(pointer, POINTER(MpvNode)) 1239 | _mpv_command_node_async(self._event_handle, id(future), ppointer) 1240 | return future 1241 | 1242 | 1243 | def node_command(self, name, *args, decoder=strict_decoder): 1244 | self.command(name, *args, decoder=decoder) 1245 | 1246 | def command(self, name, *args, decoder=strict_decoder, **kwargs): 1247 | if kwargs: 1248 | if args: 1249 | raise ValueError('Can only call mpv commands either using positional or using named arguments, not a mix of both.') 1250 | kwargs['name'] = name 1251 | _1, _2, _3, pointer = _make_node_str_map(kwargs) 1252 | else: 1253 | _1, _2, _3, pointer = _make_node_str_list([name, *args]) 1254 | 1255 | out = cast(create_string_buffer(sizeof(MpvNode)), POINTER(MpvNode)) 1256 | ppointer = cast(pointer, POINTER(MpvNode)) 1257 | _mpv_command_node(self.handle, ppointer, out) 1258 | rv = out.contents.node_value(decoder=decoder) 1259 | _mpv_free_node_contents(out) 1260 | return rv 1261 | 1262 | def seek(self, amount, reference="relative", precision="keyframes"): 1263 | """Mapped mpv seek command, see man mpv(1).""" 1264 | self.command('seek', amount, reference, precision) 1265 | 1266 | def revert_seek(self): 1267 | """Mapped mpv revert_seek command, see man mpv(1).""" 1268 | self.command('revert_seek'); 1269 | 1270 | def frame_step(self): 1271 | """Mapped mpv frame-step command, see man mpv(1).""" 1272 | self.command('frame-step') 1273 | 1274 | def frame_back_step(self): 1275 | """Mapped mpv frame_back_step command, see man mpv(1).""" 1276 | self.command('frame_back_step') 1277 | 1278 | def property_add(self, name, value=1): 1279 | """Add the given value to the property's value. On overflow or underflow, clamp the property to the maximum. If 1280 | ``value`` is omitted, assume ``1``. 1281 | """ 1282 | self.command('add', name, value) 1283 | 1284 | def property_multiply(self, name, factor): 1285 | """Multiply the value of a property with a numeric factor.""" 1286 | self.command('multiply', name, factor) 1287 | 1288 | def cycle(self, name, direction='up'): 1289 | """Cycle the given property. ``up`` and ``down`` set the cycle direction. On overflow, set the property back to 1290 | the minimum, on underflow set it to the maximum. If ``up`` or ``down`` is omitted, assume ``up``. 1291 | """ 1292 | self.command('cycle', name, direction) 1293 | 1294 | def screenshot(self, includes='subtitles', mode='single'): 1295 | """Mapped mpv screenshot command, see man mpv(1).""" 1296 | self.command('screenshot', includes, mode) 1297 | 1298 | def screenshot_to_file(self, filename, includes='subtitles'): 1299 | """Mapped mpv screenshot_to_file command, see man mpv(1).""" 1300 | self.command('screenshot_to_file', filename.encode(fs_enc), includes) 1301 | 1302 | def screenshot_raw(self, includes='subtitles'): 1303 | """Mapped mpv screenshot_raw command, see man mpv(1). Returns a pillow Image object.""" 1304 | from PIL import Image 1305 | res = self.command('screenshot-raw', includes) 1306 | if res['format'] != 'bgr0': 1307 | raise ValueError('Screenshot in unknown format "{}". Currently, only bgr0 is supported.' 1308 | .format(res['format'])) 1309 | img = Image.frombytes('RGBA', (res['stride']//4, res['h']), res['data']) 1310 | b,g,r,a = img.split() 1311 | return Image.merge('RGB', (r,g,b)) 1312 | 1313 | def allocate_overlay_id(self): 1314 | free_ids = set(range(64)) - self.overlay_ids 1315 | if not free_ids: 1316 | raise IndexError('All overlay IDs are in use') 1317 | next_id, *_ = sorted(free_ids) 1318 | self.overlay_ids.add(next_id) 1319 | return next_id 1320 | 1321 | def free_overlay_id(self, overlay_id): 1322 | self.overlay_ids.remove(overlay_id) 1323 | 1324 | def create_file_overlay(self, filename=None, size=None, stride=None, pos=(0,0)): 1325 | overlay_id = self.allocate_overlay_id() 1326 | overlay = FileOverlay(self, overlay_id, filename, size, stride, pos) 1327 | self.overlays[overlay_id] = overlay 1328 | return overlay 1329 | 1330 | def create_image_overlay(self, img=None, pos=(0,0)): 1331 | overlay_id = self.allocate_overlay_id() 1332 | overlay = ImageOverlay(self, overlay_id, img, pos) 1333 | self.overlays[overlay_id] = overlay 1334 | return overlay 1335 | 1336 | def remove_overlay(self, overlay_id): 1337 | self.overlay_remove(overlay_id) 1338 | self.free_overlay_id(overlay_id) 1339 | del self.overlays[overlay_id] 1340 | 1341 | def playlist_next(self, mode='weak'): 1342 | """Mapped mpv playlist_next command, see man mpv(1).""" 1343 | self.command('playlist_next', mode) 1344 | 1345 | def playlist_prev(self, mode='weak'): 1346 | """Mapped mpv playlist_prev command, see man mpv(1).""" 1347 | self.command('playlist_prev', mode) 1348 | 1349 | def playlist_play_index(self, idx): 1350 | """Mapped mpv playlist-play-index command, see man mpv(1).""" 1351 | self.command('playlist-play-index', idx) 1352 | 1353 | @staticmethod 1354 | def _encode_options(options): 1355 | return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items()) 1356 | 1357 | def loadfile(self, filename, mode='replace', index=None, **options): 1358 | """Mapped mpv loadfile command, see man mpv(1).""" 1359 | if self.mpv_version_tuple >= (0, 38, 0): 1360 | if index is None: 1361 | index = -1 1362 | self.command('loadfile', filename.encode(fs_enc), mode, index, MPV._encode_options(options)) 1363 | else: 1364 | if index is not None: 1365 | warn(f'The index argument to the loadfile command is only supported on mpv >= 0.38.0') 1366 | self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options)) 1367 | 1368 | def loadlist(self, playlist, mode='replace'): 1369 | """Mapped mpv loadlist command, see man mpv(1).""" 1370 | self.command('loadlist', playlist.encode(fs_enc), mode) 1371 | 1372 | def playlist_clear(self): 1373 | """Mapped mpv playlist_clear command, see man mpv(1).""" 1374 | self.command('playlist_clear') 1375 | 1376 | def playlist_remove(self, index='current'): 1377 | """Mapped mpv playlist_remove command, see man mpv(1).""" 1378 | self.command('playlist_remove', index) 1379 | 1380 | def playlist_move(self, index1, index2): 1381 | """Mapped mpv playlist_move command, see man mpv(1).""" 1382 | self.command('playlist_move', index1, index2) 1383 | 1384 | def playlist_shuffle(self): 1385 | """Mapped mpv playlist-shuffle command, see man mpv(1).""" 1386 | self.command('playlist-shuffle') 1387 | 1388 | def playlist_unshuffle(self): 1389 | """Mapped mpv playlist-unshuffle command, see man mpv(1).""" 1390 | self.command('playlist-unshuffle') 1391 | 1392 | def run(self, command, *args): 1393 | """Mapped mpv run command, see man mpv(1).""" 1394 | self.command('run', command, *args) 1395 | 1396 | def quit(self, code=None): 1397 | """Mapped mpv quit command, see man mpv(1).""" 1398 | if code is not None: 1399 | self.command('quit', code) 1400 | else: 1401 | self.command('quit') 1402 | 1403 | def quit_watch_later(self, code=None): 1404 | """Mapped mpv quit_watch_later command, see man mpv(1).""" 1405 | if code is not None: 1406 | self.command('quit_watch_later', code) 1407 | else: 1408 | self.command('quit_watch_later') 1409 | 1410 | def stop(self, keep_playlist=False): 1411 | """Mapped mpv stop command, see man mpv(1).""" 1412 | if keep_playlist: 1413 | self.command('stop', 'keep-playlist') 1414 | else: 1415 | self.command('stop') 1416 | 1417 | def audio_add(self, url, flags='select', title=None, lang=None): 1418 | """Mapped mpv audio_add command, see man mpv(1).""" 1419 | self.command('audio_add', url.encode(fs_enc), *_drop_nones(flags, title, lang)) 1420 | 1421 | def audio_remove(self, audio_id=None): 1422 | """Mapped mpv audio_remove command, see man mpv(1).""" 1423 | self.command('audio_remove', audio_id) 1424 | 1425 | def audio_reload(self, audio_id=None): 1426 | """Mapped mpv audio_reload command, see man mpv(1).""" 1427 | self.command('audio_reload', audio_id) 1428 | 1429 | def video_add(self, url, flags='select', title=None, lang=None, albumart=None): 1430 | """Mapped mpv video_add command, see man mpv(1).""" 1431 | self.command('video_add', url.encode(fs_enc), *_drop_nones(flags, title, lang, albumart)) 1432 | 1433 | def video_remove(self, video_id=None): 1434 | """Mapped mpv video_remove command, see man mpv(1).""" 1435 | self.command('video_remove', video_id) 1436 | 1437 | def video_reload(self, video_id=None): 1438 | """Mapped mpv video_reload command, see man mpv(1).""" 1439 | self.command('video_reload', video_id) 1440 | 1441 | def sub_add(self, url, flags='select', title=None, lang=None): 1442 | """Mapped mpv sub_add command, see man mpv(1).""" 1443 | self.command('sub_add', url.encode(fs_enc), *_drop_nones(flags, title, lang)) 1444 | 1445 | def sub_remove(self, sub_id=None): 1446 | """Mapped mpv sub_remove command, see man mpv(1).""" 1447 | self.command('sub_remove', sub_id) 1448 | 1449 | def sub_reload(self, sub_id=None): 1450 | """Mapped mpv sub_reload command, see man mpv(1).""" 1451 | self.command('sub_reload', sub_id) 1452 | 1453 | def sub_step(self, skip): 1454 | """Mapped mpv sub_step command, see man mpv(1).""" 1455 | self.command('sub_step', skip) 1456 | 1457 | def sub_seek(self, skip): 1458 | """Mapped mpv sub_seek command, see man mpv(1).""" 1459 | self.command('sub_seek', skip) 1460 | 1461 | def toggle_osd(self): 1462 | """Mapped mpv osd command, see man mpv(1).""" 1463 | self.command('osd') 1464 | 1465 | def print_text(self, text): 1466 | """Mapped mpv print-text command, see man mpv(1).""" 1467 | self.command('print-text', text) 1468 | 1469 | def show_text(self, string, duration='-1', level=0): 1470 | """Mapped mpv show_text command, see man mpv(1).""" 1471 | self.command('show_text', string, duration, level) 1472 | 1473 | def expand_text(self, text): 1474 | """Mapped mpv expand-text command, see man mpv(1).""" 1475 | return self.command('expand-text', text) 1476 | 1477 | def expand_path(self, path): 1478 | """Mapped mpv expand-path command, see man mpv(1).""" 1479 | return self.command('expand-path', path) 1480 | 1481 | def show_progress(self): 1482 | """Mapped mpv show_progress command, see man mpv(1).""" 1483 | self.command('show_progress') 1484 | 1485 | def rescan_external_files(self, mode='reselect'): 1486 | """Mapped mpv rescan-external-files command, see man mpv(1).""" 1487 | self.command('rescan-external-files', mode) 1488 | 1489 | def discnav(self, command): 1490 | """Mapped mpv discnav command, see man mpv(1).""" 1491 | self.command('discnav', command) 1492 | 1493 | def mouse(self, x, y, button=None, mode='single'): 1494 | """Mapped mpv mouse command, see man mpv(1).""" 1495 | if button is None: 1496 | self.command('mouse', x, y, mode) 1497 | else: 1498 | self.command('mouse', x, y, button, mode) 1499 | 1500 | def keypress(self, name): 1501 | """Mapped mpv keypress command, see man mpv(1).""" 1502 | self.command('keypress', name) 1503 | 1504 | def keydown(self, name): 1505 | """Mapped mpv keydown command, see man mpv(1).""" 1506 | self.command('keydown', name) 1507 | 1508 | def keyup(self, name=None): 1509 | """Mapped mpv keyup command, see man mpv(1).""" 1510 | if name is None: 1511 | self.command('keyup') 1512 | else: 1513 | self.command('keyup', name) 1514 | 1515 | def keybind(self, name, command): 1516 | """Mapped mpv keybind command, see man mpv(1).""" 1517 | self.command('keybind', name, command) 1518 | 1519 | def write_watch_later_config(self): 1520 | """Mapped mpv write_watch_later_config command, see man mpv(1).""" 1521 | self.command('write_watch_later_config') 1522 | 1523 | def overlay_add(self, overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride): 1524 | """Mapped mpv overlay_add command, see man mpv(1).""" 1525 | self.command('overlay_add', overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride) 1526 | 1527 | def overlay_remove(self, overlay_id): 1528 | """Mapped mpv overlay_remove command, see man mpv(1).""" 1529 | self.command('overlay_remove', overlay_id) 1530 | 1531 | def osd_overlay(self, overlay_id, data, res_x=0, res_y=720, z=0, hidden=False): 1532 | self.command('osd_overlay', id=overlay_id, data=data, res_x=res_x, res_y=res_y, z=z, hidden=hidden, 1533 | format='ass-events') 1534 | 1535 | def osd_overlay_remove(self, overlay_id): 1536 | self.command('osd_overlay', id=overlay_id, format='none') 1537 | 1538 | def script_message(self, *args): 1539 | """Mapped mpv script_message command, see man mpv(1).""" 1540 | self.command('script_message', *args) 1541 | 1542 | def script_message_to(self, target, *args): 1543 | """Mapped mpv script_message_to command, see man mpv(1).""" 1544 | self.command('script_message_to', target, *args) 1545 | 1546 | def drop_buffers(self): 1547 | self.command('drop_buffers') 1548 | 1549 | def vf_command(self, label, command, argument): 1550 | self.command('vf_command', label, command, argument) 1551 | 1552 | def af_command(self, label, command, argument): 1553 | self.command('af_command', label, command, argument) 1554 | 1555 | def observe_property(self, name, handler): 1556 | """Register an observer on the named property. An observer is a function that is called with the new property 1557 | value every time the property's value is changed. The basic function signature is ``fun(property_name, 1558 | new_value)`` with new_value being the decoded property value as a python object. This function can be used as a 1559 | function decorator if no handler is given. 1560 | 1561 | To unregister the observer, call either of ``mpv.unobserve_property(name, handler)``, 1562 | ``mpv.unobserve_all_properties(handler)`` or the handler's ``unobserve_mpv_properties`` attribute:: 1563 | 1564 | @player.property_observer('volume') 1565 | def my_handler(property_name, new_volume): 1566 | print("It's loud!", new_volume) 1567 | 1568 | my_handler.unobserve_mpv_properties() 1569 | 1570 | exit_handler is a function taking no arguments that is called when the underlying mpv handle is terminated (e.g. 1571 | from calling MPV.terminate() or issuing a "quit" input command). 1572 | """ 1573 | self._property_handlers[name].append(handler) 1574 | _mpv_observe_property(self._event_handle, hash(name)&0xffffffffffffffff, name.encode('utf-8'), MpvFormat.NODE) 1575 | 1576 | def property_observer(self, name): 1577 | """Function decorator to register a property observer. See ``MPV.observe_property`` for details.""" 1578 | def wrapper(fun): 1579 | self.observe_property(name, fun) 1580 | fun.unobserve_mpv_properties = lambda: self.unobserve_property(name, fun) 1581 | return fun 1582 | return wrapper 1583 | 1584 | def unobserve_property(self, name, handler): 1585 | """Unregister a property observer. This requires both the observed property's name and the handler function that 1586 | was originally registered as one handler could be registered for several properties. To unregister a handler 1587 | from *all* observed properties see ``unobserve_all_properties``. 1588 | """ 1589 | self._property_handlers[name].remove(handler) 1590 | if not self._property_handlers[name]: 1591 | _mpv_unobserve_property(self._event_handle, hash(name)&0xffffffffffffffff) 1592 | 1593 | def unobserve_all_properties(self, handler): 1594 | """Unregister a property observer from *all* observed properties.""" 1595 | for name in self._property_handlers: 1596 | self.unobserve_property(name, handler) 1597 | 1598 | def register_message_handler(self, target, handler=None): 1599 | """Register a mpv script message handler. This can be used to communicate with embedded lua scripts. Pass the 1600 | script message target name this handler should be listening to and the handler function. 1601 | 1602 | WARNING: Only one handler can be registered at a time for any given target. 1603 | 1604 | To unregister the message handler, call its ``unregister_mpv_messages`` function:: 1605 | 1606 | player = mpv.MPV() 1607 | @player.message_handler('foo') 1608 | def my_handler(some, args): 1609 | print(args) 1610 | 1611 | my_handler.unregister_mpv_messages() 1612 | """ 1613 | self._register_message_handler_internal(target, handler) 1614 | 1615 | def _register_message_handler_internal(self, target, handler): 1616 | self._message_handlers[target] = handler 1617 | 1618 | def unregister_message_handler(self, target_or_handler): 1619 | """Unregister a mpv script message handler for the given script message target name. 1620 | 1621 | You can also call the ``unregister_mpv_messages`` function attribute set on the handler function when it is 1622 | registered. 1623 | """ 1624 | if isinstance(target_or_handler, str): 1625 | del self._message_handlers[target_or_handler] 1626 | else: 1627 | for key, val in self._message_handlers.items(): 1628 | if val == target_or_handler: 1629 | del self._message_handlers[key] 1630 | 1631 | def message_handler(self, target): 1632 | """Decorator to register a mpv script message handler. 1633 | 1634 | WARNING: Only one handler can be registered at a time for any given target. 1635 | 1636 | To unregister the message handler, call its ``unregister_mpv_messages`` function:: 1637 | 1638 | player = mpv.MPV() 1639 | @player.message_handler('foo') 1640 | def my_handler(some, args): 1641 | print(args) 1642 | 1643 | my_handler.unregister_mpv_messages() 1644 | """ 1645 | def register(handler): 1646 | self._register_message_handler_internal(target, handler) 1647 | handler.unregister_mpv_messages = lambda: self.unregister_message_handler(handler) 1648 | return handler 1649 | return register 1650 | 1651 | def register_event_callback(self, callback): 1652 | """Register a blanket event callback receiving all event types. 1653 | 1654 | To unregister the event callback, call its ``unregister_mpv_events`` function:: 1655 | 1656 | player = mpv.MPV() 1657 | @player.event_callback('shutdown') 1658 | def my_handler(event): 1659 | print('It ded.') 1660 | 1661 | my_handler.unregister_mpv_events() 1662 | """ 1663 | self._event_callbacks.append(callback) 1664 | 1665 | def unregister_event_callback(self, callback): 1666 | """Unregiser an event callback.""" 1667 | self._event_callbacks.remove(callback) 1668 | 1669 | def event_callback(self, *event_types): 1670 | """Function decorator to register a blanket event callback for the given event types. Event types can be given 1671 | as str (e.g. 'start-file'), integer or MpvEventID object. 1672 | 1673 | WARNING: Due to the way this is filtering events, this decorator cannot be chained with itself. 1674 | 1675 | To unregister the event callback, call its ``unregister_mpv_events`` function:: 1676 | 1677 | player = mpv.MPV() 1678 | @player.event_callback('shutdown') 1679 | def my_handler(event): 1680 | print('It ded.') 1681 | 1682 | my_handler.unregister_mpv_events() 1683 | """ 1684 | def register(callback): 1685 | with self._event_handler_lock: 1686 | self.check_core_alive() 1687 | types = [MpvEventID.from_str(t) if isinstance(t, str) else t for t in event_types] or MpvEventID.ANY 1688 | @wraps(callback) 1689 | def wrapper(event, *args, **kwargs): 1690 | if event.event_id.value in types: 1691 | callback(event, *args, **kwargs) 1692 | self._event_callbacks.append(wrapper) 1693 | wrapper.unregister_mpv_events = partial(self.unregister_event_callback, wrapper) 1694 | return wrapper 1695 | return register 1696 | 1697 | @staticmethod 1698 | def _binding_name(callback_or_cmd): 1699 | return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff) 1700 | 1701 | def on_key_press(self, keydef, mode='force', repetition=False): 1702 | """Function decorator to register a simplified key binding. The callback is called whenever the key given is 1703 | *pressed*. When the ``repetition=True`` is passed, the callback is called again repeatedly while the key is held 1704 | down. 1705 | 1706 | To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute:: 1707 | 1708 | player = mpv.MPV() 1709 | @player.on_key_press('Q') 1710 | def binding(): 1711 | print('blep') 1712 | 1713 | binding.unregister_mpv_key_bindings() 1714 | 1715 | WARNING: For a single keydef only a single callback/command can be registered at the same time. If you register 1716 | a binding multiple times older bindings will be overwritten and there is a possibility of references leaking. So 1717 | don't do that. 1718 | 1719 | The BIG FAT WARNING regarding untrusted keydefs from the key_binding method applies here as well. 1720 | """ 1721 | def register(fun): 1722 | @self.key_binding(keydef, mode) 1723 | @wraps(fun) 1724 | def wrapper(state='p-', name=None, char=None, *_): 1725 | if state[0] in ('d', 'p') or (repetition and state[0] == 'r'): 1726 | fun() 1727 | return wrapper 1728 | return register 1729 | 1730 | def key_binding(self, keydef, mode='force'): 1731 | """Function decorator to register a low-level key binding. 1732 | 1733 | The callback function signature is ``fun(key_state, key_name, key_char, scale, arg)``. 1734 | 1735 | The key_state contains up to three chars, corresponding to the regex ``[udr]([m-][c-]?)?``. ``[udr]`` means 1736 | "key up", "key down", or "repetition" for when the key is held down. "m" indicates mouse events, and "c" 1737 | indicates key up events resulting from a logical cancellation. For details check out the mpv man page. 1738 | 1739 | The keydef format is: ``[Shift+][Ctrl+][Alt+][Meta+]`` where ```` is either the literal character the 1740 | key produces (ASCII or Unicode character), or a symbolic name (as printed by ``mpv --input-keylist``). 1741 | 1742 | To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute:: 1743 | 1744 | player = mpv.MPV() 1745 | @player.key_binding('Q') 1746 | def binding(state, name, char): 1747 | print('blep') 1748 | 1749 | binding.unregister_mpv_key_bindings() 1750 | 1751 | WARNING: For a single keydef only a single callback/command can be registered at the same time. If you register 1752 | a binding multiple times older bindings will be overwritten and there is a possibility of references leaking. So 1753 | don't do that. 1754 | 1755 | BIG FAT WARNING: mpv's key binding mechanism is pretty powerful. This means, you essentially get arbitrary code 1756 | exectution through key bindings. This interface makes some limited effort to sanitize the keydef given in the 1757 | first parameter, but YOU SHOULD NOT RELY ON THIS IN FOR SECURITY. If your input comes from config files, this is 1758 | completely fine--but, if you are about to pass untrusted input into this parameter, better double-check whether 1759 | this is secure in your case. 1760 | """ 1761 | def register(fun): 1762 | fun.mpv_key_bindings = getattr(fun, 'mpv_key_bindings', []) + [keydef] 1763 | def unregister_all(): 1764 | for keydef in fun.mpv_key_bindings: 1765 | self.unregister_key_binding(keydef) 1766 | fun.unregister_mpv_key_bindings = unregister_all 1767 | 1768 | self.register_key_binding(keydef, fun, mode) 1769 | return fun 1770 | return register 1771 | 1772 | def register_key_binding(self, keydef, callback_or_cmd, mode='force'): 1773 | """Register a key binding. This takes an mpv keydef and either a string containing a mpv command or a python 1774 | callback function. See ``MPV.key_binding`` for details. 1775 | """ 1776 | if not re.match(r'(Shift+)?(Ctrl+)?(Alt+)?(Meta+)?(.|\w+)', keydef): 1777 | raise ValueError('Invalid keydef. Expected format: [Shift+][Ctrl+][Alt+][Meta+]\n' 1778 | ' is either the literal character the key produces (ASCII or Unicode character), or a ' 1779 | 'symbolic name (as printed by --input-keylist') 1780 | binding_name = MPV._binding_name(keydef) 1781 | if callable(callback_or_cmd): 1782 | self._key_binding_handlers[binding_name] = callback_or_cmd 1783 | self.register_message_handler('key-binding', self._handle_key_binding_message) 1784 | self.command('define-section', 1785 | binding_name, '{} script-binding py_event_handler/{}'.format(keydef, binding_name), mode) 1786 | elif isinstance(callback_or_cmd, str): 1787 | self.command('define-section', binding_name, '{} {}'.format(keydef, callback_or_cmd), mode) 1788 | else: 1789 | raise TypeError('register_key_binding expects either an str with an mpv command or a python callable.') 1790 | self.command('enable-section', binding_name, 'allow-hide-cursor+allow-vo-dragging') 1791 | 1792 | def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None, scale=None, arg=None, *_): 1793 | binding_name = binding_name.decode('utf-8') 1794 | key_state = key_state.decode('utf-8') 1795 | key_name = key_name.decode('utf-8') if key_name is not None else None 1796 | key_char = key_char.decode('utf-8') if key_char is not None else None 1797 | self._key_binding_handlers[binding_name](key_state, key_name, key_char, scale, arg) 1798 | 1799 | def unregister_key_binding(self, keydef): 1800 | """Unregister a key binding by keydef.""" 1801 | binding_name = MPV._binding_name(keydef) 1802 | self.command('disable-section', binding_name) 1803 | self.command('define-section', binding_name, '') 1804 | if binding_name in self._key_binding_handlers: 1805 | del self._key_binding_handlers[binding_name] 1806 | if not self._key_binding_handlers: 1807 | self.unregister_message_handler('key-binding') 1808 | 1809 | def register_stream_protocol(self, proto, open_fn=None): 1810 | """ Register a custom stream protocol as documented in libmpv/stream_cb.h: 1811 | https://github.com/mpv-player/mpv/blob/master/libmpv/stream_cb.h 1812 | 1813 | proto is the protocol scheme, e.g. "foo" for "foo://" urls. 1814 | 1815 | This function can either be used with two parameters or it can be used as a decorator on the target 1816 | function. 1817 | 1818 | open_fn is a function taking an URI string and returning an mpv stream object. 1819 | open_fn may raise a ValueError to signal libmpv the URI could not be opened. 1820 | 1821 | The mpv stream protocol is as follows: 1822 | class Stream: 1823 | @property 1824 | def size(self): 1825 | return None # unknown size 1826 | return size # int with size in bytes 1827 | 1828 | def read(self, size): 1829 | ... 1830 | return read # non-empty bytes object with input 1831 | return b'' # empty byte object signals permanent EOF 1832 | 1833 | def seek(self, pos): # optional 1834 | return new_offset # integer with new byte offset. The new offset may be before the requested offset 1835 | in case an exact seek is inconvenient. 1836 | 1837 | def close(self): # optional 1838 | ... 1839 | 1840 | def cancel(self): # optional 1841 | Abort a running read() or seek() operation 1842 | ... 1843 | 1844 | """ 1845 | 1846 | def decorator(open_fn): 1847 | @StreamOpenFn 1848 | def open_backend(_userdata, uri, cb_info): 1849 | try: 1850 | frontend = open_fn(uri.decode('utf-8')) 1851 | except ValueError: 1852 | return ErrorCode.LOADING_FAILED 1853 | except Exception as e: 1854 | for fut in self._exception_futures: 1855 | try: 1856 | fut.set_exception(e) 1857 | break 1858 | except InvalidStateError: 1859 | pass 1860 | else: 1861 | warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}') 1862 | return ErrorCode.LOADING_FAILED 1863 | 1864 | cb_info.contents.cookie = None 1865 | 1866 | def read_backend(_userdata, buf, bufsize): 1867 | with self._enqueue_exceptions(): 1868 | data = frontend.read(bufsize) 1869 | for i in range(len(data)): 1870 | buf[i] = data[i] 1871 | return len(data) 1872 | return -1 1873 | read = cb_info.contents.read = StreamReadFn(read_backend) 1874 | 1875 | def close_backend(_userdata): 1876 | with self._enqueue_exceptions(): 1877 | del self._stream_protocol_frontends[proto][uri] 1878 | if hasattr(frontend, 'close'): 1879 | frontend.close() 1880 | close = cb_info.contents.close = StreamCloseFn(close_backend) 1881 | 1882 | seek, size, cancel = None, None, None 1883 | 1884 | if hasattr(frontend, 'seek'): 1885 | def seek_backend(_userdata, offx): 1886 | with self._enqueue_exceptions(): 1887 | return frontend.seek(offx) 1888 | return ErrorCode.GENERIC 1889 | seek = cb_info.contents.seek = StreamSeekFn(seek_backend) 1890 | 1891 | if hasattr(frontend, 'size') and frontend.size is not None: 1892 | def size_backend(_userdata): 1893 | with self._enqueue_exceptions(): 1894 | return frontend.size 1895 | return 0 1896 | size = cb_info.contents.size = StreamSizeFn(size_backend) 1897 | 1898 | if hasattr(frontend, 'cancel'): 1899 | def cancel_backend(_userdata): 1900 | with self._enqueue_exceptions(): 1901 | frontend.cancel() 1902 | cancel = cb_info.contents.cancel = StreamCancelFn(cancel_backend) 1903 | 1904 | # keep frontend and callbacks in memory until closed 1905 | frontend._registered_callbacks = [read, close, seek, size, cancel] 1906 | self._stream_protocol_frontends[proto][uri] = frontend 1907 | return 0 1908 | 1909 | if proto in self._stream_protocol_cbs: 1910 | raise KeyError('Stream protocol already registered') 1911 | # keep backend in memory forever 1912 | self._stream_protocol_cbs[proto] = [open_backend] 1913 | _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend) 1914 | 1915 | return open_fn 1916 | 1917 | if open_fn is not None: 1918 | decorator(open_fn) 1919 | return decorator 1920 | 1921 | # Convenience functions 1922 | def play(self, filename): 1923 | """Play a path or URL (requires ``ytdl`` option to be set).""" 1924 | self.loadfile(filename) 1925 | 1926 | @property 1927 | def playlist_filenames(self): 1928 | """Return all playlist item file names/URLs as a list of strs.""" 1929 | return [element['filename'] for element in self.playlist] 1930 | 1931 | def playlist_append(self, filename, **options): 1932 | """Append a path or URL to the playlist. This does not start playing the file automatically. To do that, use 1933 | ``MPV.loadfile(filename, 'append-play')``.""" 1934 | self.loadfile(filename, 'append', **options) 1935 | 1936 | # "Python stream" logic. This is some porcelain for directly playing data from python generators. 1937 | 1938 | def _python_stream_open(self, uri): 1939 | """Internal handler for python:// protocol streams registered through @python_stream(...) and 1940 | @python_stream_catchall 1941 | """ 1942 | name, = re.fullmatch('python://(.*)', uri).groups() 1943 | 1944 | if name in self._python_streams: 1945 | generator_fun, size = self._python_streams[name] 1946 | else: 1947 | if self._python_stream_catchall is not None: 1948 | generator_fun, size = self._python_stream_catchall(name) 1949 | else: 1950 | raise ValueError('Python stream name not found and no catch-all defined') 1951 | 1952 | return GeneratorStream(generator_fun, size) 1953 | 1954 | def python_stream(self, name=None, size=None): 1955 | """Register a generator for the python stream with the given name. 1956 | 1957 | name is the name, i.e. the part after the "python://" in the URI, that this generator is registered as. 1958 | size is the total number of bytes in the stream (if known). 1959 | 1960 | Any given name can only be registered once. The catch-all can also only be registered once. To unregister a 1961 | stream, call the .unregister function set on the callback. 1962 | 1963 | If name is None (the default), a name and corresponding python:// URI are automatically generated. You can 1964 | access the name through the .stream_name property set on the callback, and the stream URI for passing into 1965 | mpv.play(...) through the .stream_uri property. 1966 | 1967 | The generator signals EOF by returning, manually raising StopIteration or by yielding b'', an empty bytes 1968 | object. 1969 | 1970 | The generator may be called multiple times if libmpv seeks or loops. 1971 | 1972 | See also: @mpv.python_stream_catchall 1973 | 1974 | @mpv.python_stream('foobar') 1975 | def reader(): 1976 | for chunk in chunks: 1977 | yield chunk 1978 | mpv.play('python://foobar') 1979 | mpv.wait_for_playback() 1980 | reader.unregister() 1981 | """ 1982 | def register(cb): 1983 | nonlocal name 1984 | if name is None: 1985 | name = f'__python_mpv_anonymous_python_stream_{id(cb)}__' 1986 | 1987 | if name in self._python_streams: 1988 | raise KeyError('Python stream name "{}" is already registered'.format(name)) 1989 | 1990 | self._python_streams[name] = (cb, size) 1991 | def unregister(): 1992 | if name not in self._python_streams or\ 1993 | self._python_streams[name][0] is not cb: # This is just a basic sanity check 1994 | raise RuntimeError('Python stream has already been unregistered') 1995 | del self._python_streams[name] 1996 | 1997 | cb.unregister = unregister 1998 | cb.stream_name = name 1999 | cb.stream_uri = f'python://{name}' 2000 | return cb 2001 | 2002 | return register 2003 | 2004 | @contextmanager 2005 | def play_context(self): 2006 | """ Context manager for streaming bytes straight into libmpv. 2007 | 2008 | This is a convenience wrapper around python_stream. play_context returns a write method, which you can use in 2009 | the body of the context manager to feed libmpv bytes. All bytes you feed in with write() in the body of a single 2010 | call of this context manager are treated as one single file. A queue is used internally, so this function is 2011 | thread-safe. The queue is unlimited, so it cannot block and is safe to call from async code. You can use this 2012 | function to stream chunked data, e.g. from the network. 2013 | 2014 | Use it like this: 2015 | 2016 | with m.play_context() as write: 2017 | with open(TESTVID, 'rb') as f: 2018 | while (chunk := f.read(65536)): # Get some chunks of bytes 2019 | write(chunk) 2020 | """ 2021 | q = queue.Queue() 2022 | 2023 | EOF = object() # Get some unique object as EOF marker 2024 | @self.python_stream() 2025 | def reader(): 2026 | while (chunk := q.get()) is not EOF: 2027 | if chunk: 2028 | yield chunk 2029 | reader.unregister() 2030 | 2031 | def write(chunk): 2032 | q.put(chunk) 2033 | 2034 | # Start playback before yielding, the first call to reader() will block until write is called at least once. 2035 | self.play(reader.stream_uri) 2036 | yield write 2037 | q.put(EOF) 2038 | 2039 | def play_bytes(self, data): 2040 | """ Play the given bytes object as a single file. """ 2041 | 2042 | @self.python_stream() 2043 | def reader(): 2044 | yield data 2045 | reader.unregister() # unregister itself 2046 | 2047 | self.play(reader.stream_uri) 2048 | 2049 | def python_stream_catchall(self, cb): 2050 | """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a 2051 | function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown). 2052 | 2053 | An invalid URI can be signalled to libmpv by raising a ValueError inside the callback. 2054 | 2055 | See also: @mpv.python_stream(name, size) 2056 | 2057 | @mpv.python_stream_catchall 2058 | def catchall(name): 2059 | if not name.startswith('foo'): 2060 | raise ValueError('Unknown Name') 2061 | 2062 | def foo_reader(): 2063 | with open(name, 'rb') as f: 2064 | while True: 2065 | chunk = f.read(1024) 2066 | if not chunk: 2067 | break 2068 | yield chunk 2069 | return foo_reader, None 2070 | mpv.play('python://foo23') 2071 | mpv.wait_for_playback() 2072 | catchall.unregister() 2073 | """ 2074 | if self._python_stream_catchall is not None: 2075 | raise KeyError('A catch-all python stream is already registered') 2076 | 2077 | self._python_stream_catchall = cb 2078 | def unregister(): 2079 | if self._python_stream_catchall is not cb: 2080 | raise RuntimeError('This catch-all python stream has already been unregistered') 2081 | self._python_stream_catchall = None 2082 | cb.unregister = unregister 2083 | return cb 2084 | 2085 | # Property accessors 2086 | def _get_property(self, name, decoder=strict_decoder, fmt=MpvFormat.NODE): 2087 | self.check_core_alive() 2088 | out = create_string_buffer(sizeof(MpvNode)) 2089 | try: 2090 | cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, out) 2091 | 2092 | if fmt is MpvFormat.OSD_STRING: 2093 | return cast(out, POINTER(c_char_p)).contents.value.decode('utf-8') 2094 | elif fmt is MpvFormat.NODE: 2095 | rv = cast(out, POINTER(MpvNode)).contents.node_value(decoder=decoder) 2096 | _mpv_free_node_contents(out) 2097 | return rv 2098 | else: 2099 | raise TypeError('_get_property only supports NODE and OSD_STRING formats.') 2100 | except PropertyUnavailableError as ex: 2101 | return None 2102 | 2103 | def _set_property(self, name, value): 2104 | self.check_core_alive() 2105 | ename = name.encode('utf-8') 2106 | if isinstance(value, dict): 2107 | _1, _2, _3, pointer = _make_node_str_map(value) 2108 | _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer) 2109 | elif isinstance(value, (list, set)): 2110 | _1, _2, _3, pointer = _make_node_str_list(value) 2111 | _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer) 2112 | else: 2113 | _mpv_set_property_string(self.handle, ename, _mpv_coax_proptype(value)) 2114 | 2115 | def __getattr__(self, name): 2116 | return self._get_property(_py_to_mpv(name), lazy_decoder) 2117 | 2118 | def __setattr__(self, name, value): 2119 | try: 2120 | if name != 'handle' and not name.startswith('_'): 2121 | self._set_property(_py_to_mpv(name), value) 2122 | else: 2123 | super().__setattr__(name, value) 2124 | except AttributeError: 2125 | super().__setattr__(name, value) 2126 | 2127 | def __dir__(self): 2128 | return super().__dir__() + [ name.replace('-', '_') for name in self.property_list ] 2129 | 2130 | @property 2131 | def properties(self): 2132 | return { name: self.option_info(name) for name in self.property_list } 2133 | 2134 | # Dict-like option access 2135 | def __getitem__(self, name, file_local=False): 2136 | """Get an option value.""" 2137 | prefix = 'file-local-options/' if file_local else 'options/' 2138 | return self._get_property(prefix+name, lazy_decoder) 2139 | 2140 | def __setitem__(self, name, value, file_local=False): 2141 | """Set an option value.""" 2142 | prefix = 'file-local-options/' if file_local else 'options/' 2143 | return self._set_property(prefix+name, value) 2144 | 2145 | def __iter__(self): 2146 | """Iterate over all option names.""" 2147 | return iter(self.options) 2148 | 2149 | def option_info(self, name): 2150 | """Get information on the given option.""" 2151 | try: 2152 | return self._get_property('option-info/'+name) 2153 | except AttributeError: 2154 | return None 2155 | 2156 | 2157 | class MpvRenderContext: 2158 | def __init__(self, mpv, api_type, **kwargs): 2159 | self._mpv = mpv 2160 | kwargs['api_type'] = api_type 2161 | 2162 | buf = cast(create_string_buffer(sizeof(MpvRenderCtxHandle)), POINTER(MpvRenderCtxHandle)) 2163 | _mpv_render_context_create(buf, mpv.handle, kwargs_to_render_param_array(kwargs)) 2164 | self._handle = buf.contents 2165 | 2166 | def free(self): 2167 | _mpv_render_context_free(self._handle) 2168 | 2169 | def __setattr__(self, name, value): 2170 | if name.startswith('_'): 2171 | super().__setattr__(name, value) 2172 | 2173 | elif name == 'update_cb': 2174 | func = value if value else (lambda: None) 2175 | self._update_cb = value 2176 | self._update_fn_wrapper = RenderUpdateFn(lambda _userdata: func()) 2177 | _mpv_render_context_set_update_callback(self._handle, self._update_fn_wrapper, None) 2178 | 2179 | else: 2180 | param = MpvRenderParam(name, value) 2181 | _mpv_render_context_set_parameter(self._handle, param) 2182 | 2183 | def __getattr__(self, name): 2184 | if name == 'update_cb': 2185 | return self._update_cb 2186 | 2187 | elif name == 'handle': 2188 | return self._handle 2189 | 2190 | param = MpvRenderParam(name) 2191 | data_type = type(param.data.contents) 2192 | buf = cast(create_string_buffer(sizeof(data_type)), POINTER(data_type)) 2193 | param.data = buf 2194 | _mpv_render_context_get_info(self._handle, param) 2195 | return buf.contents.as_dict() 2196 | 2197 | def update(self): 2198 | """ Calls mpv_render_context_update and returns the MPV_RENDER_UPDATE_FRAME flag (see render.h) """ 2199 | return bool(_mpv_render_context_update(self._handle) & 1) 2200 | 2201 | def render(self, **kwargs): 2202 | _mpv_render_context_render(self._handle, kwargs_to_render_param_array(kwargs)) 2203 | 2204 | def report_swap(self): 2205 | _mpv_render_context_report_swap(self._handle) 2206 | 2207 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | py-modules = ['mpv'] 7 | 8 | [project] 9 | name = "mpv" 10 | version = "v1.0.8" 11 | description = "A python interface to the mpv media player" 12 | readme = "README.rst" 13 | authors = [{name = "jaseg", email = "mpv@jaseg.de"}] 14 | license = {text = "GPLv2+ or LGPLv2.1+"} 15 | requires-python = ">=3.9" 16 | keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia'] 17 | classifiers = [ 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Environment :: X11 Applications', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 22 | 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', 23 | 'Natural Language :: English', 24 | 'Operating System :: POSIX', 25 | 'Programming Language :: C', 26 | 'Programming Language :: Python :: 3.9', 27 | 'Programming Language :: Python :: 3.10', 28 | 'Programming Language :: Python :: 3.11', 29 | 'Programming Language :: Python :: 3.12', 30 | 'Topic :: Multimedia :: Sound/Audio :: Players', 31 | 'Topic :: Multimedia :: Video :: Display' 32 | ] 33 | 34 | [project.urls] 35 | homepage = "https://github.com/jaseg/python-mpv" 36 | 37 | [project.optional-dependencies] 38 | screenshot_raw = ["Pillow"] 39 | test = [ 40 | 'PyVirtualDisplay', 41 | 'pytest', 42 | 'pytest-rerunfailures' 43 | ] 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaseg/python-mpv/93c4de9bb7a0e1cfcb9d305a58a326bb24ef9d47/tests/__init__.py -------------------------------------------------------------------------------- /tests/sub_test.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,500 --> 00:00:01,000 3 | This is 4 | a subtitle test. 5 | 6 | 2 7 | 00:00:01,000 --> 00:00:02,000 8 | This is the second subtitle line. 9 | 10 | -------------------------------------------------------------------------------- /tests/test.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaseg/python-mpv/93c4de9bb7a0e1cfcb9d305a58a326bb24ef9d47/tests/test.webm -------------------------------------------------------------------------------- /tests/test_mpv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vim: ts=4 sw=4 et 4 | # 5 | # Python MPV library module 6 | # Copyright (C) 2017-2022 Sebastian Götte 7 | # 8 | # This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public 9 | # License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 13 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with this program; if not, write to the Free 16 | # Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | # 18 | 19 | import unittest 20 | from unittest import mock 21 | import threading 22 | from contextlib import contextmanager 23 | import os.path 24 | import os 25 | import time 26 | from concurrent.futures import Future, InvalidStateError 27 | 28 | os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] 29 | 30 | import mpv 31 | 32 | 33 | if os.name == 'nt': 34 | Display = mock.Mock() 35 | testvo='gpu' 36 | 37 | else: 38 | from pyvirtualdisplay import Display 39 | testvo=os.environ.get('PY_MPV_TEST_VO', 'x11') 40 | 41 | 42 | TESTVID = os.path.join(os.path.dirname(__file__), 'test.webm') 43 | TESTSRT = os.path.join(os.path.dirname(__file__), 'sub_test.srt') 44 | MPV_ERRORS = [ l(ec) for ec, l in mpv.ErrorCode.EXCEPTION_DICT.items() if l ] 45 | SKIP_TESTS = os.environ.get('PY_MPV_SKIP_TESTS', '').split() 46 | 47 | 48 | def timed_print(): 49 | start_time = time.time() 50 | def do_print(level, prefix, text): 51 | td = time.time() - start_time 52 | print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text.strip()), flush=True) 53 | return do_print 54 | 55 | 56 | class MpvTestCase(unittest.TestCase): 57 | def setUp(self): 58 | self.disp = Display() 59 | self.disp.start() 60 | self.m = mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) 61 | 62 | def tearDown(self): 63 | self.m.terminate() 64 | self.disp.stop() 65 | 66 | 67 | class TestProperties(MpvTestCase): 68 | @contextmanager 69 | def swallow_mpv_errors(self, exception_exceptions=[]): 70 | try: 71 | yield 72 | except Exception as e: 73 | if any(e.args[:2] == ex.args for ex in MPV_ERRORS): 74 | if e.args[1] not in exception_exceptions: 75 | raise 76 | else: 77 | raise 78 | 79 | def test_read(self): 80 | self.m.loop = 'inf' 81 | self.m.play(TESTVID) 82 | while self.m.core_idle: 83 | time.sleep(0.05) 84 | for name in sorted(self.m.property_list): 85 | name = name.replace('-', '_') 86 | with self.subTest(property_name=name), self.swallow_mpv_errors([ 87 | mpv.ErrorCode.PROPERTY_UNAVAILABLE, 88 | mpv.ErrorCode.PROPERTY_ERROR, 89 | mpv.ErrorCode.PROPERTY_NOT_FOUND]): 90 | getattr(self.m, name) 91 | 92 | def test_write(self): 93 | self.m.loop = 'inf' 94 | self.m.play(TESTVID) 95 | while self.m.core_idle: 96 | time.sleep(0.05) 97 | check_canaries = lambda: os.path.exists('100') or os.path.exists('foo') 98 | for name in sorted(self.m.property_list): 99 | # See issue #108 and upstream mpv issues #7919 and #7920. 100 | if name in ('demuxer', 'audio-demuxer', 'audio-files'): 101 | continue 102 | # These may cause files to be created 103 | if name in ('external-file', 'heartbeat-cmd', 'wid', 'dump-stats', 'log-file') or name.startswith('input-'): 104 | continue 105 | # Caues segmentation faults on wayland 106 | if name in ('current-window-scale',): 107 | continue 108 | name = name.replace('-', '_') 109 | old_canaries = check_canaries() 110 | with self.subTest(property_name=name), self.swallow_mpv_errors([ 111 | mpv.ErrorCode.PROPERTY_UNAVAILABLE, 112 | mpv.ErrorCode.PROPERTY_ERROR, 113 | mpv.ErrorCode.PROPERTY_FORMAT, 114 | mpv.ErrorCode.PROPERTY_NOT_FOUND]): # This is due to a bug with option-mapped properties in mpv 0.18.1 115 | setattr(self.m, name, 100) 116 | setattr(self.m, name, 1) 117 | setattr(self.m, name, 0) 118 | setattr(self.m, name, -1) 119 | setattr(self.m, name, 1) 120 | setattr(self.m, name, 1.0) 121 | setattr(self.m, name, 0.0) 122 | setattr(self.m, name, -1.0) 123 | setattr(self.m, name, float('nan')) 124 | setattr(self.m, name, 'foo') 125 | setattr(self.m, name, '') 126 | setattr(self.m, name, 'bazbazbaz'*1000) 127 | setattr(self.m, name, b'foo') 128 | setattr(self.m, name, b'') 129 | setattr(self.m, name, b'bazbazbaz'*1000) 130 | setattr(self.m, name, True) 131 | setattr(self.m, name, False) 132 | if not old_canaries and check_canaries(): 133 | raise UserWarning('Property test for {} produced files on file system, might not be safe.'.format(name)) 134 | 135 | def test_property_bounce(self): 136 | self.m.aid = False 137 | self.assertEqual(self.m.audio, False) 138 | self.m.aid = 'auto' 139 | self.assertEqual(self.m.audio, 'auto') 140 | self.m.aid = 'no' 141 | self.assertEqual(self.m.audio, False) 142 | self.m.audio = 'auto' 143 | self.assertEqual(self.m.aid, 'auto') 144 | self.m.audio = False 145 | self.assertEqual(self.m.aid, False) 146 | self.m.audio = 'auto' 147 | self.assertEqual(self.m.aid, 'auto') 148 | self.m.audio = 'no' 149 | self.assertEqual(self.m.aid, False) 150 | 151 | def test_array_property_bounce(self): 152 | self.m.alang = 'en' 153 | self.assertEqual(self.m.alang, ['en']) 154 | self.m.alang = 'de' 155 | self.assertEqual(self.m.alang, ['de']) 156 | self.m.alang = ['de', 'en'] 157 | self.assertEqual(self.m.alang, ['de', 'en']) 158 | self.m.alang = 'de,en' 159 | self.assertEqual(self.m.alang, ['de', 'en']) 160 | self.m.alang = ['de,en'] 161 | self.assertEqual(self.m.alang, ['de,en']) 162 | 163 | def test_osd_property_bounce(self): 164 | self.m.alang = ['en'] 165 | self.assertEqual(self.m.osd.alang, 'en') 166 | self.m.alang = ['de'] 167 | self.assertEqual(self.m.osd.alang, 'de') 168 | self.m.alang = ['en', 'de'] 169 | self.assertEqual(self.m.osd.alang, 'en,de') 170 | 171 | def test_raw_property_bounce(self): 172 | self.m.alang = 'en' 173 | self.assertEqual(self.m.raw.alang, [b'en']) 174 | self.m.alang = 'de' 175 | self.assertEqual(self.m.raw.alang, [b'de']) 176 | self.m.alang = ['de', 'en'] 177 | self.assertEqual(self.m.raw.alang, [b'de', b'en']) 178 | self.m.alang = 'de,en' 179 | self.assertEqual(self.m.raw.alang, [b'de', b'en']) 180 | self.m.alang = ['de,en'] 181 | self.assertEqual(self.m.raw.alang, [b'de,en']) 182 | 183 | def test_property_decoding_invalid_utf8(self): 184 | invalid_utf8 = b'foo\xc3\x28bar' 185 | self.m.alang = invalid_utf8 186 | self.assertEqual(self.m.raw.alang, [invalid_utf8]) 187 | with self.assertRaises(UnicodeDecodeError): 188 | self.m.strict.alang 189 | with self.assertRaises(UnicodeDecodeError): 190 | # alang is considered safe and pasted straight into the OSD string. But OSD strings should always be valid 191 | # UTF-8. This test may be removed in case OSD encoding sanitization is handled differently in the future. 192 | self.m.osd.alang 193 | 194 | def test_property_decoding_valid_utf8(self): 195 | valid_utf8 = 'pröpérty' 196 | self.m.alang = valid_utf8 197 | self.assertEqual(self.m.alang, [valid_utf8]) 198 | self.assertEqual(self.m.raw.alang, [valid_utf8.encode('utf-8')]) 199 | self.assertEqual(self.m.osd.alang, valid_utf8) 200 | self.assertEqual(self.m.strict.alang, [valid_utf8]) 201 | 202 | def test_property_decoding_multi(self): 203 | valid_utf8 = 'pröpérty' 204 | invalid_utf8 = b'foo\xc3\x28bar' 205 | self.m.alang = [valid_utf8, 'foo', invalid_utf8] 206 | self.assertEqual(self.m.alang, [valid_utf8, 'foo', invalid_utf8]) 207 | self.assertEqual(self.m.raw.alang, [valid_utf8.encode('utf-8'), b'foo', invalid_utf8]) 208 | with self.assertRaises(UnicodeDecodeError): 209 | self.m.strict.alang 210 | with self.assertRaises(UnicodeDecodeError): 211 | # See comment in test_property_decoding_invalid_utf8 212 | self.m.osd.alang 213 | 214 | def test_dict_valued_property(self): 215 | nasty_stuff = '\xe2\x80\x8e Mozilla/5.0 Foobar \xe2\x80\x8e \xe2\x80\x81' 216 | self.m.ytdl_raw_options = {'user-agent': nasty_stuff} 217 | self.assertEqual(self.m.ytdl_raw_options, {'user-agent': nasty_stuff}) 218 | 219 | def test_option_read(self): 220 | self.m.loop = 'inf' 221 | self.m.play(TESTVID) 222 | while self.m.core_idle: 223 | time.sleep(0.05) 224 | for name in sorted(self.m): 225 | with self.subTest(option_name=name), self.swallow_mpv_errors([ 226 | mpv.ErrorCode.PROPERTY_UNAVAILABLE, mpv.ErrorCode.PROPERTY_NOT_FOUND, mpv.ErrorCode.PROPERTY_ERROR]): 227 | self.m[name] 228 | 229 | def test_multivalued_option(self): 230 | self.m['external-files'] = ['test.webm', b'test.webm'] 231 | self.assertEqual(self.m['external-files'], ['test.webm', 'test.webm']) 232 | 233 | 234 | class ObservePropertyTest(MpvTestCase): 235 | def test_observe_property(self): 236 | handler = mock.Mock() 237 | 238 | m = self.m 239 | m.observe_property('vid', handler) 240 | 241 | time.sleep(0.1) 242 | m.play(TESTVID) 243 | 244 | time.sleep(0.5) #couple frames 245 | m.unobserve_property('vid', handler) 246 | 247 | time.sleep(0.1) #couple frames 248 | m.terminate() # needed for synchronization of event thread 249 | handler.assert_has_calls([mock.call('vid', 'auto')]) 250 | 251 | def test_property_observer_decorator(self): 252 | handler = mock.Mock() 253 | 254 | m = self.m 255 | m.play(TESTVID) 256 | 257 | m.slang = 'ru' 258 | m.mute = True 259 | 260 | @m.property_observer('mute') 261 | @m.property_observer('slang') 262 | def foo(*args, **kwargs): 263 | handler(*args, **kwargs) 264 | 265 | m.mute = False 266 | m.slang = 'jp' 267 | self.assertEqual(m.mute, False) 268 | self.assertEqual(m.slang, ['jp']) 269 | 270 | # Wait for tick. AFAICT property events are only generated at regular 271 | # intervals, and if we change a property too fast we don't get any 272 | # events. This is a limitation of the upstream API. 273 | time.sleep(0.1) 274 | # Another API limitation is that the order of property change events on 275 | # different properties does not necessarily exactly match the order in 276 | # which these properties were previously accessed. Thus, any_order. 277 | handler.assert_has_calls([ 278 | mock.call('mute', False), 279 | mock.call('slang', ['jp'])], 280 | any_order=True) 281 | handler.reset_mock() 282 | 283 | m.mute = True 284 | m.slang = 'ru' 285 | self.assertEqual(m.mute, True) 286 | self.assertEqual(m.slang, ['ru']) 287 | 288 | time.sleep(0.1) 289 | foo.unobserve_mpv_properties() 290 | 291 | m.mute = False 292 | m.slang = 'jp' 293 | m.mute = True 294 | m.slang = 'ru' 295 | m.terminate() # needed for synchronization of event thread 296 | handler.assert_has_calls([ 297 | mock.call('mute', True), 298 | mock.call('slang', ['ru'])], 299 | any_order=True) 300 | 301 | 302 | class KeyBindingTest(MpvTestCase): 303 | def test_register_direct_cmd(self): 304 | self.m.register_key_binding('a', 'playlist-clear') 305 | self.assertEqual(self.m._key_binding_handlers, {}) 306 | self.m.register_key_binding('Ctrl+Shift+a', 'playlist-clear') 307 | self.m.unregister_key_binding('a') 308 | self.m.unregister_key_binding('Ctrl+Shift+a') 309 | 310 | def test_register_direct_fun(self): 311 | b = mpv.MPV._binding_name 312 | 313 | def reg_test_fun(state, name, char): 314 | pass 315 | 316 | self.m.register_key_binding('a', reg_test_fun) 317 | self.assertIn(b('a'), self.m._key_binding_handlers) 318 | self.assertEqual(self.m._key_binding_handlers[b('a')], reg_test_fun) 319 | 320 | self.m.unregister_key_binding('a') 321 | self.assertNotIn(b('a'), self.m._key_binding_handlers) 322 | 323 | def test_register_direct_bound_method(self): 324 | b = mpv.MPV._binding_name 325 | 326 | class RegTestCls: 327 | def method(self, state, name, char): 328 | pass 329 | instance = RegTestCls() 330 | 331 | self.m.register_key_binding('a', instance.method) 332 | self.assertIn(b('a'), self.m._key_binding_handlers) 333 | self.assertEqual(self.m._key_binding_handlers[b('a')], instance.method) 334 | 335 | self.m.unregister_key_binding('a') 336 | self.assertNotIn(b('a'), self.m._key_binding_handlers) 337 | 338 | def test_register_decorator_fun(self): 339 | b = mpv.MPV._binding_name 340 | 341 | @self.m.key_binding('a') 342 | def reg_test_fun(state, name, char): 343 | pass 344 | self.assertEqual(reg_test_fun.mpv_key_bindings, ['a']) 345 | self.assertIn(b('a'), self.m._key_binding_handlers) 346 | self.assertEqual(self.m._key_binding_handlers[b('a')], reg_test_fun) 347 | 348 | reg_test_fun.unregister_mpv_key_bindings() 349 | self.assertNotIn(b('a'), self.m._key_binding_handlers) 350 | 351 | def test_register_decorator_fun_chaining(self): 352 | b = mpv.MPV._binding_name 353 | 354 | @self.m.key_binding('a') 355 | @self.m.key_binding('b') 356 | def reg_test_fun(state, name, char): 357 | pass 358 | 359 | @self.m.key_binding('c') 360 | def reg_test_fun_2_stay_intact(state, name, char): 361 | pass 362 | 363 | self.assertEqual(reg_test_fun.mpv_key_bindings, ['b', 'a']) 364 | self.assertIn(b('a'), self.m._key_binding_handlers) 365 | self.assertIn(b('b'), self.m._key_binding_handlers) 366 | self.assertIn(b('c'), self.m._key_binding_handlers) 367 | self.assertEqual(self.m._key_binding_handlers[b('a')], reg_test_fun) 368 | self.assertEqual(self.m._key_binding_handlers[b('b')], reg_test_fun) 369 | 370 | reg_test_fun.unregister_mpv_key_bindings() 371 | self.assertNotIn(b('a'), self.m._key_binding_handlers) 372 | self.assertNotIn(b('b'), self.m._key_binding_handlers) 373 | self.assertIn(b('c'), self.m._key_binding_handlers) 374 | 375 | def test_wait_for_event_error_forwarding(self): 376 | self.m.play(TESTVID) 377 | 378 | def check(evt): 379 | raise ValueError('fnord') 380 | 381 | with self.assertRaises(ValueError): 382 | self.m.wait_for_event('end_file', cond=check) 383 | 384 | def test_wait_for_property_error_forwarding(self): 385 | def run(): 386 | nonlocal self 387 | self.m.wait_until_playing(timeout=2) 388 | self.m.mute = True 389 | t = threading.Thread(target=run, daemon=True) 390 | t.start() 391 | 392 | def cond(mute): 393 | if mute: 394 | raise ValueError('fnord') 395 | 396 | with self.assertRaises(ValueError): 397 | self.m.play(TESTVID) 398 | self.m.wait_for_property('mute', cond=cond) 399 | 400 | def test_register_simple_decorator_fun_chaining(self): 401 | self.m.loop = 'inf' 402 | self.m.play(TESTVID) 403 | self.m.wait_until_playing(timeout=2) 404 | 405 | handler1, handler2 = mock.Mock(), mock.Mock() 406 | 407 | @self.m.on_key_press('a') 408 | @self.m.on_key_press('b') 409 | def reg_test_fun(*args, **kwargs): 410 | handler1(*args, **kwargs) 411 | 412 | @self.m.on_key_press('c') 413 | def reg_test_fun_2_stay_intact(*args, **kwargs): 414 | handler2(*args, **kwargs) 415 | 416 | self.assertEqual(reg_test_fun.mpv_key_bindings, ['b', 'a']) 417 | 418 | def keypress_and_sync(key): 419 | with self.m.prepare_and_wait_for_event('client_message', timeout=2): 420 | self.m.keypress(key) 421 | 422 | keypress_and_sync('a') 423 | handler1.assert_has_calls([ mock.call() ]) 424 | handler2.assert_has_calls([]) 425 | handler1.reset_mock() 426 | 427 | self.m.keypress('x') 428 | self.m.keypress('X') 429 | keypress_and_sync('b') 430 | handler1.assert_has_calls([ mock.call() ]) 431 | handler2.assert_has_calls([]) 432 | handler1.reset_mock() 433 | 434 | keypress_and_sync('c') 435 | self.m.keypress('B') 436 | handler1.assert_has_calls([]) 437 | handler2.assert_has_calls([ mock.call() ]) 438 | handler2.reset_mock() 439 | 440 | reg_test_fun.unregister_mpv_key_bindings() 441 | self.m.keypress('a') 442 | keypress_and_sync('c') 443 | self.m.keypress('x') 444 | self.m.keypress('A') 445 | handler1.assert_has_calls([]) 446 | handler2.assert_has_calls([ mock.call() ]) 447 | 448 | 449 | class TestStreams(unittest.TestCase): 450 | def test_python_stream(self): 451 | handler = mock.Mock() 452 | 453 | disp = Display() 454 | disp.start() 455 | m = mpv.MPV(vo=testvo) 456 | def cb(evt): 457 | handler(evt.as_dict(decoder=mpv.lazy_decoder)) 458 | m.register_event_callback(cb) 459 | 460 | @m.python_stream('foo') 461 | def foo_gen(): 462 | with open(TESTVID, 'rb') as f: 463 | yield f.read() 464 | 465 | @m.python_stream('bar') 466 | def bar_gen(): 467 | yield b'' 468 | 469 | m.play('python://foo') 470 | m.wait_for_playback() 471 | handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1}) 472 | handler.reset_mock() 473 | 474 | m.play('python://bar') 475 | m.wait_for_playback() 476 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 2, 'file_error': 'unrecognized file format'}) 477 | handler.reset_mock() 478 | 479 | m.play('python://baz') 480 | m.wait_for_playback() 481 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 3, 'file_error': 'loading failed'}) 482 | handler.reset_mock() 483 | 484 | m.play('foo://foo') 485 | m.wait_for_playback() 486 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 4, 'file_error': 'loading failed'}) 487 | handler.reset_mock() 488 | 489 | foo_gen.unregister() 490 | 491 | m.play('python://foo') 492 | m.wait_for_playback() 493 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 5, 'file_error': 'loading failed'}) 494 | handler.reset_mock() 495 | 496 | m.play('python://bar') 497 | m.wait_for_playback() 498 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 6, 'file_error': 'unrecognized file format'}) 499 | handler.reset_mock() 500 | 501 | m.terminate() 502 | disp.stop() 503 | 504 | def test_custom_stream(self): 505 | handler = mock.Mock() 506 | fail_mock = mock.Mock(side_effect=ValueError) 507 | stream_mock = mock.Mock() 508 | stream_mock.seek = mock.Mock(return_value=0) 509 | stream_mock.read = mock.Mock(return_value=b'') 510 | 511 | disp = Display() 512 | disp.start() 513 | m = mpv.MPV(vo=testvo, video=False) 514 | def cb(evt): 515 | handler(evt.as_dict(decoder=mpv.lazy_decoder)) 516 | m.register_event_callback(cb) 517 | 518 | m.register_stream_protocol('pythonfail', fail_mock) 519 | 520 | @m.register_stream_protocol('pythonsuccess') 521 | def open_fn(uri): 522 | self.assertEqual(uri, 'pythonsuccess://foo') 523 | return stream_mock 524 | 525 | m.play('pythondoesnotexist://foo') 526 | m.wait_for_playback() 527 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 1, 'file_error': 'loading failed'}) 528 | handler.reset_mock() 529 | 530 | m.play('pythonfail://foo') 531 | m.wait_for_playback() 532 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 2, 'file_error': 'loading failed'}) 533 | handler.reset_mock() 534 | 535 | m.play('pythonsuccess://foo') 536 | m.wait_for_playback() 537 | stream_mock.seek.assert_any_call(0) 538 | stream_mock.read.assert_called() 539 | handler.assert_any_call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 3, 'file_error': 'unrecognized file format'}) 540 | 541 | m.terminate() 542 | disp.stop() 543 | 544 | def test_stream_open_exception(self): 545 | disp = Display() 546 | disp.start() 547 | m = mpv.MPV(vo=testvo, video=False) 548 | 549 | @m.register_stream_protocol('raiseerror') 550 | def open_fn(uri): 551 | raise SystemError() 552 | 553 | waiting = threading.Semaphore() 554 | result = Future() 555 | def run(): 556 | result.set_running_or_notify_cancel() 557 | try: 558 | waiting.release() 559 | m.wait_for_playback() 560 | result.set_result(False) 561 | except SystemError: 562 | result.set_result(True) 563 | except Exception: 564 | result.set_result(False) 565 | 566 | t = threading.Thread(target=run, daemon=True) 567 | t.start() 568 | 569 | with waiting: 570 | time.sleep(0.2) 571 | m.play('raiseerror://foo') 572 | 573 | m.wait_for_playback(catch_errors=False) 574 | try: 575 | assert result.result() 576 | finally: 577 | m.terminate() 578 | disp.stop() 579 | 580 | def test_python_stream_exception(self): 581 | disp = Display() 582 | disp.start() 583 | m = mpv.MPV(vo=testvo) 584 | 585 | @m.python_stream('foo') 586 | def foo_gen(): 587 | with open(TESTVID, 'rb') as f: 588 | yield f.read(100) 589 | raise SystemError() 590 | 591 | waiting = threading.Semaphore() 592 | result = Future() 593 | def run(): 594 | result.set_running_or_notify_cancel() 595 | try: 596 | waiting.release() 597 | m.wait_for_playback() 598 | result.set_result(False) 599 | except SystemError: 600 | result.set_result(True) 601 | except Exception: 602 | result.set_result(False) 603 | 604 | t = threading.Thread(target=run, daemon=True) 605 | t.start() 606 | 607 | with waiting: 608 | time.sleep(0.2) 609 | m.play('python://foo') 610 | 611 | m.wait_for_playback(catch_errors=False) 612 | try: 613 | assert result.result() 614 | finally: 615 | m.terminate() 616 | disp.stop() 617 | 618 | def test_stream_open_forward(self): 619 | disp = Display() 620 | disp.start() 621 | m = mpv.MPV(vo=testvo, video=False) 622 | 623 | @m.register_stream_protocol('raiseerror') 624 | def open_fn(uri): 625 | raise ValueError() 626 | 627 | waiting = threading.Semaphore() 628 | result = Future() 629 | def run(): 630 | result.set_running_or_notify_cancel() 631 | try: 632 | waiting.release() 633 | m.wait_for_playback() 634 | result.set_result(True) 635 | except Exception: 636 | result.set_result(False) 637 | 638 | t = threading.Thread(target=run, daemon=True) 639 | t.start() 640 | 641 | with waiting: 642 | time.sleep(0.2) 643 | m.play('raiseerror://foo') 644 | 645 | m.wait_for_playback(catch_errors=False) 646 | try: 647 | assert result.result() 648 | finally: 649 | m.terminate() 650 | disp.stop() 651 | 652 | def test_play_context(self): 653 | handler = mock.Mock() 654 | 655 | disp = Display() 656 | disp.start() 657 | m = mpv.MPV(vo=testvo) 658 | def cb(evt): 659 | handler(evt.as_dict(decoder=mpv.lazy_decoder)) 660 | m.register_event_callback(cb) 661 | 662 | with m.play_context() as write: 663 | with open(TESTVID, 'rb') as f: 664 | write(f.read(100)) 665 | write(f.read(1000)) 666 | write(f.read()) 667 | 668 | m.wait_for_playback() 669 | handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1}) 670 | m.terminate() 671 | disp.stop() 672 | 673 | def test_play_bytes(self): 674 | handler = mock.Mock() 675 | 676 | disp = Display() 677 | disp.start() 678 | m = mpv.MPV(vo=testvo) 679 | def cb(evt): 680 | handler(evt.as_dict(decoder=mpv.lazy_decoder)) 681 | m.register_event_callback(cb) 682 | 683 | with open(TESTVID, 'rb') as f: 684 | m.play_bytes(f.read()) 685 | 686 | m.wait_for_playback() 687 | handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1}) 688 | m.terminate() 689 | disp.stop() 690 | 691 | 692 | class TestLifecycle(unittest.TestCase): 693 | def test_create_destroy(self): 694 | thread_names = lambda: [ t.name for t in threading.enumerate() ] 695 | self.assertNotIn('MPVEventHandlerThread', thread_names()) 696 | m = mpv.MPV() 697 | self.assertIn('MPVEventHandlerThread', thread_names()) 698 | m.terminate() 699 | self.assertNotIn('MPVEventHandlerThread', thread_names()) 700 | 701 | def test_flags(self): 702 | with self.assertRaises(AttributeError): 703 | mpv.MPV('this-option-does-not-exist') 704 | m = mpv.MPV('cursor-autohide-fs-only', 'fs', video=False) 705 | self.assertTrue(m.fullscreen) 706 | self.assertEqual(m.cursor_autohide, 1000) 707 | m.terminate() 708 | 709 | def test_options(self): 710 | with self.assertRaises(AttributeError): 711 | mpv.MPV(this_option_does_not_exists=23) 712 | m = mpv.MPV(osd_level=0, loop='inf', deinterlace=False) 713 | self.assertEqual(m.osd_level, 0) 714 | # For compatibility with mpv master (v0.32.0-585-gfba1c681b8) accept both 715 | self.assertIn(m.loop, ['inf', True]) 716 | self.assertEqual(m.deinterlace, False) 717 | m.terminate() 718 | 719 | def test_event_callback(self): 720 | handler = mock.Mock() 721 | m = mpv.MPV(video=False) 722 | def cb(evt): 723 | handler(evt.as_dict(decoder=mpv.lazy_decoder)) 724 | m.register_event_callback(cb) 725 | m.play(TESTVID) 726 | m.wait_for_playback() 727 | 728 | m.unregister_event_callback(cb) 729 | handler.assert_has_calls([ 730 | mock.call({'event': 'start-file', 'playlist_entry_id': 1}), 731 | mock.call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 1, 'file_error': 'no audio or video data played'}) 732 | ], any_order=True) 733 | time.sleep(1) 734 | handler.reset_mock() 735 | m.terminate() 736 | handler.assert_not_called() 737 | 738 | def test_wait_for_property_negative(self): 739 | self.disp = Display() 740 | self.disp.start() 741 | m = mpv.MPV(vo=testvo) 742 | m.play(TESTVID) 743 | result = Future() 744 | def run(): 745 | nonlocal self 746 | result.set_running_or_notify_cancel() 747 | try: 748 | m.wait_for_property('mute') 749 | result.set_result(False) 750 | except mpv.ShutdownError: 751 | result.set_result(True) 752 | t = threading.Thread(target=run, daemon=True) 753 | t.start() 754 | time.sleep(1) 755 | m.terminate() 756 | time.sleep(1) 757 | t.join() 758 | self.disp.stop() 759 | assert result.result() 760 | 761 | def test_wait_for_property_positive(self): 762 | self.disp = Display() 763 | self.disp.start() 764 | handler = mock.Mock() 765 | m = mpv.MPV(vo=testvo) 766 | m.play(TESTVID) 767 | def run(): 768 | nonlocal self 769 | m.wait_for_property('mute') 770 | handler() 771 | t = threading.Thread(target=run, daemon=True) 772 | t.start() 773 | m.wait_until_playing(timeout=2) 774 | m.mute = True 775 | t.join() 776 | m.terminate() 777 | time.sleep(1) 778 | handler.assert_called() 779 | self.disp.stop() 780 | 781 | def test_wait_for_event(self): 782 | self.disp = Display() 783 | self.disp.start() 784 | m = mpv.MPV(vo=testvo) 785 | m.play(TESTVID) 786 | result = Future() 787 | def run(): 788 | nonlocal self 789 | result.set_running_or_notify_cancel() 790 | try: 791 | m.wait_for_event('seek') 792 | result.set_result(False) 793 | except mpv.ShutdownError: 794 | result.set_result(True) 795 | t = threading.Thread(target=run, daemon=True) 796 | t.start() 797 | time.sleep(1) 798 | m.terminate() 799 | t.join() 800 | self.disp.stop() 801 | assert result.result() 802 | 803 | def test_wait_for_property_shutdown(self): 804 | self.disp = Display() 805 | self.disp.start() 806 | m = mpv.MPV(vo=testvo) 807 | m.play(TESTVID) 808 | with self.assertRaises(mpv.ShutdownError): 809 | # level_sensitive=false needed to prevent get_property on dead 810 | # handle 811 | with m.prepare_and_wait_for_property('mute', level_sensitive=False): 812 | m.terminate() 813 | time.sleep(1) 814 | self.disp.stop() 815 | 816 | @unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first") 817 | def test_wait_for_property_event_overflow(self): 818 | self.disp = Display() 819 | self.disp.start() 820 | m = mpv.MPV(vo=testvo) 821 | m.play(TESTVID) 822 | with self.assertRaises(mpv.EventOverflowError): 823 | # level_sensitive=false needed to prevent get_property on dead 824 | # handle 825 | with m.prepare_and_wait_for_property('mute', cond=lambda val: time.sleep(0.001)): 826 | for i in range(10000): 827 | try: 828 | # We really have to try hard to fill up the queue here. Simple async commands will not work, 829 | # since then command_async will throw a memory error first. Property changes also do not work, 830 | # since they are only processsed when the event loop is idle. This here works reliably. 831 | m.command_async('script-message', 'foo', 'bar') 832 | except: 833 | pass 834 | m.terminate() 835 | time.sleep(1) 836 | self.disp.stop() 837 | 838 | def test_wait_for_event_shutdown(self): 839 | self.disp = Display() 840 | self.disp.start() 841 | m = mpv.MPV(vo=testvo) 842 | m.play(TESTVID) 843 | with self.assertRaises(mpv.ShutdownError): 844 | with m.prepare_and_wait_for_event('seek'): 845 | m.terminate() 846 | self.disp.stop() 847 | 848 | def test_wait_for_shutdown(self): 849 | self.disp = Display() 850 | self.disp.start() 851 | m = mpv.MPV(vo=testvo) 852 | m.play(TESTVID) 853 | with self.assertRaises(mpv.ShutdownError): 854 | with m.prepare_and_wait_for_event(None) as result: 855 | m.terminate() 856 | result.result() 857 | self.disp.stop() 858 | 859 | def test_log_handler(self): 860 | handler = mock.Mock() 861 | self.disp = Display() 862 | self.disp.start() 863 | m = mpv.MPV(vo=testvo, log_handler=handler) 864 | m.play(TESTVID) 865 | # Wait for playback to start 866 | m.wait_until_playing(timeout=2) 867 | m.command("print-text", 'This is a python-mpv test') 868 | m.wait_for_playback() 869 | m.terminate() 870 | for call in handler.mock_calls: 871 | _1, (a, b, c), _2 = call 872 | if a == 'info' and b == 'cplayer' and 'This is a python-mpv test' in c: 873 | break 874 | else: 875 | self.fail('"Test log entry not found in log handler calls: '+','.join(repr(call) for call in handler.mock_calls)) 876 | self.disp.stop() 877 | 878 | 879 | class CommandTests(MpvTestCase): 880 | 881 | def test_loadfile_with_subtitles(self): 882 | handler = mock.Mock() 883 | self.m.property_observer('sub-text')(handler) 884 | 885 | self.m.loadfile(TESTVID, sub_file=TESTSRT) 886 | 887 | self.m.wait_for_playback() 888 | handler.assert_any_call('sub-text', 'This is\na subtitle test.') 889 | handler.assert_any_call('sub-text', 'This is the second subtitle line.') 890 | 891 | def test_sub_add(self): 892 | handler = mock.Mock() 893 | self.m.property_observer('sub-text')(handler) 894 | time.sleep(0.5) 895 | 896 | self.m.loadfile(TESTVID) 897 | self.m.wait_until_playing(timeout=2) 898 | self.m.sub_add(TESTSRT) 899 | 900 | self.m.wait_for_playback() 901 | handler.assert_any_call('sub-text', 'This is\na subtitle test.') 902 | handler.assert_any_call('sub-text', 'This is the second subtitle line.') 903 | 904 | def test_async_command(self): 905 | handler = mock.Mock() 906 | callback = mock.Mock() 907 | self.m.property_observer('sub-text')(handler) 908 | time.sleep(0.5) 909 | 910 | self.m.loadfile(TESTVID) 911 | self.m.wait_until_playing(timeout=2) 912 | self.m.command_async('sub_add', TESTSRT, callback=callback) 913 | reply = self.m.command_async('expand-text', 'test ${mute}') 914 | assert reply.result() == 'test no' 915 | 916 | self.m.wait_for_playback() 917 | handler.assert_any_call('sub-text', 'This is\na subtitle test.') 918 | handler.assert_any_call('sub-text', 'This is the second subtitle line.') 919 | callback.assert_any_call(None, None) 920 | 921 | 922 | class RegressionTests(MpvTestCase): 923 | 924 | def test_wait_for_property_concurrency(self): 925 | players = [mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) for i in range(2)] 926 | try: 927 | for _ in range(150): 928 | for player in players: 929 | player.loadfile('tests/test.webm', loop='inf') 930 | for player in players: 931 | player.wait_for_property('seekable') 932 | for player in players: 933 | player.seek(0, reference='absolute', precision='exact') 934 | 935 | except InvalidStateError: 936 | self.fail('InvalidStateError thrown from wait_for_property') 937 | 938 | finally: 939 | for player in players: 940 | player.terminate() 941 | 942 | def test_unobserve_property_runtime_error(self): 943 | """ 944 | Ensure a `RuntimeError` is not thrown within 945 | `unobserve_property`. 946 | """ 947 | handler = mock.Mock() 948 | 949 | self.m.observe_property('loop', handler) 950 | 951 | try: 952 | self.m.unobserve_property('loop', handler) 953 | except RuntimeError: 954 | self.fail( 955 | """ 956 | "RuntimeError" exception thrown within 957 | `unobserve_property` 958 | """, 959 | ) 960 | 961 | def test_instance_method_property_observer(self): 962 | """ 963 | Ensure that bound method objects can be used as property observers. 964 | See issue #26 965 | """ 966 | handler = mock.Mock() 967 | m = self.m 968 | 969 | class T(object): 970 | def t(self, *args, **kw): 971 | handler(*args, **kw) 972 | t = T() 973 | 974 | m.slang = 'ru' 975 | time.sleep(0.5) 976 | 977 | m.observe_property('slang', t.t) 978 | time.sleep(0.5) 979 | 980 | m.slang = 'jp' 981 | time.sleep(0.5) 982 | 983 | m.slang = 'ru' 984 | time.sleep(0.5) 985 | 986 | m.unobserve_property('slang', t.t) 987 | time.sleep(0.5) 988 | 989 | m.slang = 'jp' 990 | m.slang = 'ru' 991 | m.terminate() # needed for synchronization of event thread 992 | handler.assert_has_calls([mock.call('slang', ['jp']), mock.call('slang', ['ru'])]) 993 | --------------------------------------------------------------------------------