├── LICENSE.md ├── README.md ├── code_loader.py ├── console.lua ├── raw_websockets_interaction.py └── test_file_load.lua /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . 661 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | **libre-macros** is an Extension for OBS Studio built on top of its scripting facilities, 3 | utilising built-in embedded LuaJIT interpreter, filter UI and function environment from Lua 5.1 4 | 5 | # Features 6 | - Less boilerplate code: an environment provided with simple User Interface and useful functions 7 | - `source` - source reference that `Console` instance attached to 8 | - `t.pressed` - hotkey state which you can bind 9 | - `sleep(seconds)` - command to pause execution 10 | - `obsffi` - accessed via `obsffi` - native linked library 11 | - `t.raw_image` - D3D11_MAPPED_SUBRESOURCE, see example below 12 | - `bk_obs_api_interactions_functions` - more code to interact with OS and OBS API check `console.lua` 13 | - `t.tasks` - asynchronous event loop 14 | - Patches: run code that is GLOBAL and before registering all of `Console` instance sources 15 | - Browser source interaction: 16 | - real keyboard and mouse interaction functions 17 | - snippet for auto refresh 18 | - patch to inject and run arbitrary javascript without browser refreshing `Win64` 19 | - Hotkeys support for each `Console` instance 20 | - Send, pause, resume, switch, recompile `Console` instances via GLOBAL(per OBS Studio instance) multi actions pipes 21 | - `obs-websockets` interaction support to run any code or execute existing one, see `raw_websockets_interaction.py` 22 | - **Auto run** code when OBS starts, **load from file**, **Hot reload** expressions 23 | - Create hollow gaps sources to assist with layout 24 | 25 | # Installation 26 | - Download [source code](https://github.com/upgradeQ/obs-libre-macros/archive/master.zip), unpack/unzip 27 | - Add `console.lua` to OBS Studio via Tools > Scripts > "+" button 28 | --- 29 | # Usage 30 | 31 | - Left click on any source, add `Console` filter to it 32 | - Open `Script Log` to view `Console` output 33 | - Type some code into the text area 34 | - Press `Execute!` 35 | 36 | Each Console instance has it's own namespace `t` and custom environment, you can access source which Console is attached to. e.g: 37 | ```lua 38 | print(obs_source_get_name(source)) 39 | ``` 40 | To access global the state of script do it via `_G`, when you write x = 5, only that instance of `Console` will have it 41 | 42 | > [!NOTE] 43 | > There might be exceptions in your code, it is recommended to add `print('start')` and `print('end')` statements to debug code in `Console` 44 | 45 | > [!TIP] 46 | > Make a backup of your scene collection. 47 | > You can rename the script name from `console.lua` to something else if crashing on start. 48 | 49 | --- 50 | # Essential stuff 51 | 52 | ## Hotkeys usage 53 | There are 2 types of hotkeys: 54 | - First, can be found in settings with prefixed `0;` - it will execute code in text area 55 | - Second, prefixed with `1;`, `2;`, `3;` - it will mutate `t.pressed`, `t.pressed2`, `t.pressed3` states 56 | 57 | ## Snippets 58 | * `On/off sceneitem every 2.5 seconds` - source must be a scene 59 | * `Loop media source between start and end via hotkey` - adds two hotkeys to set and clear loop (`1;` and `2;`) 60 | * `Write internal stats to text source` - based on [`OBS-Stats-on-Stream`](https://github.com/GreenComfyTea/OBS-Stats-on-Stream) use FreeType2 for it, it's more efficient 61 | * `Update browser every 15 minutes` 62 | * `Overwrite maximum render delay limit` 63 | 64 | ## View and set settings 65 | - `print_settings(source)` - shows all settings 66 | - `print_settings_new(source)` - uses `obs_data_get_json_pretty_with_defaults` 67 | - `print_settings2(source, filter_name)` - shows all settings for a filter on that source 68 | - `set_settings2(source, filter_name, opts)` - sets one setting for filter of a source 69 | - `set_settings52(source, opts)` - sets just one setting for a source 70 | - `set_settings3(source, filter_name, json_string)` - sets settings for a filter of a source 71 | - `set_settings4(source, json_string)` - sets settings for source 72 | 73 | ```lua 74 | set_settings2(source, "Color Correction", {_type ="double", _field= "gamma", _value= 0}) 75 | ``` 76 | 77 | ```lua 78 | local my_json_string = [==[ 79 | {"brightness":0.0,"color_add":0,"color_multiply":16777215, 80 | "contrast":0.0,"gamma":0.0,"hue_shift":0.0,"opacity":1.0,"saturation":0.0} 81 | ]==] 82 | set_settings3(source, "Color Correction", my_json_string) 83 | ``` 84 | 85 | ## Save filter settings and restore them 86 | ```lua 87 | stash "Retro Effects" 88 | ``` 89 | Click `Execute!` to save filter state of settings into the stash, Click again to restore it 90 | Duplicate `Console` filter if you want another stash 91 | Note: Built-in filters work differently and you may want to press `Defaults` first, then restore from stash 92 | 93 | ## Permanent storage in private source settings 94 | ```lua 95 | settings1 = obs_source_get_private_settings(source) 96 | obs_data_set_int(settings1,"__private__", 7) 97 | obs_apply_private_data(settings1) 98 | obs_data_release(settings1) 99 | ``` 100 | Those settings are global for a source, e.g in the next Console filter 101 | ```lua 102 | settings2 = obs_source_get_private_settings(source) 103 | local xc = obs_data_get_int(settings2,"__private__") 104 | print(xc) 105 | obs_data_release(settings2) 106 | ``` 107 | 108 | ## Useful functions 109 | Read the source code to know exactly how they work in the section `bk_obs_api_interactions_functions` 110 | 111 | * `execute(command_line, current_directory)` - executes command line command without console blinking WINDOWS ONLY 112 | 113 | ```lua 114 | if execute[["C:\full\path\to\python.exe" "C:\Users\YOUR_USERNAME\path\to\program.py" ]] then 115 | error('done') else error('not done') end 116 | ``` 117 | 118 | * `pp_execute` - works roughly same as above, based on util.h from libobs [see also](https://github.com/obsproject/obs-studio/commit/225f597379dd0af56f749374a07bea1f7beebf6e) 119 | * `sname(source)` - returns source name as string 120 | * `sceneitem = get_scene_sceneitem(scene_name, scene_item_name)` - gets scene item object 121 | * `click_property(source, property_name)` - on browser source : `click_property(source, "refreshnocache")` 122 | * `click_property_filter_ffi(source, filter_name, prop_name)` - This will press `Execute!` button `click_property_filter_ffi(source, "Console", "button1")` 123 | 124 | ## Play media segments 125 | get the current time (in milliseconds) of the media with `get_timing()`, length - `get_duration()` 126 | ```lua 127 | repeat 128 | play_once(21012,23528) 129 | play_once(13012,15528) 130 | play_once(40012,43528) 131 | play_once(33012,37528) 132 | until false 133 | ``` 134 | 135 | ## Raw image ffi screenshots 136 | Get image data of any source as 512x288px, scaled. Enable it first in Show/Hide in properties. 137 | Windows, DirectX only. Bindings written for `gs_texture_get_obj` and `gs_get_device_obj`. May hit FPS, check stats 138 | ```lua 139 | dx_screenshot "Scene 2" 140 | local img = c_u8_p(t.raw_image) 141 | local n = MAX_LEN 142 | print(table.concat({img[0], img[1], img[2], img[3]}, ' ')) 143 | print(table.concat({img[n-4], img[n-3], img[n-2], img[n-1]}, ' ')) 144 | ``` 145 | 146 | ## Browser source interaction 147 | ### Send mouse move 148 | ```lua 149 | repeat sleep(1) 150 | send_mouse_move_tbs(source, 12, 125) 151 | local get_t = function() return math.random(125, 140) end 152 | for i=12, 200, 6 do 153 | sleep(0.03) 154 | send_mouse_move_tbs(source, i, get_t()) 155 | end 156 | until false 157 | ``` 158 | 2 consoles are sending mouse move events into browser sources: 159 | ![gif](https://i.imgur.com/gI6LbRF.gif) 160 | Website link: 161 | 162 | ### Send Click 163 | ```lua 164 | repeat sleep(1) 165 | --send_mouse_move_tbs(source, 95, 80) -- 300x300 browser source 166 | _opts = {x=95, y=80, button_type=MOUSE_LEFT, mouse_up=false, click_count=0} 167 | send_mouse_click_tbs(source, _opts) 168 | -- here might be delay which specifies how long mouse is pressed 169 | _opts.mouse_up, _opts.click_count = true, 2 170 | send_mouse_click_tbs(source, _opts) 171 | until false 172 | ``` 173 | 174 | ### Keyboard interaction 175 | ```lua 176 | -- Send tab 177 | send_hotkey_tbs1(source, "OBS_KEY_TAB", false) 178 | send_hotkey_tbs1(source, "OBS_KEY_TAB", true) 179 | 180 | -- Send tab with shift modifier 181 | send_hotkey_tbs1(source, "OBS_KEY_TAB", false, {shift=true}) 182 | send_hotkey_tbs1(source, "OBS_KEY_TAB", true, {shift=true}) 183 | 184 | send_hotkey_tbs1(source, "OBS_KEY_RETURN", false) 185 | send_hotkey_tbs1(source, "OBS_KEY_RETURN", true) 186 | 187 | -- char_to_obskey (ASCII only) 188 | send_hotkey_tbs1(source, char_to_obskey('j'), false, {shift=true}) 189 | send_hotkey_tbs1(source, char_to_obskey('j'), true, {shift=true}) 190 | -- or use 191 | send_hotkey_tbs1(source, c2o('j'), false) 192 | send_hotkey_tbs1(source, c2o('j'), true) 193 | -- might work with unicode input 194 | send_hotkey_tbs2(source, 'q', false) 195 | send_hotkey_tbs2(source, 'й', false) 196 | ``` 197 | 198 | ### Send javascript 199 | > [!CAUTION] 200 | > This will rewrite **all** CSS on **all** browser sources. 201 | 202 | `patch_bs_js()` must be written in the GLOBAL code config. 203 | Restart the program or reload the script, when adding new BS 204 | In version `4.1.2` `patch_bs_js(1)` accepts numerical index in the offsets table, defaults to last index when calling without arguments `patch_bs_js()` 205 | 206 | ```lua 207 | send_js "document.documentElement.style.filter='grayscale(100%)'" 208 | sleep(1.3) 209 | send_js [[document.documentElement.style.filter='invert(100%)']] 210 | sleep(0.8) 211 | send_js [==[ 212 | document.body.innerHTML = ` 213 | 214 | `; 215 | let r = Math.random; 216 | const c = document.getElementById('cvs').getContext('2d'); 217 | c.beginPath();c.moveTo(50, 0);c.lineTo(0, 100);c.lineTo(100, 100); 218 | c.fillStyle=`rgb(${r()*256|0},${r()*256|0},${r()*256|0})`;c.fill(); 219 | ]==] 220 | ``` 221 | 222 | ## Auto run 223 | If you check `Auto run` then code from this console will be executed automatically 224 | when OBS starts 225 | 226 | ## Loading from file 227 | To load from file you need first select which one to load from properties, 228 | see "Settings for internal use", then paste this template into text area: 229 | ```lua 230 | local f = loadfile(t.p1, "t",getfenv(1)) 231 | success, result = pcall(f) 232 | if not success then print(result) end 233 | ``` 234 | 235 | ## Hot reload with delay: 236 | ```lua 237 | print('restarted') -- expression print_source_name(source) 238 | local delay = 0.5 239 | while true do 240 | local f=load( t.hotreload) 241 | setfenv(f,getfenv(1)) 242 | success, result = pcall(f) 243 | if not success then print(result) end 244 | sleep(delay) 245 | end 246 | ``` 247 | 248 | ## Run multiactions 249 | `Console` instance with this entries in first and second text area 250 | ```lua 251 | okay("pipe1") 252 | print('exposing pipe 1') 253 | ``` 254 | Actual code, write it in second text area in each instance of `Console` 255 | ```lua 256 | print(os.time()) print(' start 11111') ; sleep (0.5) ; print(os.time()) 257 | print_source_name(source) ; sleep(2) print('done 11111') 258 | ``` 259 | Another `Console` instance with same code first text area but different in second 260 | ```lua 261 | okay("pipe2") 262 | print('exposing pipe 2') 263 | ``` 264 | And in multiaction text area add this 265 | ```lua 266 | print(os.time()) print('start ss22222ssssss2ss') ; sleep (2.5 ) ; print(os.time()) 267 | print_source_name(source) ; sleep(2) print('done 2222') 268 | ``` 269 | Main `Console` instance. This will start `pipe1` then after sec `pipe2` 270 | ``` 271 | offer('pipe1') 272 | sleep(1) 273 | offer('pipe2') 274 | ``` 275 | - `okay` - exposes actions 276 | - `offer` - starts actions 277 | - `stall` - pause 278 | - `forward` - continue 279 | - `switch` - pause/continue 280 | - `recompile` - restarts actions 281 | 282 | ## Gaps sources 283 | ***Only usable through attaching via filter to scene (not groups)*** 284 | 285 | - Add gap: 286 | ```lua 287 | add_gap {x=300,y=500, width = 100, height = 100} 288 | ``` 289 | - Add outer gaps - `add_outer_gap(100)` 290 | - Resize outer gaps - `resize_outer_gaps(30)` 291 | - Delete all gaps on scene - `delete_all_gaps()` 292 | 293 | # Extra 294 | Here is the stuff that is rarely used, presented as API interaction examples. 295 | 296 |
297 | Toggle visibility of collapsed markdown text 298 | 299 | ## Push-to-talk release delay 300 | set hotkey for `1;` of Audio Input source 301 | ```lua 302 | repeat 303 | sleep(0.0) 304 | if t.pressed 305 | then obs_source_set_volume(source,0.5) 306 | else 307 | sleep(0.8) obs_source_set_volume(source,0.0) 308 | end 309 | until false 310 | ``` 311 | 312 | ## Access sceneitem from scene: 313 | ```lua 314 | local sceneitem = get_scene_sceneitem("Scene 2", sname(source)) 315 | repeat 316 | sleep(0.01) 317 | if sceneitem then 318 | obs_sceneitem_set_rot(sceneitem, math.sin(math.random() * 100)) 319 | end 320 | until false 321 | ``` 322 | 323 | ## High frequency blinking source: 324 | - [x] Auto run 325 | ```lua 326 | while true do 327 | sleep(0.03) 328 | obs_source_set_enabled(source, true) 329 | sleep(0.03) 330 | obs_source_set_enabled(source, false) 331 | end 332 | ``` 333 | 334 | ## Print source name while holding hotkey: 335 | ```lua 336 | repeat 337 | sleep(0.1) 338 | if t.pressed then print_source_name(source) end 339 | until false 340 | ``` 341 | 342 | ## Shake a text source and update its text based on location from scene 343 | (using code from [wiki](https://github.com/obsproject/obs-studio/wiki/Scripting-Tutorial-Source-Shake)) 344 | Paste into `Console` or load from file this code: 345 | ```lua 346 | local source_name = obs_source_get_name(source) 347 | local _name = "YOUR CURRENT SCENE NAME YOU ARE ON" 348 | local sceneitem = get_scene_sceneitem(_name, return_source_name(source)) 349 | local amplitude , shaken_sceneitem_angle , frequency = 10, 0, 2 350 | local pos = vec2() 351 | 352 | local function update_text(source, text) 353 | local settings = obs_data_create() 354 | obs_data_set_string(settings, "text", text) 355 | obs_source_update(source, settings) 356 | obs_data_release(settings) 357 | end 358 | local function get_position(opts) 359 | return "pos x: " .. opts.x .. " y: " .. opts.y 360 | end 361 | repeat 362 | sleep(0) -- sometimes obs freezes if sceneitem is double clicked 363 | local angle = shaken_sceneitem_angle + amplitude*math.sin(os.clock()*frequency*2*math.pi) 364 | obs_sceneitem_set_rot(sceneitem, angle) 365 | obs_sceneitem_get_pos(sceneitem, pos) 366 | local result = get_position { x = pos.x, y = pos.y } 367 | update_text(source, result) 368 | until false 369 | ``` 370 | 371 | ## Tasks 372 | Print a source name every second while also print current filters attached to 373 | source in `t.tasks`, shutdown this task after 10 seconds 374 | 375 | ```lua 376 | function print_filters() 377 | repeat 378 | local filters_list = obs_source_enum_filters(source) 379 | for _, fs in pairs(filters_list) do 380 | print_source_name(fs) 381 | end 382 | source_list_release(filters_list) 383 | sleep(math.random()) 384 | until false 385 | end 386 | 387 | t.tasks[1] = run(print_filters) 388 | function shutdown_all() 389 | for task, _coro in pairs(t.tasks) do 390 | t.tasks[task] = nil 391 | end 392 | end 393 | 394 | t.tasks[2] = run(function() 395 | sleep(10) 396 | shutdown_all() 397 | end) 398 | 399 | repeat 400 | sleep(1) 401 | print_source_name(source) 402 | until false 403 | ``` 404 | ## Internal settings redirection 405 | Using [move-transition plugin](https://obsproject.com/forum/resources/move-transition.913/) with its move-audio filter, redirect to `t.mv2`, then show value of `t.mv2` in `Script Log` 406 | ```lua 407 | repeat 408 | sleep(0.3) 409 | print(t.mv2) 410 | until false 411 | ``` 412 | 413 | ## Start virtual camera as a triggered named callback: 414 | 415 | ```lua 416 | local description = 'OBSBasic.StartVirtualCam' 417 | trigger_from_hotkey_callback(description) 418 | ``` 419 | 420 | ## Send hotkey combination to OBS: 421 | ```lua 422 | send_hotkey('OBS_KEY_2', {shift=true}) 423 | ``` 424 | 425 | ## Hook state of right and left mouse buttons: 426 | ```lua 427 | hook_mouse_buttons() 428 | repeat 429 | sleep(0.1) 430 | print(tostring(LMB)) 431 | print(tostring(RMB)) 432 | until false 433 | ``` 434 | 435 | ## Move plugin 436 | Route audio move value filter from obs-move-transition to change console settings 437 | Attach console to image source, add images to directory with `console.lua` 438 | In audio move set `Input Peak Sample`, select `Move value[0, 100] 1` base value `1`, factor `100` 439 | ```lua 440 | function update_image(state) 441 | local settings = obs_data_create() 442 | obs_data_set_string(settings, "file", script_path() .. state) 443 | obs_source_update(source, settings) 444 | obs_data_release(settings) 445 | end 446 | local skip, scream, normal, silent = false, 30, 20, 20 447 | while true do ::continue:: 448 | sleep(0.03) 449 | if t.mv2 > scream then update_image("scream.png") skip = false 450 | sleep(0.5) goto continue end 451 | if t.mv2 > normal then update_image("normal.png") skip = false 452 | sleep(0.3) goto continue 453 | end -- pause for a moment then goto start 454 | if t.mv2 < silent then if skip then goto continue end 455 | update_image("silent.png") 456 | skip = true -- do not update afterwards 457 | end 458 | end 459 | ``` 460 | Result: 461 | ![gif](https://i.imgur.com/4HysoIE.gif) 462 | 463 | ## Execute python(must load helper script) 464 | ```lua 465 | exec_py( 466 | [=[def print_hello(): 467 | print('hello world') 468 | a = [ x for x in range(10) ][0] 469 | return a 470 | print_hello() 471 | ]=]) 472 | ``` 473 | ## React on source signals 474 | ```lua 475 | register_on_show(function() 476 | print('on show') 477 | sleep(3) 478 | print('on show exit') 479 | end) 480 | ``` 481 | 482 |
483 | 484 | # See also 485 | * Source code to read - https://github.com/upgradeQ/libre-macros/blob/master/console.lua 486 | * Advanced scene switcher [plugin](https://github.com/WarmUpTill/SceneSwitcher) 487 | * [Examples & Cheatsheet (python)](https://github.com/upgradeQ/Streaming-Software-Scripting-Reference) 488 | * https://lua.org/ , https://luajit.org/ 489 | 490 | # License 491 | 492 | 493 | 494 | 495 | The **libre-macros** is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. That means that IF users interacting with it remotely(through a network) - they are entitled to source code. And if it is **not** modified, then you can direct them [here](https://github.com/upgradeQ/obs-libre-macros), but if you had **modified** it, you simply have to publish your modifications. The easiest way to do this is to have a public GitHub repository of your fork or create a PR upstream. Otherwise, you will be in violation of the license. The relevant part of the license is under section 13 of the AGPLv3. 496 | -------------------------------------------------------------------------------- /code_loader.py: -------------------------------------------------------------------------------- 1 | copyleft =""" 2 | obs-libre-macros - scripting and macros hotkeys in OBS Studio for Humans 3 | Contact/URL https://www.github.com/upgradeQ/obs-libre-macros 4 | Copyright (C) 2021 upgradeQ 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | """ 19 | import obspython as obs 20 | from time import sleep, gmtime, strftime 21 | from contextlib import contextmanager 22 | from threading import Thread 23 | from functools import partial 24 | 25 | @contextmanager 26 | def p_data_ar(data_type, field): 27 | settings = obs.obs_get_private_data() 28 | get = getattr(obs, f"obs_data_get_{data_type}") 29 | try: 30 | yield get(settings, field) 31 | finally: 32 | obs.obs_data_release(settings) 33 | 34 | def send_to_private_data(data_type, field, result): 35 | settings = obs.obs_data_create() 36 | set = getattr(obs, f"obs_data_set_{data_type}") 37 | set(settings, field, result) 38 | obs.obs_apply_private_data(settings) 39 | obs.obs_data_release(settings) 40 | 41 | def execute_from_private_registry(address=None): 42 | handshake = "__py_dispatch" if not address else "__py_dispatch%s" % address 43 | address = "__py_registry" if not address else "__py_registry%s" % address 44 | with p_data_ar("string", address) as code: 45 | with p_data_ar("bool", handshake) as proceed: 46 | if proceed: 47 | exec(code) 48 | send_to_private_data("bool", handshake, False) 49 | 50 | def execute_lua(address=None, code = None): 51 | handshake = "__lua_dispatch" if not address else "__lua_dispatch%s" % address 52 | address = "__lua_registry" if not address else "__lua_registry%s" % address 53 | code = code or """ 54 | print("hello from %s, time: %s") 55 | """ % ("python", strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())) 56 | send_to_private_data("string", address, code) 57 | send_to_private_data("bool", handshake, True) 58 | 59 | 60 | def event_loop(): 61 | while True: 62 | sleep(1/60) 63 | execute_from_private_registry() 64 | execute_lua() 65 | 66 | start_timer = True # obs will not close properly with threads 67 | if start_timer: 68 | obs.timer_add(execute_from_private_registry,16) 69 | obs.timer_add(execute_lua,1000) 70 | another_func_lua = partial(execute_lua,"1", "print(2); print_source_name(t.source)") 71 | another_func_py = partial(execute_from_private_registry,"2") # accept from 2 72 | obs.timer_add(another_func_lua, 16) 73 | obs.timer_add(another_func_py, 16) 74 | 75 | else: 76 | t = Thread(target=event_loop) 77 | t.start() 78 | 79 | # vim: ft=python ts=4 sw=4 et sts=4 80 | -------------------------------------------------------------------------------- /console.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | libre-macros - Scripting and macros hotkeys overhaul for OBS Studio 3 | Contact/URL https://www.github.com/upgradeQ/libre-macros 4 | Copyright (C) 2021-2025 upgradeQ 5 | Distributed under AGPL license 6 | ]] 7 | _ver = "4.1.2" 8 | print('[+] libre-macros https://www.github.com/upgradeQ/libre-macros' .. ' ' .. _ver) 9 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~BOOKMARKSwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 10 | -- localization - below 11 | -- bk_imports - all imported modules 12 | -- bk_obs_api_interactions_functions - global general purpose functions - preloaded 13 | -- bk_console_instance_functions - t. == self. ; local functions for Console instance 14 | -- bk_console_snippets_code - snippets to get you started with Console 15 | -- bk_obs_source_definition - scripted source: UI, hotkeys, event loop 16 | -- bk_obs_script_definition - registration of scripted sources and UI 17 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 18 | -- https://stackoverflow.com/a/8891620 by kikito 19 | local i18n = { locales = {} } 20 | 21 | local current_locale = 'en' -- the default language 22 | 23 | function i18n.set_locale(new_locale) 24 | current_locale = new_locale 25 | assert(i18n.locales[current_locale], ("The locale %q was unknown"):format(new_locale)) 26 | end 27 | 28 | local function translate(id) 29 | local result = i18n.locales[current_locale][id] 30 | assert(result, ("The id %q was not found in the current locale (%q)"):format(id, current_locale)) 31 | return result 32 | end 33 | 34 | i18n.translate = translate 35 | 36 | setmetatable(i18n, {__call = function(_, ...) return translate(...) end}) 37 | 38 | i18n.locales.en = { 39 | select_lang = 'Select language', 40 | execute = 'Execute!', 41 | view_output = 'View output', 42 | auto_run = 'Auto run', 43 | s_mv1 = 'Move value variable[0, 1] 0.01', 44 | s_mv2 = 'Move value variable[0, 100] 1', 45 | hotreload = 'Hot reload expression', 46 | p1 = 'Path 1', 47 | p2 = 'Path 2', 48 | p_group_name = 'Settings for interval use', 49 | p_text_area2 = 'Text area for global multi action pipes', 50 | p_dx_screenshot = 'Enable texture reading', 51 | width = 'Width', 52 | height = 'Height', 53 | g2_restart = 'Restart required to enable/disable features ', -- padding 54 | ['Console (Timer)'] = 'Console (Timer)', 55 | ['Console sceneitem custom'] = 'Console sceneitem custom source', 56 | ['Gap source'] = 'Gap source', 57 | ['Console'] = 'Console', 58 | interval = 'Console (Timer) interval per second', 59 | _snippets ='Snippets', 60 | _snip_select ='Select snippet', 61 | _snip_confirm ='Confirm', 62 | s_on_off_sceneitem = 'On/off sceneitem every 2.5 seconds', 63 | s_loop_media = 'Loop media source between start and end via hotkey', 64 | s_general_stats = 'Write internal stats to text source', 65 | s_browser_refresh = 'Update browser every 15 minutes', 66 | s_render_delay = 'Overwrite maximum render delay limit', 67 | sh_checkbox = 'Show/Hide ', 68 | s_patch_err = '[patch] ERROR in console.lua', 69 | } 70 | 71 | i18n.locales.ru = { 72 | select_lang = 'Выбрать язык', 73 | execute = 'Выполнить!', 74 | view_output = 'Посмотреть результат', 75 | auto_run = 'Запускать автоматически', 76 | s_mv1 = 'Перменная движения[0, 1] 0.01', 77 | s_mv2 = 'Перменная движения[0, 100] 1', 78 | hotreload = 'Выполнять выражение автоматически', 79 | p1 = 'Путь 1', 80 | p2 = 'Путь 2', 81 | p_group_name = 'Внутренние настройки', 82 | p_text_area2 = 'Поле текста для глобальных мульти последовательностей', 83 | p_dx_screenshot = 'Включить чтение текстуры', 84 | width = 'Ширина', 85 | height = 'Высота', 86 | g2_restart = 'Требуется перезапуск чтобы вкл/выкл функции ', 87 | ['Console (Timer)'] = 'Консоль (Таймер)', 88 | ['Console sceneitem custom'] = 'Консоль специальный предмет (источник) сцены', 89 | ['Gap source'] = 'Пустой источник', 90 | ['Console'] = 'Консоль', 91 | interval = 'Консоль (Таймер) интервал раз в секунду', 92 | _snippets ='Сниппеты', 93 | _snip_select ='Выбрать сниппет', 94 | _snip_confirm ='Подтвердить', 95 | s_on_off_sceneitem = 'Вкл/выкл предмет сцены каждые 2.5 секунды', 96 | s_loop_media = 'Повтор медиа источника через сочетание клавиш', 97 | s_general_stats = 'Записать специальную статистику в текстовый источник', 98 | s_browser_refresh = 'Обновлять браузер каждые 15 минут', 99 | s_render_delay = 'Выставить сверхзначение задержки отображения', 100 | sh_checkbox = 'Показать/Скрыть ', 101 | s_patch_err = '[патч] ОШИБКА в console.lua', 102 | } 103 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 104 | -- id - obs keyboard id , c - character , cs - character with shift pressed 105 | qwerty_minimal_keyboard_layout = { 106 | {id="OBS_KEY_BACKSPACE", c="backspace", cs="backspace"}, 107 | {id="OBS_KEY_RETURN", c="enter", cs="enter"}, 108 | {id="OBS_KEY_TAB", c="tab", cs="tab"}, 109 | {id="OBS_KEY_ASCIITILDE", c="`", cs="~"}, 110 | {id ="OBS_KEY_COMMA", c=", ", cs="<"}, 111 | {id ="OBS_KEY_PLUS", c="=", cs="+"}, 112 | {id ="OBS_KEY_MINUS", c="-", cs="_"}, 113 | {id ="OBS_KEY_BRACKETLEFT", c="[", cs="{"}, 114 | {id ="OBS_KEY_BRACKETRIGHT", c="]", cs="}"}, 115 | {id ="OBS_KEY_PERIOD", c=".", cs=">"}, 116 | {id ="OBS_KEY_APOSTROPHE", c="'", cs='"'}, 117 | {id ="OBS_KEY_SEMICOLON", c=";", cs=":"}, 118 | {id ="OBS_KEY_SLASH", c="/", cs="?"}, 119 | {id ="OBS_KEY_SPACE", c=" ", cs=" "}, 120 | {id ="OBS_KEY_0", c="0", cs=")"}, 121 | {id ="OBS_KEY_1", c="1", cs="!"}, 122 | {id ="OBS_KEY_2", c="2", cs="@"}, 123 | {id ="OBS_KEY_3", c="3", cs="#"}, 124 | {id ="OBS_KEY_4", c="4", cs="$"}, 125 | {id ="OBS_KEY_5", c="5", cs="%"}, 126 | {id ="OBS_KEY_6", c="6", cs="^"}, 127 | {id ="OBS_KEY_7", c="7", cs="&"}, 128 | {id ="OBS_KEY_8", c="8", cs="*"}, 129 | {id ="OBS_KEY_9", c="9", cs="("}, 130 | {id ="OBS_KEY_A", c="a", cs="A"}, 131 | {id ="OBS_KEY_B", c="b", cs="B"}, 132 | {id ="OBS_KEY_C", c="c", cs="C"}, 133 | {id ="OBS_KEY_D", c="d", cs="D"}, 134 | {id ="OBS_KEY_E", c="e", cs="E"}, 135 | {id ="OBS_KEY_F", c="f", cs="F"}, 136 | {id ="OBS_KEY_G", c="g", cs="G"}, 137 | {id ="OBS_KEY_H", c="h", cs="H"}, 138 | {id ="OBS_KEY_I", c="i", cs="I"}, 139 | {id ="OBS_KEY_J", c="j", cs="J"}, 140 | {id ="OBS_KEY_K", c="k", cs="K"}, 141 | {id ="OBS_KEY_L", c="l", cs="L"}, 142 | {id ="OBS_KEY_M", c="m", cs="M"}, 143 | {id ="OBS_KEY_N", c="n", cs="N"}, 144 | {id ="OBS_KEY_O", c="o", cs="O"}, 145 | {id ="OBS_KEY_P", c="p", cs="P"}, 146 | {id ="OBS_KEY_Q", c="q", cs="Q"}, 147 | {id ="OBS_KEY_R", c="r", cs="R"}, 148 | {id ="OBS_KEY_S", c="s", cs="S"}, 149 | {id ="OBS_KEY_T", c="t", cs="T"}, 150 | {id ="OBS_KEY_U", c="u", cs="U"}, 151 | {id ="OBS_KEY_V", c="v", cs="V"}, 152 | {id ="OBS_KEY_W", c="w", cs="W"}, 153 | {id ="OBS_KEY_X", c="x", cs="X"}, 154 | {id ="OBS_KEY_Y", c="y", cs="Y"}, 155 | {id ="OBS_KEY_Z", c="z", cs="Z"}, 156 | } 157 | --bk_imports~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 158 | local function open_package(ns) 159 | for n, v in pairs(ns) do _G[n] = v end 160 | end 161 | 162 | open_package(obslua) 163 | ffi = require "ffi" -- for native libs and C code access 164 | jit = require "jit" -- for C thread callback behavior change 165 | bit = require "bit" -- binary logic 166 | 167 | local C = ffi.C 168 | 169 | function try_load_library(alias, name) 170 | if ffi.os == "OSX" then name = name .. ".0.dylib" end 171 | ok, _G[alias] = pcall(ffi.load, name) 172 | if not ok then 173 | print(("WARNING:%s:Has failed to load, %s is nil"):format(name, alias)) 174 | end 175 | end 176 | 177 | try_load_library("obsffi", "obs") 178 | --try_load_library("frontendC", "frontend-api") 179 | --try_load_library("openglC", "opengl") 180 | --try_load_library("scriptingC", "scripting") 181 | 182 | --bk_obs_api_interactions_functions~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 183 | local Timer = {} 184 | function Timer:new(o) 185 | o = o or {} 186 | setmetatable(o, self) 187 | self.__index = self 188 | return o 189 | end 190 | 191 | function Timer:update(dt) 192 | self.current_accumulated_time = self.current_accumulated_time + dt 193 | if self.current_accumulated_time >= self.duration then 194 | self.finished = true 195 | end 196 | end 197 | 198 | function Timer:enter() 199 | self.finished = false 200 | self.current_accumulated_time = 0 201 | end 202 | 203 | function Timer:launch() 204 | self:enter() 205 | while not self.finished do 206 | local dt = coroutine.yield() 207 | self:update(dt) 208 | end 209 | end 210 | 211 | function sleep(s) 212 | local action = Timer:new{duration=s} 213 | action:launch() 214 | end 215 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 216 | function sname(source) return obs_source_get_name(source) end 217 | 218 | return_source_name = sname 219 | 220 | function print_source_name(source) print(obs_source_get_name(source)) end 221 | 222 | function get_scene_sceneitem(scene_name, scene_item_name) 223 | local sceneitem; 224 | local scenes = obs_frontend_get_scenes() 225 | for _, scene in pairs(scenes) do 226 | if sname(scene) == scene_name then 227 | scene = obs_scene_from_source(scene) 228 | sceneitem = obs_scene_find_source_recursive(scene, scene_item_name) 229 | end 230 | end 231 | source_list_release(scenes) 232 | return sceneitem 233 | end 234 | 235 | function print_settings(source) 236 | local settings = obs_source_get_settings(source) 237 | local psettings = obs_source_get_private_settings(source) 238 | local dsettings = obs_data_get_defaults(settings); 239 | local pdsettings = obs_data_get_defaults(psettings); 240 | print("[---------- settings ----------") 241 | print(obs_data_get_json(settings)) 242 | print("---------- private_settings ----------") 243 | print(obs_data_get_json(psettings)) 244 | print("---------- default settings for this source type ----------") 245 | print(obs_data_get_json(dsettings)) 246 | print("---------- default private settings for this source type ----------") 247 | print(obs_data_get_json(pdsettings)) 248 | print(("----------%s----------]"):format(return_source_name(source))) 249 | for _, s in pairs { settings, psettings, dsettings, pdsettings} 250 | do obs_data_release(s) 251 | end 252 | end 253 | 254 | function print_settings2(source, filter_name) 255 | local result = obs_source_enum_filters(source) 256 | for _, f in pairs(result) do 257 | if return_source_name(f) == filter_name then 258 | print_settings(f) 259 | end 260 | end 261 | source_list_release(result) 262 | end 263 | 264 | function print_settings_new(source) 265 | local settings = obs_source_get_settings(source) 266 | local psettings = obs_source_get_private_settings(source) 267 | print("[---------- settings ----------") 268 | print(obs_data_get_json_pretty_with_defaults(settings)) 269 | print("---------- private_settings ----------") 270 | print(obs_data_get_json_pretty_with_defaults(psettings)) 271 | print(("----------%s----------]"):format(return_source_name(source))) 272 | for _, s in pairs { settings, psettings} 273 | do obs_data_release(s) 274 | end 275 | end 276 | 277 | 278 | function set_settings2(source, filter_name, opts) 279 | local result = obs_source_enum_filters(source) 280 | local settings = obs_data_create() 281 | for _, f in pairs(result) do 282 | if return_source_name(f) == filter_name then 283 | _G[("obs_data_set_%s"):format(opts._type)](settings, opts._field, opts._value) 284 | obs_source_update(f, settings) 285 | obs_data_release(settings) 286 | end 287 | end 288 | source_list_release(result) 289 | end 290 | 291 | function set_settings3(source, filter_name, json_string) 292 | local result = obs_source_enum_filters(source) 293 | local settings = obs_data_create_from_json(json_string) 294 | for _, f in pairs(result) do 295 | if return_source_name(f) == filter_name then 296 | obs_source_update(f, settings) 297 | obs_data_release(settings) 298 | end 299 | end 300 | source_list_release(result) 301 | end 302 | 303 | function set_settings4(source, json_string) 304 | local settings = obs_data_create_from_json(json_string) 305 | obs_source_update(f, settings) 306 | obs_data_release(settings) 307 | end 308 | 309 | function set_settings52(source, opts) 310 | local settings = obs_source_get_settings(source) 311 | _G[("obs_data_set_%s"):format(opts._type)](settings, opts._field, opts._value) 312 | obs_source_update(source, settings) 313 | obs_data_release(settings) 314 | end 315 | 316 | LMB, RMB, MOUSE_HOOKED = false, false, false 317 | function htk_1_cb(pressed) LMB = pressed end 318 | function htk_2_cb(pressed) RMB = pressed end 319 | function hook_mouse_buttons() 320 | if MOUSE_HOOKED then return error('already hooked mouse') end 321 | local key_1 = '{"htk_1_mouse": [ { "key": "OBS_KEY_MOUSE1" } ], ' 322 | local key_2 = '"htk_2_mouse": [ { "key": "OBS_KEY_MOUSE2" } ]}' 323 | local json_s = key_1 .. key_2 324 | local default_hotkeys = { 325 | {id='htk_1_mouse', des='LMB state', callback=htk_1_cb}, 326 | {id='htk_2_mouse', des='RMB state', callback=htk_2_cb}, 327 | } 328 | local s = obs_data_create_from_json(json_s) 329 | for _, v in pairs(default_hotkeys) do 330 | local a = obs_data_get_array(s, v.id) 331 | h = obs_hotkey_register_frontend(v.id, v.des, v.callback) 332 | obs_hotkey_load(h, a) 333 | obs_data_array_release(a) 334 | end 335 | obs_data_release(s) 336 | MOUSE_HOOKED = true 337 | end 338 | 339 | function get_modifiers(ctx) 340 | local key_modifiers = ctx or {} 341 | local shift = key_modifiers.shift or false 342 | local control = key_modifiers.control or false 343 | local alt = key_modifiers.alt or false 344 | local command = key_modifiers.command or false 345 | local modifiers = 0 346 | 347 | if shift then modifiers = bit.bor(modifiers, INTERACT_SHIFT_KEY ) end 348 | if control then modifiers = bit.bor(modifiers, INTERACT_CONTROL_KEY ) end 349 | if alt then modifiers = bit.bor(modifiers, INTERACT_ALT_KEY ) end 350 | if command then modifiers = bit.bor(modifiers, INTERACT_COMMAND_KEY ) end 351 | return modifiers 352 | end 353 | 354 | function send_hotkey(hotkey_id_name, key_modifiers) 355 | local combo = obs_key_combination() 356 | combo.modifiers = get_modifiers(key_modifiers) 357 | combo.key = obs_key_from_name(hotkey_id_name) 358 | 359 | if not modifiers and -- there is should be OBS_KEY_NONE, but it is missing in obslua 360 | (combo.key == 0 or combo.key >= OBS_KEY_LAST_VALUE) then 361 | return error('invalid key-modifier combination') 362 | end 363 | 364 | obs_hotkey_inject_event(combo, false) 365 | obs_hotkey_inject_event(combo, true) 366 | obs_hotkey_inject_event(combo, false) 367 | end 368 | 369 | function char_to_obskey(char) 370 | for _, row in pairs(qwerty_minimal_keyboard_layout) do 371 | if char == row.c or char == row.cs then 372 | return row.id 373 | end 374 | end 375 | error('character not found within qwerty minimal table') 376 | end 377 | c2o = char_to_obskey 378 | 379 | function send_hotkey_tbs1(source, hotkey_id_name, key_up, key_modifiers) 380 | local key = obs_key_from_name(hotkey_id_name) 381 | local vk = obs_key_to_virtual_key(key) 382 | local event = obs_key_event() 383 | event.native_vkey = vk 384 | event.modifiers = get_modifiers(key_modifiers) 385 | event.native_modifiers = event.modifiers 386 | event.native_scancode = vk 387 | event.text = "" 388 | obs_source_send_key_click(source, event, key_up) 389 | end 390 | 391 | function send_hotkey_tbs2(source, char, key_up, key_modifiers) 392 | local event = obs_key_event() 393 | event.native_vkey = 0 394 | event.native_modifiers = 0 395 | event.native_scancode = 0 396 | event.modifiers = get_modifiers(key_modifiers) 397 | event.text = char 398 | obs_source_send_key_click(source, event, key_up) 399 | end 400 | 401 | function send_mouse_click_tbs(source, opts, key_modifiers) 402 | local event = obs_mouse_event() 403 | event.modifiers = get_modifiers(key_modifiers) 404 | event.x = opts.x 405 | event.y = opts.y 406 | obs_source_send_mouse_click( 407 | source, event, opts.button_type, opts.mouse_up, opts.click_count 408 | ) 409 | end 410 | 411 | function send_mouse_move_tbs(source, x, y, key_modifiers) 412 | local event = obs_mouse_event() 413 | event.modifiers = get_modifiers(key_modifiers) 414 | event.x = x 415 | event.y = y 416 | obs_source_send_mouse_move(source, event, false) -- do not leave 417 | end 418 | 419 | -- depricated 420 | function send_mouse_wheel_tbs(source, x, y, x_delta, y_delta, key_modifiers) 421 | local event = obs_mouse_event() 422 | event.x = opts.x or 0 423 | event.y = opts.y or 0 424 | event.modifiers = get_modifiers(key_modifiers) 425 | local x_delta = opts.x_delta or 0 426 | local y_delta = opts.y_delta or 0 427 | obs_source_send_mouse_wheel(source, event, x_delta, y_delta) 428 | end 429 | 430 | ffi.cdef[[ 431 | typedef struct obs_hotkey obs_hotkey_t; 432 | typedef size_t obs_hotkey_id; 433 | 434 | const char *obs_hotkey_get_name(const obs_hotkey_t *key); 435 | typedef bool (*obs_hotkey_enum_func)(void *data, obs_hotkey_id id, obs_hotkey_t *key); 436 | void obs_enum_hotkeys(obs_hotkey_enum_func func, void *data); 437 | ]] 438 | 439 | function trigger_from_hotkey_callback(description) 440 | local htk_id; 441 | function callback_htk(data, id, key) 442 | local name = obsffi.obs_hotkey_get_name(key) 443 | if ffi.string(name) == description then 444 | htk_id = tonumber(id) 445 | return false 446 | else 447 | return true 448 | end 449 | end 450 | local cb = ffi.cast("obs_hotkey_enum_func", callback_htk) 451 | obsffi.obs_enum_hotkeys(cb, nil) 452 | if htk_id then 453 | obs_hotkey_trigger_routed_callback(htk_id, false) 454 | obs_hotkey_trigger_routed_callback(htk_id, true) 455 | obs_hotkey_trigger_routed_callback(htk_id, false) 456 | end 457 | end 458 | 459 | function read_private_data(data_type, field) 460 | local s = obs_get_private_data() 461 | local result = _G[("obs_data_get_%s"):format(data_type)](s, field) 462 | obs_data_release(s) 463 | return result 464 | end 465 | 466 | function write_private_data(data_type, field, result) 467 | local s = obs_data_create() 468 | _G[("obs_data_set_%s"):format(data_type)](s, field, result) 469 | obs_apply_private_data(s) 470 | obs_data_release(s) 471 | end 472 | 473 | function exec_py(string_, address) 474 | local handshake; 475 | if not address then 476 | handshake = "__py_dispatch" 477 | address = "__py_registry" 478 | else 479 | handshake = ("__py_dispatch%s"):format(address) 480 | address = ("__py_registry%s"):format(address) 481 | end 482 | local s = obs_data_create() 483 | obs_data_set_string(s, address, string_) 484 | obs_data_set_bool(s, handshake, true) 485 | obs_apply_private_data(s) 486 | obs_data_release(s) 487 | end 488 | 489 | function get_code(address) 490 | local handshake; 491 | if not address then 492 | handshake = "__lua_dispatch" 493 | address = "__lua_registry" 494 | else 495 | handshake = ("__lua_dispatch%s"):format(address) 496 | address = ("__lua_registry%s"):format(address) 497 | end 498 | local s = obs_get_private_data() 499 | local string_ = obs_data_get_string(s, address) 500 | local proceed = obs_data_get_bool(s, handshake) 501 | obs_data_release(s) 502 | return proceed, string_, handshake 503 | end 504 | -- https://stackoverflow.com/a/61269226 by Egor-Skriptunoff 505 | if ffi.os == 'Windows' then 506 | ffi.cdef[[ 507 | typedef struct _STARTUPINFOA { 508 | uint32_t cb; 509 | void * lpReserved; 510 | void * lpDesktop; 511 | void * lpTitle; 512 | uint32_t dwX; 513 | uint32_t dwY; 514 | uint32_t dwXSize; 515 | uint32_t dwYSize; 516 | uint32_t dwXCountChars; 517 | uint32_t dwYCountChars; 518 | uint32_t dwFillAttribute; 519 | uint32_t dwFlags; 520 | uint16_t wShowWindow; 521 | uint16_t cbReserved2; 522 | void * lpReserved2; 523 | void ** hStdInput; 524 | void ** hStdOutput; 525 | void ** hStdError; 526 | } STARTUPINFOA, *LPSTARTUPINFOA; 527 | typedef struct _PROCESS_INFORMATION { 528 | void ** hProcess; 529 | void ** hThread; 530 | uint32_t dwProcessId; 531 | uint32_t dwThreadId; 532 | } PROCESS_INFORMATION, *LPPROCESS_INFORMATION; 533 | uint32_t CreateProcessA( 534 | void *, 535 | const char * commandLine, 536 | void *, 537 | void *, 538 | uint32_t, 539 | uint32_t, 540 | void *, 541 | const char * currentDirectory, 542 | LPSTARTUPINFOA, 543 | LPPROCESS_INFORMATION 544 | ); 545 | uint32_t CloseHandle(void **); 546 | ]] 547 | 548 | function execute(command_line, current_directory) 549 | local si = ffi.new"STARTUPINFOA" 550 | si.cb = ffi.sizeof(si) 551 | si.lpReserved = nil; 552 | si.lpDesktop = nil; 553 | si.lpTitle = nil; 554 | si.dwFlags = 1; -- STARTF_USESHOWWINDOW 555 | si.wShowWindow = 0; -- SW_HIDE 556 | si.cbReserved2 = 0; -- must be zero 557 | si.lpReserved2 = nil 558 | local pi = ffi.new"PROCESS_INFORMATION" 559 | local ok = ffi.C.CreateProcessA(nil, command_line, nil, nil, 0, 0, nil, current_directory, si, pi) ~= 0 560 | if ok then 561 | ffi.C.CloseHandle(pi.hProcess) 562 | ffi.C.CloseHandle(pi.hThread) 563 | end 564 | return ok -- true/false 565 | end 566 | end 567 | 568 | ffi.cdef[[ 569 | struct os_process_pipe; 570 | typedef struct os_process_pipe os_process_pipe_t; 571 | os_process_pipe_t *os_process_pipe_create(const char *cmd_line, const char *type); 572 | size_t os_process_pipe_read(os_process_pipe_t *pp, uint8_t *data, size_t len); 573 | size_t os_process_pipe_read_err(os_process_pipe_t *pp, uint8_t *data, size_t len); 574 | size_t os_process_pipe_write(os_process_pipe_t *pp, const uint8_t *data, size_t len); 575 | int os_process_pipe_destroy(os_process_pipe_t *pp); 576 | ]] 577 | 578 | function pp_execute(cmd_line) 579 | local pp = obsffi.os_process_pipe_create(cmd_line "r"); 580 | obsffi.os_process_pipe_destroy(pp) 581 | end 582 | 583 | function click_property(source, name) 584 | local props = obs_source_properties(source) 585 | local prop = obs_properties_get(props, name) 586 | obs_property_button_clicked(prop, source) 587 | obs_properties_destroy(props) 588 | end 589 | 590 | 591 | ffi.cdef[[ 592 | 593 | struct obs_source; 594 | struct obs_properties; 595 | struct obs_property; 596 | typedef struct obs_source obs_source_t; 597 | typedef struct obs_properties obs_properties_t; 598 | typedef struct obs_property obs_property_t; 599 | 600 | obs_source_t *obs_get_source_by_name(const char *name); 601 | obs_source_t *obs_source_get_filter_by_name(obs_source_t *source, const char *name); 602 | obs_properties_t *obs_source_properties(const obs_source_t *source); 603 | obs_property_t *obs_properties_first(obs_properties_t *props); 604 | bool obs_property_button_clicked(obs_property_t *p, void *obj); 605 | 606 | bool obs_property_next(obs_property_t **p); 607 | 608 | const char *obs_property_name(obs_property_t *p); 609 | void obs_properties_destroy(obs_properties_t *props); 610 | void obs_source_release(obs_source_t *source); 611 | 612 | ]] 613 | 614 | function click_property_filter_ffi(source, filter_name, prop_name) 615 | local source_name = return_source_name(source) 616 | local source = obsffi.obs_get_source_by_name(source_name) 617 | if source then 618 | local fSource = obsffi.obs_source_get_filter_by_name(source, filter_name) 619 | if fSource then 620 | local props = obsffi.obs_source_properties(fSource) 621 | if props then 622 | local prop = obsffi.obs_properties_first(props) 623 | local name = obsffi.obs_property_name(prop) 624 | if name then 625 | local _p = ffi.new("obs_property_t *[1]", prop) 626 | local foundProp = obsffi.obs_property_next(_p) 627 | prop = ffi.new("obs_property_t *", _p[0]) 628 | while foundProp do 629 | name = obsffi.obs_property_name(prop) 630 | if ffi.string(name) == prop_name then 631 | obsffi.obs_property_button_clicked(prop, fSource) 632 | end 633 | _p = ffi.new("obs_property_t *[1]", prop) 634 | foundProp = obsffi.obs_property_next(_p) 635 | prop = ffi.new("obs_property_t *", _p[0]) 636 | end 637 | end 638 | obsffi.obs_properties_destroy(props) 639 | end 640 | obsffi.obs_source_release(fSource) 641 | end 642 | obsffi.obs_source_release(source) 643 | end 644 | end 645 | 646 | _js_patch_loaded = false 647 | 648 | function patch_bs_js(version_num) if not _js_patch_loaded then -- begin patch_bs_js 649 | 650 | local C, ffi_new, ffi_copy, ffi_cast = ffi.C, ffi.new, ffi.copy, ffi.cast 651 | ffi.cdef[[ 652 | int VirtualProtect(uintptr_t, unsigned long, unsigned long, unsigned long *); 653 | uint64_t GetModuleHandleA(const char*); 654 | enum { PAGE_READWRITE = 0x04 }; 655 | ]] 656 | 657 | local offsets = { 658 | 0x96B60, --[1] circa late 2024 - early 2025 ~31.0.0 659 | 0x9A130, --[2] 2025_03_08 - version 31.0.2 660 | } 661 | local offset = offsets[version_num or #offsets] + 0x40 662 | 663 | local function virtual_protect(address, size, new_protect) 664 | local old_protect = ffi_new("unsigned long[1]") 665 | address = ffi_cast("uintptr_t", address) 666 | C.VirtualProtect(address, size, new_protect, old_protect) 667 | return old_protect[0] 668 | end 669 | 670 | local function post_load_cb() 671 | local sources = obs_enum_sources() 672 | if sources ~= nil then 673 | for _, source in ipairs(sources) do 674 | local source_id = obs_source_get_unversioned_id(source) 675 | if source_id == "browser_source" then 676 | local settings = obs_source_get_settings(source) 677 | obs_data_set_string(settings, "css", tostring(os.time())) 678 | obs_source_update(source, settings) 679 | obs_data_release(settings) 680 | end 681 | end 682 | end 683 | source_list_release(sources) 684 | remove_current_callback() 685 | end 686 | 687 | local function patch_preloaded_js() 688 | local js_code = "window.addEventListener('m',e=>eval(e.detail.k));//" 689 | local len = #js_code 690 | local buffer = ffi_new("char[?]", len) 691 | ffi_copy(buffer, js_code) 692 | local address = ffi_cast("void*", C.GetModuleHandleA("obs-browser.dll") + offset) 693 | local old_protection = virtual_protect(address, len, C.PAGE_READWRITE) 694 | ffi_copy(address, buffer, len) 695 | virtual_protect(address, len, old_protection) 696 | end 697 | 698 | patch_preloaded_js() 699 | timer_add(post_load_cb, 1500) 700 | print('[+] patch_bs_js activated') 701 | end _js_patch_loaded = true 702 | end -- end patch_bs_js 703 | 704 | --bk_console_instance_functions~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 705 | -- setfenv functions with nonlocal variable t(instance) 706 | -- tables creation in utils.some_table is prohibited! Use instance.some_table in SourceDef 707 | utils = {} 708 | 709 | function utils.send_js(code_str) 710 | local json = obs_data_create() 711 | obs_data_set_string(json, "k", code_str) 712 | local cd = calldata_create() 713 | local ph = obs_source_get_proc_handler(source) 714 | calldata_set_string(cd, "eventName", "m") 715 | calldata_set_string(cd, "jsonString", obs_data_get_json(json)) 716 | obs_data_release(json) 717 | proc_handler_call(ph, "javascript_event", cd) 718 | calldata_destroy(cd) 719 | end 720 | 721 | function utils.dx_screenshot(source_name) 722 | if not t.created_dx_ctx then return end 723 | obs_enter_graphics() 724 | local my_source = obs_get_source_by_name(source_name) 725 | local cx, cy = MAX_SIDE_W, MAX_SIDE_H 726 | local s_w = obs_source_get_width(my_source) 727 | local s_h = obs_source_get_height(my_source) 728 | obsffi.gs_texrender_reset(t.texture_a) 729 | if my_source and obsffi.gs_texrender_begin(t.texture_a, cx, cy) then 730 | gs_matrix_scale3f(cx/s_w, cy/s_h, 1.0) 731 | gs_clear(t.clear_flags, t.texture_clear_color, 0, 0) 732 | gs_ortho(0.0, cx, 0.0, cy, -100.0, 100.0) 733 | obs_source_inc_showing(my_source) 734 | obs_source_video_render(my_source) 735 | obs_source_dec_showing(my_source) 736 | obsffi.gs_texrender_end(t.texture_a) 737 | end 738 | t._tex = obsffi.gs_texture_get_obj(obsffi.gs_texrender_get_texture(t.texture_a)) 739 | t.mapped_subresource = c_tex2d_mapped_new{} 740 | t.p_mapped_subres = c_void_p(t.mapped_subresource) 741 | t.CopyResource(t.pContext, t.stage, t._tex) 742 | local x = t.Map(t.pContext, t.stage, 0, C.D3D11_MAP_READ, 0, t.p_mapped_subres)--print(x)--0x00000000 ok 743 | t.p_mapped_subres = c_tex2d_mapped_p(t.p_mapped_subres)--print(tostring(t.p_mapped_subres.RowPitch)) 744 | ffi.copy(t.raw_image, t.p_mapped_subres.pData, MAX_LEN) 745 | t.Unmap(t.pContext, t.stage, 0) 746 | obs_source_release(my_source) 747 | obs_leave_graphics() 748 | end 749 | 750 | function utils._set_stash(filter_name) 751 | local settings; 752 | local result = obs_source_enum_filters(source) 753 | for _, f in pairs(result) do 754 | if return_source_name(f) == filter_name then 755 | settings = obs_source_get_settings(f) 756 | end 757 | end 758 | source_list_release(result) 759 | -- obs_data_get_json_pretty_with_defaults 760 | t._json_settings[filter_name] = obs_data_get_json(settings) 761 | obs_data_release(settings) 762 | end 763 | 764 | function utils._grab_stash(filter_name) 765 | local result = obs_source_enum_filters(source) 766 | for _, f in pairs(result) do 767 | if return_source_name(f) == filter_name then 768 | set_settings3(source, filter_name, t._json_settings[filter_name]) 769 | end 770 | end 771 | source_list_release(result) 772 | end 773 | 774 | function utils.stash(filter_name) 775 | if t._stash_ready then _grab_stash(filter_name) end 776 | if not t._stash_ready then 777 | _set_stash(filter_name) 778 | t._stash_ready = true 779 | end 780 | end 781 | 782 | function utils.get_duration() 783 | return obs_source_media_get_duration(source) 784 | end 785 | 786 | function utils.get_timing() 787 | return obs_source_media_get_time(source) 788 | end 789 | 790 | function utils._play_once(a, b) 791 | local done = false 792 | obs_source_media_set_time(source, a) 793 | sleep(0) 794 | repeat 795 | if not (obs_source_media_get_time(source) >= b) then 796 | sleep(0) 797 | else 798 | done = true 799 | end 800 | until done 801 | return(0) 802 | end 803 | 804 | function utils.play_once(a, b) 805 | _play_once(0, 0) 806 | repeat sleep(0) until _play_once(a, b) == 0 807 | _play_once(0, 0) 808 | end 809 | 810 | function utils.res_defer(item) 811 | local id = tostring(item.res) 812 | 813 | for k, v in pairs(t._res_defer) do 814 | if v.created == id then 815 | return 816 | end 817 | end 818 | item.created = id 819 | table.insert(t._res_defer, item) 820 | end 821 | 822 | function utils.okay(name) t.pipe_name = name end 823 | 824 | function utils.execute_from_private_registry(address, tickrate) 825 | tickrate = tickrate or 1/60 826 | while true do 827 | proceed, code, handshake = get_code(address) 828 | sleep(tickrate) 829 | if proceed then 830 | executor(t, code, "external_py", "python_receiver") 831 | write_private_data("bool", handshake, false) 832 | end 833 | end 834 | end 835 | 836 | function utils.accept(address, tickrate) 837 | execute_from_private_registry(address, tickrate) 838 | end 839 | 840 | function utils.register_on_show(delayed_callback) 841 | t.on_show_do = function() 842 | t.on_show_task = run(function() delayed_callback() end) 843 | end 844 | end 845 | 846 | function utils.register_on_hide(delayed_callback) 847 | t.on_hide_do = function() 848 | t.on_hide_task = run(function() delayed_callback() end) 849 | end 850 | end 851 | 852 | function utils.register_on_activate(delayed_callback) 853 | t.on_activate_do = function() 854 | t.on_activate_task = run(function() delayed_callback() end) 855 | end 856 | end 857 | 858 | function utils.register_on_deactivate(delayed_callback) 859 | t.on_deactivate_do = function() 860 | t.on_deactivate_task = run(function() delayed_callback() end) 861 | end 862 | end 863 | 864 | function utils.get_gap_source(opts) 865 | local gap, settings; 866 | local w = opts.w 867 | local h = opts.h 868 | local n = opts.n 869 | settings = obs_data_create() 870 | obs_data_set_double(settings, "_width", w) 871 | obs_data_set_double(settings, "_height", h) 872 | gap = obs_source_create("_gap_source", n, settings, nil) 873 | return gap, settings 874 | end 875 | 876 | function utils.__c(source, settings) 877 | -- clear current context 878 | obs_source_release(source) 879 | obs_data_release(settings) 880 | end 881 | 882 | function utils.add_outer_gap(size) 883 | size = size or 15 884 | if not t.__scene then -- otherwise its crashes 885 | t.__scene = obs_scene_from_source(source) 886 | end 887 | local width = obs_source_get_base_width(source) 888 | local height = obs_source_get_base_height(source) 889 | 890 | local rgap, rsettings = get_gap_source({w=size, h=height, n="_right_gap"}); 891 | local lgap, lsettings = get_gap_source({w=size, h=height, n="_left_gap"}); 892 | local ugap, usettings = get_gap_source({w=width, h=size, n="_up_gap"}); 893 | local dgap, dsettings = get_gap_source({w=width, h=size, n="_down_gap"}); 894 | local rpos, lpos, upos, dpos = vec2(), vec2(), vec2(), vec2() 895 | local r = obs_scene_add(t.__scene, rgap); __c(rgap, rsettings) 896 | local l = obs_scene_add(t.__scene, lgap); __c(lgap, lsettings) 897 | local u = obs_scene_add(t.__scene, ugap); __c(ugap, usettings) 898 | local d = obs_scene_add(t.__scene, dgap); __c(dgap, dsettings) 899 | lpos.x, lpos.y = 0, 0; obs_sceneitem_set_pos(l, lpos) 900 | rpos.x, rpos.y = width - size, 0; obs_sceneitem_set_pos(r, rpos) 901 | upos.x, upos.y = 0, 0; obs_sceneitem_set_pos(u, upos) 902 | dpos.x, dpos.y = 0, height - size; obs_sceneitem_set_pos(d, dpos) 903 | end 904 | 905 | function utils.delete_all_gaps() 906 | if not t.__scene then -- otherwise its crashes 907 | t.__scene = obs_scene_from_source(source) 908 | end 909 | local items = obs_scene_enum_items(t.__scene) 910 | for _, i in pairs(items) do 911 | if obs_source_get_unversioned_id(obs_sceneitem_get_source(i)) == '_gap_source' then 912 | obs_sceneitem_remove(i) 913 | end 914 | end 915 | sceneitem_list_release(items) 916 | end 917 | 918 | function utils._update_gap_base(gs, opts) 919 | local settings = obs_source_get_settings(gs) 920 | obs_data_set_double(settings, "_width", opts.w) 921 | obs_data_set_double(settings, "_height", opts.h) 922 | obs_source_update(gs, settings) 923 | obs_data_release(settings) 924 | end 925 | 926 | function utils._set_gap(gs, gi, size, width, height) 927 | local pos = vec2() 928 | local name = obs_source_get_name(gs) 929 | 930 | if name == '_right_gap' then 931 | _update_gap_base(gs, {w = size, h = height}) 932 | pos.x, pos.y = width - size, 0; obs_sceneitem_set_pos(gi, pos) 933 | elseif name =='_left_gap' then 934 | _update_gap_base(gs, {w = size, h = height}) 935 | pos.x, pos.y = 0, 0; obs_sceneitem_set_pos(gi, pos) 936 | elseif name =='_up_gap' then 937 | _update_gap_base(gs, {w = width, h = size}) 938 | pos.x, pos.y = 0, 0; obs_sceneitem_set_pos(gi, pos) 939 | elseif name =='_down_gap' then 940 | _update_gap_base(gs, {w = width, h = size}) 941 | pos.x, pos.y = 0, height - size; obs_sceneitem_set_pos(gi, pos) 942 | end 943 | end 944 | 945 | function utils.resize_outer_gaps(size) 946 | size = size or 15 947 | if not t.__scene then -- otherwise its crashes 948 | t.__scene = obs_scene_from_source(source) 949 | end 950 | local items = obs_scene_enum_items(t.__scene) 951 | local width = obs_source_get_base_width(source) 952 | local height = obs_source_get_base_height(source) 953 | for _, i in pairs(items) do 954 | local s = obs_sceneitem_get_source(i) 955 | if obs_source_get_unversioned_id(s) == '_gap_source' then 956 | _set_gap(s, i, size, width, height) 957 | end 958 | end 959 | sceneitem_list_release(items) 960 | end 961 | 962 | function utils.add_gap(opts) 963 | -- add_gap {x=300, y=500, width = 100, height = 100} 964 | if not t.__scene then -- otherwise its crashes 965 | t.__scene = obs_scene_from_source(source) 966 | end 967 | local gap, settings = get_gap_source({w=opts.width, h=opts.height, n="_unnamed_gap"}); 968 | local item = obs_scene_add(t.__scene, gap); __c(gap, settings) 969 | local pos = vec2(); pos.x, pos.y = opts.x, opts.y 970 | obs_sceneitem_set_pos(item, pos) 971 | end 972 | 973 | --bk_console_snippets_code~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 974 | SNIPPETS = {} -- { name = string value} 975 | SNIPPETS.s_general_stats = [===[ 976 | 977 | ffi.cdef[[ 978 | struct video_output; 979 | typedef struct video_output video_t; 980 | 981 | struct os_cpu_usage_info; 982 | typedef struct os_cpu_usage_info os_cpu_usage_info_t; 983 | 984 | uint32_t video_output_get_skipped_frames(const video_t *video); 985 | uint32_t video_output_get_total_frames(const video_t *video); 986 | double video_output_get_frame_rate(const video_t *video); 987 | 988 | os_cpu_usage_info_t *os_cpu_usage_info_start(void); 989 | double os_cpu_usage_info_query(os_cpu_usage_info_t *info); 990 | void os_cpu_usage_info_destroy(os_cpu_usage_info_t *info); 991 | 992 | video_t *obs_get_video(void); 993 | ]] 994 | 995 | 996 | local s = {} 997 | 998 | s.lagged_frames = "" 999 | s.lagged_total_frames = "" 1000 | s.lagged_percents = "" 1001 | 1002 | s.skipped_frames = "" 1003 | s.skipped_total_frames = "" 1004 | s.skipped_percents = "" 1005 | 1006 | s.dropped_frames = "" 1007 | s.dropped_total_frames = "" 1008 | s.dropped_percents = "" 1009 | 1010 | s.congestion = "" 1011 | s.average_congestion = "" 1012 | 1013 | s.memory_usage = "" 1014 | s.cpu_usage = "" 1015 | s.cpu_cores = "" 1016 | 1017 | s.average_frame_time = "" 1018 | s.fps = "" 1019 | s.target_fps = "30" 1020 | s.average_fps = "" 1021 | 1022 | s.bitrate = "" 1023 | 1024 | s.streaming_status = "Offline" 1025 | s.recording_status = "Off" 1026 | 1027 | s.bitrate = 0 1028 | s.last_bytes_sent = 0 1029 | s.last_bytes_time = 0 1030 | 1031 | s.recording_bitrate = 0 1032 | s.recording_last_bytes_recorded = 0 1033 | 1034 | s.total_ticks = 0 1035 | s.congestion_cumulative = 0 1036 | s.fps_cumulative = 0 1037 | 1038 | s.is_live = false 1039 | 1040 | 1041 | function obs_stats_tick() 1042 | s.total_ticks = s.total_ticks + 1 1043 | 1044 | -- Get CPU usage 1045 | local cpu_usage = 0.0 1046 | s.cpu_usage = obsffi.os_cpu_usage_info_query(t.cpu_info) 1047 | 1048 | -- Get memory usage 1049 | local memory_usage = os_get_proc_resident_size() / (1024.0 * 1024.0) 1050 | 1051 | -- Get FPS/framerate 1052 | local fps = obs_get_active_fps() 1053 | s.fps_cumulative = s.fps_cumulative + fps 1054 | 1055 | -- Get average time to render frame 1056 | local average_frame_time = obs_get_average_frame_time_ns() / 1000000.0 1057 | 1058 | -- Get lagged/missed frames 1059 | local rendered_frames = obs_get_total_frames() 1060 | local lagged_frames = obs_get_lagged_frames() 1061 | 1062 | -- Get skipped frames 1063 | local encoded_frames = 0 1064 | local skipped_frames = 0 1065 | 1066 | local video = obsffi.obs_get_video() 1067 | if video ~= nil then 1068 | encoded_frames = obsffi.video_output_get_total_frames(video) 1069 | skipped_frames = obsffi.video_output_get_skipped_frames(video) 1070 | end 1071 | 1072 | -- Get dropped frames, congestion and total bytes 1073 | local dropped_frames = 0 1074 | local congestion = 0.0 1075 | local total_bytes = 0 1076 | local total_frames = 0 1077 | 1078 | -- local streaming_status = is_live ? "Live" : "Offline" 1079 | local streaming_status = "Offline" 1080 | if s.is_live then 1081 | streaming_status = "Live" 1082 | end 1083 | 1084 | local streaming_duration_total_seconds = 0 1085 | 1086 | local streaming_output = obs_frontend_get_streaming_output() 1087 | -- output will be nil when not actually streaming 1088 | if streaming_output ~= nil then 1089 | dropped_frames = obs_output_get_frames_dropped(streaming_output) 1090 | congestion = obs_output_get_congestion(streaming_output) 1091 | total_bytes = obs_output_get_total_bytes(streaming_output) 1092 | --local connect_time = obs_output_get_connect_time_ms(streaming_output) 1093 | 1094 | -- Streaming status 1095 | local is_reconnecting = obs_output_reconnecting(streaming_output) 1096 | if is_reconnecting then 1097 | streaming_status = "Reconnecting" 1098 | end 1099 | 1100 | -- Get streaming duration 1101 | total_frames = obs_output_get_total_frames(streaming_output) 1102 | streaming_duration_total_seconds = total_frames / fps 1103 | 1104 | obs_output_release(streaming_output) 1105 | end 1106 | 1107 | -- Check that congestion is not NaN 1108 | if(congestion == congestion) then 1109 | s.congestion_cumulative = s.congestion_cumulative + congestion 1110 | end 1111 | 1112 | -- Get bitrate 1113 | local current_time = os_gettime_ns() 1114 | local time_passed = (current_time - s.last_bytes_time) / 1000000000.0 1115 | 1116 | if time_passed > 2.0 then 1117 | local bytes_sent = total_bytes 1118 | 1119 | if bytes_sent < s.last_bytes_sent then 1120 | bytes_sent = 0 1121 | end 1122 | if bytes_sent == 0 then 1123 | s.last_bytes_sent = 0 1124 | end 1125 | 1126 | local bits_between = (bytes_sent - s.last_bytes_sent) * 8 1127 | bitrate = bits_between / time_passed / 1000.0 1128 | 1129 | s.last_bytes_sent = bytes_sent 1130 | s.last_bytes_time = current_time 1131 | end 1132 | 1133 | local recording_duration_total_seconds = 0 1134 | 1135 | -- Get recording bitrate 1136 | if obs_frontend_recording_active() then 1137 | local recording_output = obs_frontend_get_recording_output() 1138 | local recording_total_bytes = 0 1139 | 1140 | if recording_output ~= nil then 1141 | recording_total_bytes = obs_output_get_total_bytes(recording_output) 1142 | 1143 | -- Get recording duration 1144 | local recording_total_frames = obs_output_get_total_frames(recording_output) 1145 | recording_duration_total_seconds = recording_total_frames / fps 1146 | 1147 | obs_output_release(recording_output) 1148 | end 1149 | 1150 | if time_passed > 2.0 then 1151 | local recording_bytes_recorded = recording_total_bytes 1152 | 1153 | if recording_bytes_recorded < s.recording_last_bytes_recorded then 1154 | recording_bytes_recorded = 0 1155 | end 1156 | if recording_bytes_recorded == 0 then 1157 | s.recording_last_bytes_recorded = 0 1158 | end 1159 | 1160 | local recording_bits_between = (recording_bytes_recorded - s.recording_last_bytes_recorded) * 8 1161 | s.recording_bitrate = recording_bits_between / time_passed / 1000.0 1162 | 1163 | s.recording_last_bytes_recorded = recording_bytes_recorded 1164 | end 1165 | end 1166 | 1167 | -- fix NaN 1168 | if rendered_frames == 0 then rendered_frames = 1 end 1169 | if encoded_frames == 0 then encoded_frames = 1 end 1170 | if total_frames == 0 then total_frames = 1 end 1171 | if s.total_ticks == 0 then s.total_ticks = 1 end 1172 | 1173 | -- Update strings with new values 1174 | s.lagged_frames = tostring(lagged_frames) 1175 | s.lagged_total_frames = tostring(rendered_frames) 1176 | s.lagged_percents = string.format("%.1f", 100.0 * lagged_frames / rendered_frames) 1177 | 1178 | s.skipped_frames = tostring(skipped_frames) 1179 | s.skipped_total_frames = tostring(encoded_frames) 1180 | s.skipped_percents = string.format("%.1f", 100.0 * skipped_frames / encoded_frames) 1181 | 1182 | s.dropped_frames = tostring(dropped_frames) 1183 | s.dropped_total_frames = tostring(total_frames) 1184 | s.dropped_percents = string.format("%.1f", 100.0 * dropped_frames / total_frames) 1185 | 1186 | s.congestion = string.format("%.2f", 100 * congestion) 1187 | s.average_congestion = string.format("%.2f", 100 * s.congestion_cumulative / s.total_ticks) 1188 | 1189 | s.average_frame_time = string.format("%.1f", average_frame_time) 1190 | s.fps = string.format("%.2g", fps) 1191 | s.average_fps = string.format("%.2g", s.fps_cumulative / s.total_ticks) 1192 | 1193 | s.memory_usage = string.format("%.1f", memory_usage) 1194 | s.cpu_usage = string.format("%.1f", cpu_usage) 1195 | 1196 | s.bitrate = string.format("%.0f", bitrate) 1197 | s.recording_bitrate = string.format("%.0f", s.recording_bitrate) 1198 | 1199 | s.streaming_status = string.format("%s", streaming_status) 1200 | 1201 | end 1202 | 1203 | function tick_script_update() 1204 | 1205 | local physical_cores = os_get_physical_cores() 1206 | local logical_cores = os_get_logical_cores() 1207 | 1208 | is_live = obs_frontend_streaming_active() 1209 | if obs_frontend_recording_active() then 1210 | if obs_frontend_recording_paused() then 1211 | s.recording_status = "Paused" 1212 | else 1213 | s.recording_status = "On" 1214 | end 1215 | else 1216 | s.recording_status = "Off" 1217 | end 1218 | s.cpu_cores = string.format("%sC/%sT", physical_cores, logical_cores) 1219 | 1220 | if not t.cpu_info then 1221 | t.cpu_info = obsffi.os_cpu_usage_info_start() 1222 | res_defer {res = t.cpu_info, defer = obsffi.os_cpu_usage_info_destroy } 1223 | end 1224 | end 1225 | 1226 | repeat sleep(0.3) 1227 | tick_script_update() 1228 | obs_stats_tick() 1229 | local info_stats = '' 1230 | for k, v in pairs(s) do info_stats = info_stats .. ("[%s] [%s] \n"):format(k, v) end 1231 | local function update_text(source, text) local settings = obs_data_create() obs_data_set_string(settings, "text", text) 1232 | obs_source_update(source, settings) obs_data_release(settings) end 1233 | update_text(source, info_stats) 1234 | 1235 | 1236 | until false 1237 | 1238 | ]===] 1239 | 1240 | SNIPPETS.s_loop_media = [==[ 1241 | 1242 | local loop = {} 1243 | function set_loop() 1244 | if not loop.start then loop.start = obs_source_media_get_time(source) sleep(0.2) return end 1245 | if not loop._end then loop._end = obs_source_media_get_time(source) sleep(0.2) return end 1246 | 1247 | end 1248 | function watch_duration() 1249 | if loop._end then 1250 | local current = obs_source_media_get_time(source) 1251 | if current >= loop._end then 1252 | obs_source_media_set_time(source, loop.start) 1253 | end 1254 | end 1255 | end 1256 | repeat sleep(0) 1257 | if t.pressed then set_loop() end 1258 | if t.pressed2 then loop.start, loop._end = nil, nil end 1259 | watch_duration() 1260 | until false 1261 | 1262 | ]==] 1263 | 1264 | SNIPPETS.s_on_off_sceneitem = [==[ 1265 | 1266 | local name = "" 1267 | local scene_item = get_scene_sceneitem(return_source_name(source), name) 1268 | repeat sleep(2.5) 1269 | local boolean = not obs_sceneitem_visible(scene_item) 1270 | obs_sceneitem_set_visible(scene_item, boolean) 1271 | until false 1272 | 1273 | ]==] 1274 | 1275 | SNIPPETS.s_browser_refresh = [==[ 1276 | 1277 | repeat sleep(1*60*15) 1278 | click_property(source, "refreshnocache") 1279 | until false 1280 | 1281 | ]==] 1282 | 1283 | SNIPPETS.s_render_delay = [==[ 1284 | 1285 | local filter_name = "Render Delay" 1286 | set_settings3(source, filter_name, '{"delay_ms":3000}') 1287 | 1288 | ]==] 1289 | 1290 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 1291 | 1292 | CODE_STORAGE_INIT = [===[ 1293 | 1294 | -- leave empty new line with 2 spaces, there might be bootstraping and initialization code here 1295 | 1296 | ]===] 1297 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 1298 | -- function script_tick(dt) end -- external loop, can be used for message bus/signalling 1299 | run = coroutine.create 1300 | init = function() return run(function() coroutine.yield() end) end 1301 | SUB = {} -- {{"pipe_name": num_code}, ...} 1302 | gn = {} 1303 | 1304 | _OFFER = 111;function offer(pipe_name) SUB[pipe_name] = _OFFER end 1305 | _STALL = 222;function stall(pipe_name) SUB[pipe_name] = _STALL end 1306 | _FORWARD = 333;function forward(pipe_name) SUB[pipe_name] = _FORWARD end 1307 | _SWITCH = 444;function switch(pipe_name) SUB[pipe_name] = _SWITCH end 1308 | _RECOMPILE = 555;function recompile(pipe_name) SUB[pipe_name] = _RECOMPILE end 1309 | 1310 | local function executor(ctx, code, loc, name) -- args defined automatically as local 1311 | local custom_env52 = {} 1312 | setmetatable(custom_env52, {__index = _G}) 1313 | custom_env52.source = obs_filter_get_parent(ctx.filter) 1314 | loc = loc or "exec" -- special location address if the python script is present 1315 | name = name or "obs repl" 1316 | custom_env52.t = ctx 1317 | code = code or custom_env52.t.code 1318 | for k, v in pairs(utils) do custom_env52[k] = setfenv(v, custom_env52) end 1319 | local exec = assert(load(CODE_STORAGE_INIT .. code, name, "t", custom_env52)) 1320 | -- executor submits code to the event loop, which will execute it with .resume 1321 | ctx[loc] = run(exec) 1322 | end 1323 | 1324 | local function skip_tick_render(ctx) 1325 | local target = obs_filter_get_target(ctx.filter) 1326 | local width, height; 1327 | if target == nil then width = 0; height = 0; else 1328 | width = obs_source_get_base_width(target) 1329 | height = obs_source_get_base_height(target) 1330 | end 1331 | ctx.width, ctx.height = width , height 1332 | end 1333 | 1334 | local function viewer() 1335 | error(">Script Log") 1336 | end 1337 | --bk_obs_source_definition~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 1338 | local SourceDef = {} 1339 | 1340 | function SourceDef:new(o) 1341 | o = o or {} 1342 | setmetatable(o, self) 1343 | self.__index = self 1344 | return o 1345 | end 1346 | 1347 | function SourceDef:_set_timer_loop() 1348 | local value = SourceDef.__interval 1349 | local ms = (1/value) * 1000; ms = ms + (2^52 + 2^51) - (2^52 + 2^51) 1350 | local interval = 1/value 1351 | timer_add(function() SourceDef._event_loop(self, interval) end, ms) 1352 | self.loop_executor_timer = true 1353 | end 1354 | 1355 | function SourceDef:create(source) 1356 | local instance = {} 1357 | instance.filter = source -- filter source itself 1358 | instance.hotkeys = {} 1359 | instance.hk = {} 1360 | instance.pressed = false 1361 | instance.pressed2 = false 1362 | instance.pressed3 = false 1363 | instance.created_hotkeys = false 1364 | 1365 | instance.created_dx_ctx = false 1366 | 1367 | instance.button_dispatch = false 1368 | instance.preload = true 1369 | instance.hotkey_dispatch = false 1370 | instance.actions_dispatch = false 1371 | instance.is_action_paused = false 1372 | instance.external_dispatch = false 1373 | 1374 | instance.tasks = {} 1375 | instance.exec = init() 1376 | instance.external_py = init() 1377 | instance.exec_action_code = init() 1378 | instance.on_show_task = init() 1379 | instance.on_hide_task = init() 1380 | instance.on_activate_task = init() 1381 | instance.on_deactivate_task = init() 1382 | 1383 | instance._res_defer = {} -- { {res = some_resource, defer = some_cleanup_callback, created = id }, ...} 1384 | instance._json_settings = {} 1385 | instance._stash_ready = false 1386 | 1387 | if obs_source_get_unversioned_id(source):find("timer") then 1388 | SourceDef._set_timer_loop(instance) 1389 | end 1390 | SourceDef.update(instance, self) -- self = settings 1391 | executor(instance) -- preload coroutines to start when hotkey or external 1392 | return instance 1393 | end 1394 | 1395 | function SourceDef:destroy() 1396 | for k, v in pairs(self._res_defer) do 1397 | v.defer(v.res) 1398 | end 1399 | if self.created_dx_ctx then 1400 | obs_enter_graphics() 1401 | self.Release_pContext(self.pContext) -- should automatically clear all resources created with it 1402 | obsffi.gs_texrender_destroy(self.texture_a) 1403 | obs_leave_graphics() 1404 | end 1405 | end 1406 | 1407 | function SourceDef:update(settings) 1408 | self.code = obs_data_get_string(settings, "_text") 1409 | self.action_code = obs_data_get_string(settings, "_action") 1410 | self.autorun = obs_data_get_bool(settings, "_autorun") 1411 | self.mv1 = obs_data_get_double(settings, "_mv1") 1412 | self.mv2 = obs_data_get_double(settings, "_mv2") 1413 | self.hotreload = obs_data_get_string(settings, "_hotreload") 1414 | self.p1 = obs_data_get_string(settings, "_p1") 1415 | self.p2 = obs_data_get_string(settings, "_p2") 1416 | self.external_dispatch = obs_data_get_bool(settings, "_external_dispatch") 1417 | if self.external_dispatch then 1418 | executor(self) 1419 | end 1420 | self.expect_dx = obs_data_get_bool(settings, "t4") 1421 | self.snippet_name = obs_data_get_string(settings, "_snippet_name") 1422 | 1423 | if not self.created_hotkeys then 1424 | SourceDef._reg_htk(self, settings) 1425 | end 1426 | if self.expect_dx and (not self.created_dx_ctx) then 1427 | SourceDef._create_dx_ctx(self) 1428 | end 1429 | end 1430 | 1431 | function SourceDef:_event_loop(seconds) 1432 | -- button restarts code on a click 1433 | -- actions and external python code does the same 1434 | -- hotkey trigger waits until execution has finished 1435 | 1436 | if self.button_dispatch then 1437 | coroutine.resume(self.exec, seconds) -- begin/continue execution; yields, obslua API code is not suspendable 1438 | if coroutine.status(self.exec) == "dead" then 1439 | self.button_dispatch = false 1440 | executor(self) -- preparing for hotkey_dispatch or external_dispatch 1441 | goto end_of_manual_triggers 1442 | end 1443 | 1444 | elseif self.hotkey_dispatch then 1445 | coroutine.resume(self.exec, seconds) 1446 | if coroutine.status(self.exec) == "dead" then 1447 | executor(self) 1448 | self.hotkey_dispatch = false 1449 | goto end_of_manual_triggers 1450 | end 1451 | 1452 | elseif self.external_dispatch then 1453 | coroutine.resume(self.exec, seconds) 1454 | if coroutine.status(self.exec) == "dead" then 1455 | executor(self) 1456 | local settings = obs_source_get_settings(self.filter) 1457 | obs_data_set_bool(settings, "_external_dispatch", false) 1458 | obs_source_update(self.filter, settings) 1459 | obs_data_release(settings) 1460 | goto end_of_manual_triggers 1461 | end 1462 | 1463 | elseif self.autorun then 1464 | if self.preload then 1465 | self.preload = false 1466 | executor(self) 1467 | end 1468 | coroutine.resume(self.exec, seconds) 1469 | 1470 | end 1471 | ::end_of_manual_triggers:: 1472 | 1473 | for _, coro in pairs(self.tasks) do 1474 | coroutine.resume(coro, seconds) 1475 | end 1476 | 1477 | for _, i in pairs {"show", "hide", "activate", "deactivate"} do 1478 | coroutine.resume(self["on_"..i.."_task"], seconds) 1479 | if self["emit_"..i] and self["on_"..i.."_do"] 1480 | and coroutine.status(self["on_"..i.."_task"]) == "dead" then -- blocking 1481 | self["emit_"..i] = false 1482 | self["on_"..i.."_do"]() 1483 | end 1484 | end 1485 | -- poll for changes in the global shared table for all of console sources 1486 | for name, num_code in pairs(SUB) do 1487 | if num_code == _OFFER and self.pipe_name == name then 1488 | self.actions_dispatch = true 1489 | SUB[name] = 999 1490 | elseif num_code == _STALL and self.pipe_name == name then 1491 | self.is_action_paused = true 1492 | SUB[name] = 999 1493 | elseif num_code == _FORWARD and self.pipe_name == name then 1494 | self.is_action_paused = false 1495 | SUB[name] = 999 1496 | elseif num_code == _SWITCH and self.pipe_name == name then 1497 | self.is_action_paused = not self.is_action_paused 1498 | SUB[name] = 999 1499 | elseif num_code == _RECOMPILE and self.pipe_name == name then 1500 | executor(self, self.action_code, "exec_action_code", "actions entry recompiled") 1501 | SUB[name] = 999 1502 | end 1503 | end 1504 | if self.actions_dispatch then 1505 | executor(self, self.action_code, "exec_action_code", "actions entry") 1506 | self.actions_dispatch = false 1507 | end 1508 | if not self.is_action_paused then 1509 | coroutine.resume(self.exec_action_code, seconds) 1510 | end 1511 | 1512 | coroutine.resume(self.external_py, seconds) 1513 | 1514 | end 1515 | 1516 | function SourceDef:get_properties() 1517 | local props = obs_properties_create() 1518 | local text_area = obs_properties_add_text(props, "_text", "", OBS_TEXT_MULTILINE) 1519 | obs_property_text_set_monospace(text_area, true) 1520 | obs_properties_add_button(props, "button1", i18n"execute", function() 1521 | self.button_dispatch = true 1522 | executor(self) 1523 | end) 1524 | local s = "+ - - - - - - - - - - [ " .. i18n"view_output" 1525 | .. " ] - - - - - - - - - - +" 1526 | obs_properties_add_button(props, "button2", s, viewer) 1527 | obs_properties_add_bool(props, "_autorun", i18n"auto_run") 1528 | 1529 | -- Show/Hide properties, allows for different customization of each source 1530 | local group_config = obs_properties_create() 1531 | local pt1 = obs_properties_add_bool(group_config, "t1", i18n"p_text_area2") 1532 | obs_property_set_modified_callback(pt1, function(props, prop, set) 1533 | local flag = obs_data_get_bool(set, "t1") 1534 | obs_property_set_visible(obs_properties_get(props, "_action"), flag) 1535 | return true end) 1536 | 1537 | local pt2 = obs_properties_add_bool(group_config, "t2", i18n"_snippets") 1538 | obs_property_set_modified_callback(pt2, function(props, prop, set) 1539 | local flag = obs_data_get_bool(set, "t2") 1540 | obs_property_set_visible(obs_properties_get(props, "_groupextra"), flag) 1541 | return true end) 1542 | 1543 | local pt3 = obs_properties_add_bool(group_config, "t3", i18n"p_group_name") 1544 | obs_property_set_modified_callback(pt3, function(props, prop, set) 1545 | local flag = obs_data_get_bool(set, "t3") 1546 | obs_property_set_visible(obs_properties_get(props, "_group"), flag) 1547 | return true end) 1548 | 1549 | local pt4 = obs_properties_add_bool(group_config, "t4", i18n"p_dx_screenshot") 1550 | 1551 | local g1 = obs_properties_add_group(props, "_group_config", i18n"sh_checkbox", OBS_GROUP_CHECKABLE, group_config) 1552 | 1553 | obs_property_set_modified_callback(g1, function(props, prop, set) 1554 | local flag = obs_data_get_bool(set, "_group_config") 1555 | obs_property_set_visible(obs_properties_get(props, "t1") , flag) 1556 | obs_property_set_visible(obs_properties_get(props, "t2") , flag) 1557 | obs_property_set_visible(obs_properties_get(props, "t3") , flag) 1558 | obs_property_set_visible(obs_properties_get(props, "t4") , flag) 1559 | return true end) 1560 | 1561 | local text_area2 = obs_properties_add_text(props, "_action", "", OBS_TEXT_MULTILINE) 1562 | obs_property_text_set_monospace(text_area2, true) 1563 | local snippets = obs_properties_create() 1564 | local mylist = obs_properties_add_list(snippets, "_snippet_name", "", OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING) 1565 | for k, _ in pairs(SNIPPETS) do 1566 | obs_property_list_add_string(mylist, i18n(k), k) 1567 | end 1568 | 1569 | local btn = obs_properties_add_button(snippets, "button3", i18n"_snip_select", function() 1570 | local s = obs_source_get_settings(self.filter) 1571 | if not obs_data_get_bool(s, "_confirm") then obs_data_release(s) return true end 1572 | obs_data_set_string(s, "_text", SNIPPETS[obs_data_get_string(s, "_snippet_name")]) 1573 | obs_data_set_bool(s, "_confirm", false) 1574 | obs_source_update(self.filter, s) 1575 | obs_data_release(s) 1576 | return true 1577 | end) 1578 | obs_properties_add_bool(snippets, "_confirm", i18n"_snip_confirm") 1579 | obs_properties_add_group(props, "_groupextra", i18n"_snippets", OBS_GROUP_NORMAL, snippets) 1580 | 1581 | local group_props = obs_properties_create() 1582 | local _mv1, _mv2, _hotreload, _p1, _p2, _external_dispatch_p; 1583 | _mv1 = obs_properties_add_float_slider(group_props, "_mv1", i18n"s_mv1", 0, 1, 0.01) 1584 | _mv2 = obs_properties_add_int_slider(group_props, "_mv2", i18n"s_mv2", 0, 100, 1) 1585 | _hotreload = obs_properties_add_text(group_props, "_hotreload", i18n"hotreload", OBS_TEXT_DEFAULT) 1586 | _p1 = obs_properties_add_path(group_props, "_p1", i18n"p1", OBS_PATH_FILE, "*.lua", script_path()) 1587 | _p2 = obs_properties_add_path(group_props, "_p2", i18n"p2", OBS_PATH_FILE, "*.lua", script_path()) 1588 | _external_dispatch_p = obs_properties_add_bool(group_props, "_external_dispatch", "__private_do_not_use") 1589 | obs_property_set_visible(_external_dispatch_p,false) 1590 | obs_properties_add_group(props, "_group", i18n"p_group_name", OBS_GROUP_NORMAL, group_props) 1591 | 1592 | return props 1593 | end 1594 | 1595 | function SourceDef:show() 1596 | self.emit_show = true -- going to preview 1597 | end 1598 | 1599 | function SourceDef:hide() 1600 | self.emit_hide = true -- hiding from preview 1601 | end 1602 | 1603 | function SourceDef:activate() 1604 | self.emit_activate = true -- going to program 1605 | end 1606 | 1607 | function SourceDef:deactivate() 1608 | self.emit_deactivate = true -- retiring from program 1609 | end 1610 | 1611 | 1612 | function SourceDef:video_render(effect) 1613 | local target = obs_filter_get_parent(self.filter) 1614 | if target ~= nil then -- do not render, assign height & width to make scene item source selectable 1615 | self.width = obs_source_get_base_width(target) 1616 | self.height = obs_source_get_base_height(target) 1617 | end 1618 | obs_source_skip_video_filter(self.filter) 1619 | end 1620 | 1621 | function SourceDef:get_width() return self.width end 1622 | 1623 | function SourceDef:get_height() return self.height end 1624 | 1625 | function SourceDef:get_name() return i18n"Console" end 1626 | 1627 | function SourceDef:load(settings) SourceDef._reg_htk(self, settings) end 1628 | 1629 | function SourceDef:video_tick(seconds) 1630 | if not self.loop_executor_timer then 1631 | SourceDef._event_loop(self, seconds) 1632 | end 1633 | skip_tick_render(self) -- if source has crop or transform applied to it, this will let it render 1634 | end 1635 | 1636 | function SourceDef:save(settings) 1637 | if self.created_hotkeys then 1638 | self.created_hotkeys = true 1639 | end 1640 | for k, v in pairs(self.hotkeys) do 1641 | local a = obs_hotkey_save(self.hk[k]) 1642 | obs_data_set_array(settings, k, a) 1643 | obs_data_array_release(a) 1644 | end 1645 | end 1646 | 1647 | function SourceDef:_reg_htk(settings) 1648 | local parent = obs_filter_get_parent(self.filter) 1649 | local source_name = obs_source_get_name(parent) 1650 | local filter_name = obs_source_get_name(self.filter) 1651 | -- sets filter state off when starting and creating from UI due to bug with selection 1652 | -- OBS may hang when selecting a sceneitem and moving it, when it has filter enabled 1653 | obs_source_set_enabled(self.filter, false) 1654 | if parent and source_name and filter_name then 1655 | self.hotkeys["0;" .. source_name .. ";" .. filter_name] = function() 1656 | self.hotkey_dispatch = true 1657 | end 1658 | self.hotkeys["1;" .. source_name .. ";" .. filter_name] = function(pressed) 1659 | self.pressed = pressed 1660 | end 1661 | self.hotkeys["2;" .. source_name .. ";" .. filter_name] = function(pressed) 1662 | self.pressed2 = pressed 1663 | end 1664 | self.hotkeys["3;" .. source_name .. ";" .. filter_name] = function(pressed) 1665 | self.pressed3 = pressed 1666 | end 1667 | 1668 | for k, v in pairs(self.hotkeys) do 1669 | self.hk[k] = OBS_INVALID_HOTKEY_ID 1670 | end 1671 | 1672 | function reroute_hotkey_state(k) 1673 | self.hk[k] = obs_hotkey_register_frontend(k, k, function(pressed) 1674 | if pressed then 1675 | self.hotkeys[k](true) 1676 | else 1677 | self.hotkeys[k](false) 1678 | end 1679 | end) 1680 | end 1681 | 1682 | for k, v in pairs(self.hotkeys) do 1683 | if k:sub(1, 1) == "1" then -- starts with 1 symbol 1684 | reroute_hotkey_state(k) 1685 | elseif k:sub(1, 1) == "2" then 1686 | reroute_hotkey_state(k) 1687 | elseif k:sub(1, 1) == "3" then 1688 | reroute_hotkey_state(k) 1689 | else 1690 | self.hk[k] = obs_hotkey_register_frontend(k, k, function(pressed) 1691 | if pressed then 1692 | self.hotkeys[k]() 1693 | end 1694 | end) 1695 | end 1696 | local a = obs_data_get_array(settings, k) 1697 | obs_hotkey_load(self.hk[k], a) 1698 | obs_data_array_release(a) 1699 | end 1700 | if not self.created_hotkeys then 1701 | self.created_hotkeys = true 1702 | end 1703 | end 1704 | end 1705 | 1706 | if ffi.os == 'Windows' then 1707 | ffi.cdef[[ 1708 | void *gs_get_device_obj(void); 1709 | 1710 | struct d3ddeviceVTBL { 1711 | void *QueryInterface; // void *fns[43]; // alternative representation 1712 | void *AddRef; 1713 | void *Release; 1714 | void *CreateBuffer; 1715 | void *CreateTexture1D; 1716 | void *CreateTexture2D; 1717 | void *CreateTexture3D; 1718 | void *CreateShaderResourceView; 1719 | void *CreateUnorderedAccessView; 1720 | void *CreateRenderTargetView; 1721 | void *CreateDepthStencilView; 1722 | void *CreateInputLayout; 1723 | void *CreateVertexShader; 1724 | void *CreateGeometryShader; 1725 | void *CreateGeometryShaderWithStreamOutput; 1726 | void *CreatePixelShader; 1727 | void *CreateHullShader; 1728 | void *CreateDomainShader; 1729 | void *CreateComputeShader; 1730 | void *CreateClassLinkage; 1731 | void *CreateBlendState; 1732 | void *CreateDepthStencilState; 1733 | void *CreateRasterizerState; 1734 | void *CreateSamplerState; 1735 | void *CreateQuery; 1736 | void *CreatePredicate; 1737 | void *CreateCounter; 1738 | void *CreateDeferredContext; 1739 | void *OpenSharedResource; 1740 | void *CheckFormatSupport; 1741 | void *CheckMultisampleQualityLevels; 1742 | void *CheckCounterInfo; 1743 | void *CheckCounter; 1744 | void *CheckFeatureSupport; 1745 | void *GetPrivateData; 1746 | void *SetPrivateData; 1747 | void *SetPrivateDataInterface; 1748 | void *GetFeatureLevel; 1749 | void *GetCreationFlags; 1750 | void *GetDeviceRemovedReason; 1751 | void *GetImmediateContext; 1752 | void *SetExceptionMode; 1753 | void *GetExceptionMode; 1754 | }; 1755 | struct d3ddevice { 1756 | struct d3ddeviceVTBL** lpVtbl; 1757 | }; 1758 | 1759 | struct d3ddevicecontextVTBL { 1760 | void *QueryInterface; 1761 | void *Addref; 1762 | void *Release; 1763 | void *GetDevice; 1764 | void *GetPrivateData; 1765 | void *SetPrivateData; 1766 | void *SetPrivateDataInterface; 1767 | void *VSSetConstantBuffers; 1768 | void *PSSetShaderResources; 1769 | void *PSSetShader; 1770 | void *SetSamplers; 1771 | void *SetShader; 1772 | void *DrawIndexed; 1773 | void *Draw; 1774 | void *Map; 1775 | void *Unmap; 1776 | void *PSSetConstantBuffer; 1777 | void *IASetInputLayout; 1778 | void *IASetVertexBuffers; 1779 | void *IASetIndexBuffer; 1780 | void *DrawIndexedInstanced; 1781 | void *DrawInstanced; 1782 | void *GSSetConstantBuffers; 1783 | void *GSSetShader; 1784 | void *IASetPrimitiveTopology; 1785 | void *VSSetShaderResources; 1786 | void *VSSetSamplers; 1787 | void *Begin; 1788 | void *End; 1789 | void *GetData; 1790 | void *GSSetPredication; 1791 | void *GSSetShaderResources; 1792 | void *GSSetSamplers; 1793 | void *OMSetRenderTargets; 1794 | void *OMSetRenderTargetsAndUnorderedAccessViews; 1795 | void *OMSetBlendState; 1796 | void *OMSetDepthStencilState; 1797 | void *SOSetTargets; 1798 | void *DrawAuto; 1799 | void *DrawIndexedInstancedIndirect; 1800 | void *DrawInstancedIndirect; 1801 | void *Dispatch; 1802 | void *DispatchIndirect; 1803 | void *RSSetState; 1804 | void *RSSetViewports; 1805 | void *RSSetScissorRects; 1806 | void *CopySubresourceRegion; 1807 | void *CopyResource; 1808 | void *UpdateSubresource; 1809 | void *CopyStructureCount; 1810 | void *ClearRenderTargetView; 1811 | void *ClearUnorderedAccessViewUint; 1812 | void *ClearUnorderedAccessViewFloat; 1813 | void *ClearDepthStencilView; 1814 | void *GenerateMips; 1815 | void *SetResourceMinLOD; 1816 | void *GetResourceMinLOD; 1817 | void *ResolveSubresource; 1818 | void *ExecuteCommandList; 1819 | void *HSSetShaderResources; 1820 | void *HSSetShader; 1821 | void *HSSetSamplers; 1822 | void *HSSetConstantBuffers; 1823 | void *DSSetShaderResources; 1824 | void *DSSetShader; 1825 | void *DSSetSamplers; 1826 | void *DSSetConstantBuffers; 1827 | void *DSSetShaderResources; 1828 | void *CSSetUnorderedAccessViews; 1829 | void *CSSetShader; 1830 | void *CSSetSamplers; 1831 | void *CSSetConstantBuffers; 1832 | void *VSGetConstantBuffers; 1833 | void *PSGetShaderResources; 1834 | void *PSGetShader; 1835 | void *PSGetSamplers; 1836 | void *VSGetShader; 1837 | void *PSGetConstantBuffers; 1838 | void *IAGetInputLayout; 1839 | void *IAGetVertexBuffers; 1840 | void *IAGetIndexBuffer; 1841 | void *GSGetConstantBuffers; 1842 | void *GSGetShader; 1843 | void *IAGetPrimitiveTopology; 1844 | void *VSGetShaderResources; 1845 | void *VSGetSamplers; 1846 | void *GetPredication; 1847 | void *GSGetShaderResources; 1848 | void *GSGetSamplers; 1849 | void *OMGetRenderTargets; 1850 | void *OMGetRenderTargetsAndUnorderedAccessViews; 1851 | void *OMGetBlendState; 1852 | void *OMGetDepthStencilState; 1853 | void *SOGetTargets; 1854 | void *RSGetState; 1855 | void *RSGetViewports; 1856 | void *RSGetScissorRects; 1857 | void *HSGetShaderResources; 1858 | void *HSGetShader; 1859 | void *HSGetSamplers; 1860 | void *HSGetConstantBuffers; 1861 | void *DSGetShaderResources; 1862 | void *DSGetShader; 1863 | void *DSGetSamplers; 1864 | void *DSGetConstantBuffers; 1865 | void *CSGetShaderResources; 1866 | void *CSGetUnorderedAccessViews; 1867 | void *CSGetShader; 1868 | void *CSGetSamplers; 1869 | void *CSGetConstantBuffers; 1870 | void *ClearState; 1871 | void *Flush; 1872 | void *GetType; 1873 | void *GetContextFlags; 1874 | void *FinishCommandList; 1875 | }; 1876 | 1877 | struct d3ddevicecontext { 1878 | struct d3ddevicecontextVTBL** lpVtbl; 1879 | }; 1880 | 1881 | struct d3d11tex2dVTBL { 1882 | void *QueryInterface; 1883 | void *Addref; 1884 | void *Release; 1885 | void *GetDevice; 1886 | void *GetPrivateData; 1887 | void *SetPrivateData; 1888 | void *SetPrivateDataInterface; 1889 | void *GetType; 1890 | void *SetEvictionPriority; 1891 | void *GetEvictionPriority; 1892 | void *GetDesc; 1893 | }; 1894 | 1895 | struct d3d11tex2d { 1896 | struct d3d11tex2dVTBL** lpVtbl; 1897 | }; 1898 | 1899 | typedef unsigned int UINT; 1900 | 1901 | typedef struct DXGI_SAMPLE_DESC 1902 | { 1903 | int Count; 1904 | int Quality; 1905 | } DXGI_SAMPLE_DESC; 1906 | 1907 | typedef enum D3D11_USAGE { 1908 | D3D11_USAGE_DEFAULT = 0, 1909 | D3D11_USAGE_IMMUTABLE = 1, 1910 | D3D11_USAGE_DYNAMIC = 2, 1911 | D3D11_USAGE_STAGING = 3 1912 | } D3D11_USAGE ; 1913 | 1914 | typedef enum D3D11_MAP { 1915 | D3D11_MAP_READ = 1, 1916 | D3D11_MAP_WRITE = 2, 1917 | D3D11_MAP_READ_WRITE = 3, 1918 | D3D11_MAP_WRITE_DISCARD = 4, 1919 | D3D11_MAP_WRITE_NO_OVERWRITE = 5 1920 | } D3D11_MAP; 1921 | 1922 | typedef struct D3D11_MAPPED_SUBRESOURCE { 1923 | void *pData; 1924 | UINT RowPitch, DepthPitch; 1925 | } D3D11_MAPPED_SUBRESOURCE; 1926 | 1927 | typedef enum D3D11_CPU_ACCESS_FLAG { 1928 | D3D11_CPU_ACCESS_WRITE = 0x10000, 1929 | D3D11_CPU_ACCESS_READ = 0x20000 1930 | } D3D11_CPU_ACCESS_FLAG; 1931 | 1932 | typedef struct D3D11_TEXTURE2D_DESC { 1933 | UINT Width; 1934 | UINT Height; 1935 | UINT MipLevels; 1936 | UINT ArraySize; 1937 | UINT Format; //DXGI_FORMAT Format; // big enum list, but not needed... 1938 | DXGI_SAMPLE_DESC SampleDesc; 1939 | D3D11_USAGE Usage; 1940 | UINT BindFlags; 1941 | UINT CPUAccessFlags; 1942 | UINT MiscFlags; 1943 | } D3D11_TEXTURE2D_DESC; 1944 | 1945 | typedef struct gs_texture gs_texture_t; 1946 | void *gs_texture_get_obj(gs_texture_t *tex); 1947 | typedef struct gs_texture_render gs_texrender_t; 1948 | gs_texrender_t *gs_texrender_create(int format, int zsformat); 1949 | void gs_texrender_destroy(gs_texrender_t *texrender); 1950 | void gs_texrender_reset(gs_texrender_t *texrender); 1951 | gs_texture_t *gs_texrender_get_texture(const gs_texrender_t *texrender); 1952 | bool gs_texrender_begin(gs_texrender_t *texrender, uint32_t cx, uint32_t cy); 1953 | void gs_texrender_end(gs_texrender_t *texrender); 1954 | ]] 1955 | end 1956 | 1957 | function SourceDef:_create_dx_ctx() 1958 | if not (ffi.os == 'Windows') then return end -- emulation support?? 1959 | if self.created_dx_ctx then return end 1960 | obs_enter_graphics() 1961 | self.texture_a = obsffi.gs_texrender_create(GS_RGBA, GS_ZS_NONE) -- random texture 1962 | self.texture_clear_color = vec4() 1963 | self.clear_flags = bit.bor(GS_CLEAR_COLOR) 1964 | local effect_solid = obs_get_base_effect(OBS_EFFECT_SOLID) 1965 | MAX_SIDE_W = 512 1966 | MAX_SIDE_H = 288 1967 | MAX_LEN = MAX_SIDE_W * MAX_SIDE_H * 4 1968 | if obsffi.gs_texrender_begin(self.texture_a, MAX_SIDE_W, MAX_SIDE_H) then 1969 | while gs_effect_loop(effect_solid, "Random") do 1970 | gs_draw_sprite(nil, 0, MAX_SIDE_W, MAX_SIDE_H) 1971 | end 1972 | obsffi.gs_texrender_end(self.texture_a) 1973 | end 1974 | self.raw_image = ffi.new("uint8_t[?]", MAX_LEN) 1975 | -- usefull globals 1976 | c_void_p = ffi.typeof("void*") 1977 | c_void_pp = ffi.typeof("void**") 1978 | c_u8_p = ffi.typeof("uint8_t*") 1979 | c_tex2d_new = ffi.typeof("struct d3d11tex2d[1]") 1980 | c_tex2d_p = ffi.typeof("struct d3d11tex2d*") 1981 | c_tex2d_desk_new = ffi.typeof("D3D11_TEXTURE2D_DESC[1]") 1982 | c_tex2d_desk_p = ffi.typeof("D3D11_TEXTURE2D_DESC*") 1983 | c_tex2d_mapped_p = ffi.typeof("D3D11_MAPPED_SUBRESOURCE*") 1984 | c_tex2d_mapped_new = ffi.typeof("D3D11_MAPPED_SUBRESOURCE[1]") 1985 | 1986 | self._tex = obsffi.gs_texture_get_obj(obsffi.gs_texrender_get_texture(self.texture_a)) 1987 | self.p_tex = c_tex2d_p(self._tex) 1988 | self.GetDesc = ffi.cast("void (__stdcall*)(void*, void**)", self.p_tex.lpVtbl[10]) 1989 | self._desk = c_tex2d_desk_new{} 1990 | self.p_desk = ffi.cast(c_void_pp, self._desk) 1991 | self.GetDesc(self.p_tex, self.p_desk) 1992 | 1993 | self.desc_stage = c_tex2d_desk_p(self._desk) 1994 | self.desc_stage.Usage = C.D3D11_USAGE_STAGING 1995 | self.desc_stage.CPUAccessFlags = C.D3D11_CPU_ACCESS_READ 1996 | self.desc_stage.BindFlags = 0 1997 | self.desc_stage.Width = MAX_SIDE_W 1998 | self.desc_stage.Height = MAX_SIDE_H 1999 | self.desc_stage.MiscFlags = 0 2000 | 2001 | self._device = obsffi.gs_get_device_obj() 2002 | self.pDevice = ffi.cast("struct d3ddevice*", self._device) 2003 | self.GetImmediateContext = ffi.cast("long (__stdcall*)(void*, void**)", self.pDevice.lpVtbl[40]) 2004 | 2005 | self._arg1 = ffi.new("unsigned long[1]", {}) 2006 | self._pContext = ffi.cast(c_void_pp, self._arg1) 2007 | self.GetImmediateContext(self.pDevice, self._pContext) 2008 | self.pContext = ffi.cast("struct d3ddevicecontext*", self._pContext[0]) 2009 | 2010 | self.Release_pContext = ffi.cast("unsigned long (__stdcall*)(void*)", self.pContext.lpVtbl[2]) 2011 | self.CopyResource = ffi.cast("void (__stdcall*)(void*, void*, void* )", self.pContext.lpVtbl[47]) 2012 | self.Map = ffi.cast("long (__stdcall*)(void*, void*, UINT, UINT, UINT, void *)", self.pContext.lpVtbl[14]) 2013 | self.Unmap = ffi.cast("long (__stdcall*)(void*, void*, UINT)", self.pContext.lpVtbl[15]) 2014 | self.CreateTexture2D = ffi.cast("long (__stdcall*)(void*, void*, void*, void**)", self.pDevice.lpVtbl[5]) 2015 | 2016 | -- create texture 2017 | self._my_tex = c_tex2d_new{} 2018 | self.p_stage_tex = ffi.cast(c_void_pp, self._my_tex) 2019 | self.CreateTexture2D(self.pDevice, self.desc_stage, nil, self.p_stage_tex) 2020 | -- create staging surface, care: p_stage_tex[0] 2021 | self.stage = c_tex2d_p(self.p_stage_tex[0]) 2022 | obs_leave_graphics() 2023 | self.created_dx_ctx = true 2024 | end 2025 | 2026 | 2027 | --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 2028 | as_custom_source = SourceDef:new({ 2029 | id = "s_console_source", 2030 | type = OBS_SOURCE_TYPE_SOURCE, 2031 | output_flags = bit.bor(OBS_SOURCE_VIDEO, OBS_SOURCE_CUSTOM_DRAW, OBS_SOURCE_AUDIO), 2032 | }) 2033 | function as_custom_source:get_name() return "AGPLv3+ libre-macros by upgradeQ" end 2034 | function as_custom_source:video_render(settings) end 2035 | function as_custom_source:get_height() return 200 end 2036 | function as_custom_source:get_width() return 200 end 2037 | function as_custom_source:load(settings) as_custom_source._reg_htk(self, settings) end 2038 | 2039 | function as_custom_source:update(settings) 2040 | self.code = obs_data_get_string(settings, "_text") 2041 | self.autorun = obs_data_get_bool(settings, "_autorun") 2042 | self.mv1 = obs_data_get_double(settings, "_mv1") 2043 | self.mv2 = obs_data_get_double(settings, "_mv2") 2044 | self.hotreload = obs_data_get_string(settings, "_hotreload") 2045 | self.p1 = obs_data_get_string(settings, "_p1") 2046 | self.p2 = obs_data_get_string(settings, "_p2") 2047 | self.external_dispatch = obs_data_get_bool(settings, "_external_dispatch") 2048 | if self.external_dispatch then 2049 | executor(self) 2050 | end 2051 | -- custom source logic for registering hotkeys 2052 | if not self.created_hotkeys then 2053 | as_custom_source._reg_htk(self, settings) 2054 | end 2055 | end 2056 | 2057 | function as_custom_source:_reg_htk(settings) 2058 | -- note it's not a filter but rather a source itself 2059 | local source_name = obs_source_get_name(self.filter) 2060 | if source_name then 2061 | self.hotkeys["2;" .. source_name] = function() 2062 | self.hotkey_dispatch = true 2063 | end 2064 | self.hotkeys["3;" .. source_name] = function(pressed) 2065 | self.pressed = pressed 2066 | end 2067 | 2068 | for k, v in pairs(self.hotkeys) do 2069 | self.hk[k] = OBS_INVALID_HOTKEY_ID 2070 | end 2071 | 2072 | for k, v in pairs(self.hotkeys) do 2073 | if k:sub(1, 1) == "3" then -- starts with 3 symbol 2074 | self.hk[k] = obs_hotkey_register_frontend(k, k, function(pressed) 2075 | if pressed then 2076 | self.hotkeys[k](true) 2077 | else 2078 | self.hotkeys[k](false) 2079 | end 2080 | end) 2081 | else 2082 | self.hk[k] = obs_hotkey_register_frontend(k, k, function(pressed) 2083 | if pressed then 2084 | self.hotkeys[k]() 2085 | end 2086 | end) 2087 | end 2088 | local a = obs_data_get_array(settings, k) 2089 | obs_hotkey_load(self.hk[k], a) 2090 | obs_data_array_release(a) 2091 | end 2092 | if not self.created_hotkeys then 2093 | self.created_hotkeys = true 2094 | end 2095 | end 2096 | end 2097 | 2098 | as_gap_source = {} 2099 | function as_gap_source:create(source) 2100 | local instance = {} 2101 | as_gap_source.update(instance, self) -- self = settings and this shows it on screen 2102 | return instance 2103 | end 2104 | function as_gap_source:get_name() return i18n"Gap source" end 2105 | function as_gap_source:update(settings) 2106 | self.height = obs_data_get_double(settings, "_height") 2107 | self.width = obs_data_get_double(settings, "_width") 2108 | end 2109 | function as_gap_source:get_properties() 2110 | local props = obs_properties_create() 2111 | obs_properties_add_int_slider(props, "_width", i18n"width", 1, 9999, 1) 2112 | obs_properties_add_int_slider(props, "_height", i18n"height", 1, 9999, 1) 2113 | return props 2114 | end 2115 | function as_gap_source:load(settings) 2116 | self.height = obs_data_get_double(settings, "_height") 2117 | self.width = obs_data_get_double(settings, "_width") 2118 | end 2119 | function as_gap_source:get_height() return self.height end 2120 | function as_gap_source:get_width() return self.width end 2121 | as_gap_source.id = "_gap_source" 2122 | as_gap_source.type = OBS_SOURCE_TYPE_SOURCE 2123 | as_gap_source.output_flags = bit.bor(OBS_SOURCE_VIDEO, OBS_SOURCE_CUSTOM_DRAW) 2124 | 2125 | --bk_obs_script_definition~~~~~~~~~~~~~~~wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww 2126 | function script_description() return [[ 2127 |

