├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------