Scripting and macros hotkeys overhaul for OBS Studio

2128 | Visit the repository README.md
2130 | Copyright © 2021-2025 upgradeQ
2131 | Distributed under AGPL license 2132 | ]] 2133 | end 2134 | 2135 | function script_properties() 2136 | local props = obs_properties_create() 2137 | local props_group1 = obs_properties_create() 2138 | obs_properties_add_bool(props_group1, "_flag_custom", i18n"Console sceneitem custom") 2139 | obs_properties_add_bool(props_group1, "_flag_gap", i18n"Gap source") 2140 | obs_properties_add_bool(props_group1, "_flag_console_timer", i18n"Console (Timer)") 2141 | local p_suffix = obs_properties_add_float(props_group1, "_interval", "", 0, 999, 0.001) 2142 | obs_property_float_set_suffix(p_suffix," " .. i18n"interval") 2143 | obs_properties_add_text(props_group1, "_patches", "", OBS_TEXT_MULTILINE) 2144 | obs_properties_add_group(props, "group1", i18n"g2_restart", OBS_GROUP_NORMAL, props_group1) 2145 | local _langs = obs_properties_add_list(props, "_lang", i18n"select_lang", OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING) 2146 | 2147 | for k, v in pairs {en="English", ru="Русский"} do 2148 | obs_property_list_add_string(_langs, v, k) 2149 | end 2150 | 2151 | return props 2152 | end 2153 | 2154 | function script_defaults(settings) 2155 | obs_data_set_default_string(settings, "_lang", "en") 2156 | obs_data_set_default_string(settings, "_patches", "-- here goes your GLOBAL code\n") 2157 | obs_data_set_default_double(settings, "_interval", 60) 2158 | end 2159 | 2160 | function script_load(settings) 2161 | 2162 | local run = load(obs_data_get_string(settings, "_patches"), "global patches", "t") 2163 | local ok, _ = pcall(run) 2164 | if not ok then 2165 | print('[+]' .. i18n"s_patch_err") 2166 | end 2167 | local not_allowed_to_run_in_console_instance = function() error('[ERROR] must be in GLOBAL') end 2168 | _G.patch_bs_js = not_allowed_to_run_in_console_instance 2169 | 2170 | i18n.set_locale(obs_data_get_string(settings, "_lang")) -- must load first 2171 | 2172 | local as_video_filter = SourceDef:new({id = "v_console_source", type = OBS_SOURCE_TYPE_FILTER, output_flags = bit.bor(OBS_SOURCE_VIDEO),}) 2173 | obs_register_source(as_video_filter) 2174 | 2175 | local as_audio_filter = SourceDef:new({ id = "a_console_source", type = OBS_SOURCE_TYPE_FILTER, output_flags = bit.bor(OBS_SOURCE_AUDIO),}) 2176 | obs_register_source(as_audio_filter) 2177 | 2178 | 2179 | if obs_data_get_bool(settings, "_flag_gap") then 2180 | obs_register_source(as_gap_source) 2181 | end 2182 | if obs_data_get_bool(settings, "_flag_custom") then 2183 | obs_register_source(as_custom_source) 2184 | end 2185 | if obs_data_get_bool(settings, "_flag_console_timer") then 2186 | local interval = obs_data_get_double(settings, "_interval") 2187 | local as_audio_filter_timer, as_video_filter_timer = as_audio_filter, as_video_filter 2188 | as_video_filter_timer.id = "v_console_source_timer" 2189 | SourceDef.__interval = interval 2190 | function as_video_filter_timer:get_name() return i18n"Console (Timer)" end 2191 | 2192 | as_audio_filter_timer.id = "a_console_source_timer" 2193 | SourceDef.__interval = interval 2194 | function as_audio_filter_timer:get_name() return i18n"Console (Timer)" end 2195 | obs_register_source(as_video_filter_timer) 2196 | obs_register_source(as_audio_filter_timer) 2197 | end 2198 | end 2199 | 2200 | -- vim: ft=lua ts=2 sw=2 et sts=2 2201 | -------------------------------------------------------------------------------- /raw_websockets_interaction.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import json 4 | 5 | # for version websocket 5.0.0; docs https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md 6 | # note https://obsproject.com/forum/threads/python-script-to-connect-to-obs-websocket-server-help.173395/ 7 | # logging.basicConfig(level=logging.DEBUG) 8 | import websocket # https://pypi.org/project/websocket-client/ 9 | 10 | # LOG = logging.getLogger(__name__) 11 | 12 | host = "localhost" 13 | port = 4455 14 | password = "c7CpTD1EqyZBoBDM" # change password here 15 | ws = websocket.WebSocket() 16 | url = "ws://{}:{}".format(host, port) 17 | ws.connect(url) 18 | 19 | 20 | def _build_auth_string(salt, challenge): 21 | secret = base64.b64encode( 22 | hashlib.sha256((password + salt).encode("utf-8")).digest() 23 | ) 24 | auth = base64.b64encode( 25 | hashlib.sha256(secret + challenge.encode("utf-8")).digest() 26 | ).decode("utf-8") 27 | return auth 28 | 29 | 30 | def _auth(): 31 | message = ws.recv() 32 | result = json.loads(message) 33 | server_version = result["d"].get("obsWebSocketVersion") 34 | auth = _build_auth_string( 35 | result["d"]["authentication"]["salt"], 36 | result["d"]["authentication"]["challenge"], 37 | ) 38 | payload = { 39 | "op": 1, 40 | "d": {"rpcVersion": 1, "authentication": auth, "eventSubscriptions": 1000}, 41 | } 42 | ws.send(json.dumps(payload)) 43 | message = ws.recv() 44 | result = json.loads(message) 45 | 46 | 47 | _auth() 48 | 49 | payload_text_value = """ 50 | send_js "document.documentElement.style.filter='grayscale(100%)'" 51 | sleep(1.3) 52 | send_js [[document.documentElement.style.filter='invert(100%)']] 53 | sleep(0.8) 54 | send_js [==[ 55 | document.body.innerHTML = ` 56 | 57 | `; 58 | let r = Math.random; 59 | const c = document.getElementById('cvs').getContext('2d'); 60 | c.beginPath();c.moveTo(50, 0);c.lineTo(0, 100);c.lineTo(100, 100); 61 | c.fillStyle=`rgb(${r()*256|0},${r()*256|0},${r()*256|0})`;c.fill(); 62 | ]==] 63 | """ 64 | 65 | #payload_text_value = "print(0)mr = math.random; print(mr()) sleep(0.5) print(mr()) sleep(1) print(mr())print(1)" 66 | payload = { # sending with new code to run 67 | "op": 6, 68 | "d": { 69 | "requestId": "Set_Console_Settings", 70 | "requestType": "SetSourceFilterSettings", 71 | "requestData": { 72 | "sourceName": "Browser", 73 | "filterName": "Console", 74 | "filterSettings": { 75 | "_text": payload_text_value, 76 | "_external_dispatch": True, 77 | }, 78 | }, 79 | }, 80 | } 81 | ws.send(json.dumps(payload)) 82 | message = ws.recv() 83 | print(message) 84 | 85 | ##### or you can just run existing code 86 | # payload = { 87 | # "op": 6, 88 | # "d": { 89 | # "requestId": "Set_Console_Settings", 90 | # "requestType": "SetSourceFilterSettings", 91 | # "requestData": { 92 | # "sourceName": "Browser", 93 | # "filterName": "Console", 94 | # "filterSettings": { 95 | # "_external_dispatch": True, 96 | # }, 97 | # }, 98 | # }, 99 | # } 100 | # ws.send(json.dumps(payload)) # sending with new code to run 101 | # message = ws.recv() 102 | # print(message) 103 | 104 | ws.close() 105 | -------------------------------------------------------------------------------- /test_file_load.lua: -------------------------------------------------------------------------------- 1 | print("loading from file"); print_source_name(source);print("loaded from file") 2 | --------------------------------------------------------------------------------