1 Commits

Author SHA1 Message Date
rustdesk
ad0a24ba23 source code 2021-03-27 09:41:55 +08:00
176 changed files with 35192 additions and 2 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/target
.idea
.DS_Store
src/ui/inline.rs
extractor
__pycache__
src/version.rs
*dmg

4296
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

109
Cargo.toml Normal file
View File

@@ -0,0 +1,109 @@
[package]
name = "rustdesk"
version = "1.1.2"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2018"
build= "build.rs"
description = "A remote control software."
[lib]
crate-type = ["cdylib", "staticlib", "rlib"]
[features]
inline = []
cli = []
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
whoami = "0.9"
scrap = { path = "libs/scrap" }
hbb_common = { path = "libs/hbb_common" }
enigo = { path = "libs/enigo" }
serde_derive = "1.0"
serde = "1.0"
serde_json = "1.0"
cfg-if = "1.0"
lazy_static = "1.4"
sha2 = "0.9"
repng = "0.2"
libc = "0.2"
parity-tokio-ipc = { path = "libs/parity-tokio-ipc" }
flexi_logger = "0.16"
runas = "0.2"
magnum-opus = { path = "libs/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"] }
async-trait = "0.1"
crc32fast = "1.2"
uuid = { version = "0.8", features = ["v4"] }
copypasta = "0.7"
clap = "2.33"
rpassword = "5.0"
[target.'cfg(not(any(target_os = "android")))'.dependencies]
cpal = { git = "https://github.com/rustaudio/cpal" }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
machine-uid = "0.2"
mac_address = "1.1"
sciter-rs = { git = "https://github.com/sciter-sdk/rust-sciter" }
[target.'cfg(target_os = "windows")'.dependencies]
systray = { path = "libs/systray-rs" }
winapi = { version = "0.3", features = ["winuser"] }
winreg = "0.7"
windows-service = { git = 'https://github.com/mullvad/windows-service-rs.git' }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"
cocoa = "0.24"
dispatch = "0.2"
core-foundation = "0.9"
core-graphics = "0.22"
[target.'cfg(target_os = "linux")'.dependencies]
libpulse-simple-binding = "2.16"
libpulse-binding = "2.16"
rust-pulsectl = { path = "libs/pulsectl" }
ctrlc = "3.1"
[target.'cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))'.dependencies]
psutil = "3.2"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.9"
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo"]
[package.metadata.winres]
LegalCopyright = "Copyright © 2020"
# this FileDescription overrides package.description
FileDescription = "RustDesk"
[target.'cfg(target_os="windows")'.build-dependencies]
winres = "0.1"
winapi = { version = "0.3", features = [ "winnt" ] }
[build-dependencies]
cc = "1.0"
hbb_common = { path = "libs/hbb_common" }
[dev-dependencies]
hound = "3.4"
[package.metadata.bundle]
name = "RustDesk"
identifier = "com.carriez.rustdesk"
icon = ["32x32.png", "128x128.png", "128x128@2x.png"]
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio"]
osx_minimum_system_version = "10.14"
#https://github.com/johnthagen/min-sized-rust
#!!! rembember call "strip target/release/rustdesk"
# which reduce binary size a lot
[profile.release]
#lto = true
#codegen-units = 1
#panic = 'abort'
#opt-level = 'z' # only have smaller size after strip

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,5 +1,35 @@
### RustDesk | Your Remote Desktop Software ### RustDesk | Your Remote Desktop Software
This is a repository used to release RustDesk software and track issues. The best open source remote desktop software written with Rust.
[**DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## Dependence
Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself.
[Windows](https://github.com/c-smile/sciter-sdk/blob/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.win/x64/sciter.dll)
[Linux](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.lnx/x64/libsciter-gtk.so)
[Osx](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.osx/sciter-osx-64.dylib)
## How To Build
* Prepare your Rust development env and C++ build env
* Install [vcpkg](https://github.com/microsoft/vcpkg), and set VCPKG_ROOT env variable correctly
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
- Linux/Osx: vcpkg install libvpx libyuv opus
* cargo run
## File Structure
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code

74
build.rs Normal file
View File

@@ -0,0 +1,74 @@
#[cfg(windows)]
fn build_windows() {
cc::Build::new().file("windows.cc").compile("windows");
// println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=windows.cc");
}
#[cfg(all(windows, feature = "inline"))]
fn build_manifest() {
use std::io::Write;
if std::env::var("PROFILE").unwrap() == "release" {
let mut res = winres::WindowsResource::new();
res.set_icon("icon.ico")
.set_language(winapi::um::winnt::MAKELANGID(
winapi::um::winnt::LANG_ENGLISH,
winapi::um::winnt::SUBLANG_ENGLISH_US,
))
.set_manifest_file("manifest.xml");
match res.compile() {
Err(e) => {
write!(std::io::stderr(), "{}", e).unwrap();
std::process::exit(1);
}
Ok(_) => {}
}
}
}
fn install_oboe() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os != "android" {
return;
}
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "x86_64" {
target_arch = "x64".to_owned();
} else if target_arch == "aarch64" {
target_arch = "arm64".to_owned();
} else {
target_arch = "arm".to_owned();
}
let target = format!("{}-android-static", target_arch);
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: std::path::PathBuf = vcpkg_root.into();
path.push("installed");
path.push(target);
println!(
"{}",
format!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
)
);
println!("cargo:rustc-link-lib=oboe");
println!("cargo:rustc-link-lib=c++");
println!("cargo:rustc-link-lib=OpenSLES");
// I always got some strange link error with oboe, so as workaround, put oboe.cc into oboe src: src/common/AudioStreamBuilder.cpp
// also to avoid libc++_shared not found issue, cp ndk's libc++_shared.so to jniLibs, e.g.
// ./flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/libc++_shared.so
// let include = path.join("include");
//cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper");
}
fn main() {
#[cfg(all(windows, feature = "inline"))]
build_manifest();
#[cfg(windows)]
build_windows();
#[cfg(target_os = "macos")]
println!("cargo:rustc-link-lib=framework=ApplicationServices");
hbb_common::gen_version();
install_oboe();
}

View File

@@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug, needs investigation
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps or a minimal code example to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. Linux, Windows, macOS ..]
- Rust [e.g. rustc --version]
- Library Version [e.g. enigo 0.0.13 or commit hash fa448be ]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement, needs investigation
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,19 @@
---
name: Question
about: Ask your Question here
title: ''
labels: question
assignees: ''
---
**Describe your Question**
A clear and concise description of what you want to know.
**Describe your Goal**
A clear and concise description of what you want to achieve. Consider the [XYProblem](http://xyproblem.info/)
**Environment (please complete the following information):**
- OS: [e.g. Linux, Windows, macOS ..]
- Rust [e.g. rustc --version]
- Library Version [e.g. enigo 0.0.13 or commit hash fa448be ]

14
libs/enigo/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
.DS_Store
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
# RustFmt files
**/*.rs.bk
# intellij
.idea

15
libs/enigo/.travis.yml Normal file
View File

@@ -0,0 +1,15 @@
language: rust
rust:
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
before_install:
- if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get -qq update; fi
- if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get install -y libxdo-dev; fi
os:
- linux
- osx

13
libs/enigo/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"type": "gdb",
"request": "launch",
"target": "./target/debug/examples/keyboard",
"cwd": "${workspaceRoot}"
}
]
}

41
libs/enigo/Cargo.toml Normal file
View File

@@ -0,0 +1,41 @@
[package]
name = "enigo"
version = "0.0.14"
authors = ["Dustin Bensing <dustin.bensing@googlemail.com>"]
edition = "2018"
build = "build.rs"
description = "Enigo lets you control your mouse and keyboard in an abstract way on different operating systems (currently only Linux, macOS, Win Redox and *BSD planned)"
documentation = "https://docs.rs/enigo/"
homepage = "https://github.com/enigo-rs/enigo"
repository = "https://github.com/enigo-rs/enigo"
readme = "README.md"
keywords = ["input", "mouse", "testing", "keyboard", "automation"]
categories = ["development-tools::testing", "api-bindings", "hardware-support"]
license = "MIT"
[badges]
travis-ci = { repository = "enigo-rs/enigo" }
appveyor = { repository = "pythoneer/enigo-85xiy" }
[dependencies]
serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true }
log = "0.4"
[features]
with_serde = ["serde", "serde_derive"]
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "winbase"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.22"
objc = "0.2"
unicode-segmentation = "1.6"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
[build-dependencies]
pkg-config = "0.3"

21
libs/enigo/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 pythoneer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

46
libs/enigo/README.md Normal file
View File

@@ -0,0 +1,46 @@
[![Build Status](https://travis-ci.org/enigo-rs/enigo.svg?branch=master)](https://travis-ci.org/enigo-rs/enigo)
[![Build status](https://ci.appveyor.com/api/projects/status/6cd00pajx4tvvl3e?svg=true)](https://ci.appveyor.com/project/pythoneer/enigo-85xiy)
[![Dependency Status](https://dependencyci.com/github/pythoneer/enigo/badge)](https://dependencyci.com/github/pythoneer/enigo)
[![Docs](https://docs.rs/enigo/badge.svg)](https://docs.rs/enigo)
[![Crates.io](https://img.shields.io/crates/v/enigo.svg)](https://crates.io/crates/enigo)
[![Discord chat](https://img.shields.io/discord/315925376486342657.svg)](https://discord.gg/Eb8CsnN)
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/enigo-rs/Lobby)
# enigo
Cross platform input simulation in Rust!
- [x] Linux (X11) mouse
- [x] Linux (X11) text
- [ ] Linux (Wayland) mouse
- [ ] Linux (Wayland) text
- [x] MacOS mouse
- [x] MacOS text
- [x] Win mouse
- [x] Win text
- [x] Custom Parser
```Rust
let mut enigo = Enigo::new();
enigo.mouse_move_to(500, 200);
enigo.mouse_click(MouseButton::Left);
enigo.key_sequence_parse("{+CTRL}a{-CTRL}{+SHIFT}Hello World{-SHIFT}");
```
for more look at examples
Runtime dependencies
--------------------
Linux users may have to install libxdo-dev. For example, on Ubuntu:
```Bash
apt install libxdo-dev
```
On Arch:
```Bash
pacman -S xdotool
```

121
libs/enigo/appveyor.yml Normal file
View File

@@ -0,0 +1,121 @@
# Appveyor configuration template for Rust using rustup for Rust installation
# https://github.com/starkat99/appveyor-rust
## Operating System (VM environment) ##
# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets.
os: Visual Studio 2015
## Build Matrix ##
# This configuration will setup a build for each channel & target combination (12 windows
# combinations in all).
#
# There are 3 channels: stable, beta, and nightly.
#
# Alternatively, the full version may be specified for the channel to build using that specific
# version (e.g. channel: 1.5.0)
#
# The values for target are the set of windows Rust build targets. Each value is of the form
#
# ARCH-pc-windows-TOOLCHAIN
#
# Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker
# toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for
# a description of the toolchain differences.
# See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of
# toolchains and host triples.
#
# Comment out channel/target combos you do not wish to build in CI.
#
# You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands
# and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly
# channels to enable unstable features when building for nightly. Or you could add additional
# matrix entries to test different combinations of features.
environment:
matrix:
### MSVC Toolchains ###
# Stable 64-bit MSVC
- channel: stable
target: x86_64-pc-windows-msvc
# Stable 32-bit MSVC
- channel: stable
target: i686-pc-windows-msvc
# Beta 64-bit MSVC
- channel: beta
target: x86_64-pc-windows-msvc
# Beta 32-bit MSVC
- channel: beta
target: i686-pc-windows-msvc
# Nightly 64-bit MSVC
- channel: nightly
target: x86_64-pc-windows-msvc
#cargoflags: --features "unstable"
# Nightly 32-bit MSVC
- channel: nightly
target: i686-pc-windows-msvc
#cargoflags: --features "unstable"
### GNU Toolchains ###
# Stable 64-bit GNU
- channel: stable
target: x86_64-pc-windows-gnu
# Stable 32-bit GNU
- channel: stable
target: i686-pc-windows-gnu
# Beta 64-bit GNU
- channel: beta
target: x86_64-pc-windows-gnu
# Beta 32-bit GNU
- channel: beta
target: i686-pc-windows-gnu
# Nightly 64-bit GNU
- channel: nightly
target: x86_64-pc-windows-gnu
#cargoflags: --features "unstable"
# Nightly 32-bit GNU
- channel: nightly
target: i686-pc-windows-gnu
#cargoflags: --features "unstable"
### Allowed failures ###
# See Appveyor documentation for specific details. In short, place any channel or targets you wish
# to allow build failures on (usually nightly at least is a wise choice). This will prevent a build
# or test failure in the matching channels/targets from failing the entire build.
matrix:
allow_failures:
- channel: nightly
# If you only care about stable channel build failures, uncomment the following line:
#- channel: beta
## Install Script ##
# This is the most important part of the Appveyor configuration. This installs the version of Rust
# specified by the 'channel' and 'target' environment variables from the build matrix. This uses
# rustup to install Rust.
#
# For simple configurations, instead of using the build matrix, you can simply set the
# default-toolchain and default-host manually here.
install:
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init -yv --default-toolchain %channel% --default-host %target%
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV
## Build Script ##
# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents
# the "directory does not contain a project or solution file" error.
build: false
# Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs
#directly or perform other testing commands. Rust will automatically be placed in the PATH
# environment variable.
test_script:
- cargo test --verbose %cargoflags%

61
libs/enigo/build.rs Normal file
View File

@@ -0,0 +1,61 @@
#[cfg(target_os = "windows")]
fn main() {}
#[cfg(target_os = "macos")]
fn main() {}
#[cfg(target_os = "linux")]
use pkg_config;
#[cfg(target_os = "linux")]
use std::env;
#[cfg(target_os = "linux")]
use std::fs::File;
#[cfg(target_os = "linux")]
use std::io::Write;
#[cfg(target_os = "linux")]
use std::path::Path;
#[cfg(target_os = "linux")]
fn main() {
let libraries = [
"xext",
"gl",
"xcursor",
"xxf86vm",
"xft",
"xinerama",
"xi",
"x11",
"xlib_xcb",
"xmu",
"xrandr",
"xtst",
"xrender",
"xscrnsaver",
"xt",
];
let mut config = String::new();
for lib in libraries.iter() {
let libdir = match pkg_config::get_variable(lib, "libdir") {
Ok(libdir) => format!("Some(\"{}\")", libdir),
Err(_) => "None".to_string(),
};
config.push_str(&format!(
"pub const {}: Option<&'static str> = {};\n",
lib, libdir
));
}
let config = format!("pub mod config {{ pub mod libdir {{\n{}}}\n}}", config);
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("config.rs");
let mut f = File::create(&dest_path).unwrap();
f.write_all(&config.into_bytes()).unwrap();
let target = env::var("TARGET").unwrap();
if target.contains("linux") {
println!("cargo:rustc-link-lib=dl");
} else if target.contains("freebsd") || target.contains("dragonfly") {
println!("cargo:rustc-link-lib=c");
}
}

View File

@@ -0,0 +1,11 @@
use enigo::{Enigo, KeyboardControllable};
use std::thread;
use std::time::Duration;
fn main() {
thread::sleep(Duration::from_secs(2));
let mut enigo = Enigo::new();
// write text and select all
enigo.key_sequence_parse("{+UNICODE}{{Hello World!}} ❤️{-UNICODE}{+CTRL}a{-CTRL}");
}

View File

@@ -0,0 +1,12 @@
use enigo::{Enigo, Key, KeyboardControllable};
use std::thread;
use std::time::Duration;
fn main() {
thread::sleep(Duration::from_secs(2));
let mut enigo = Enigo::new();
enigo.key_down(Key::Layout('a'));
thread::sleep(Duration::from_secs(1));
enigo.key_up(Key::Layout('a'));
}

View File

@@ -0,0 +1,16 @@
use enigo::{Enigo, Key, KeyboardControllable};
use std::thread;
use std::time::Duration;
fn main() {
thread::sleep(Duration::from_secs(2));
let mut enigo = Enigo::new();
// write text
enigo.key_sequence("Hello World! here is a lot of text ❤️");
// select all
enigo.key_down(Key::Control);
enigo.key_click(Key::Layout('a'));
enigo.key_up(Key::Control);
}

View File

@@ -0,0 +1,37 @@
use enigo::{Enigo, MouseButton, MouseControllable};
use std::thread;
use std::time::Duration;
fn main() {
let wait_time = Duration::from_secs(2);
let mut enigo = Enigo::new();
thread::sleep(wait_time);
enigo.mouse_move_to(500, 200);
thread::sleep(wait_time);
enigo.mouse_down(MouseButton::Left);
thread::sleep(wait_time);
enigo.mouse_move_relative(100, 100);
thread::sleep(wait_time);
enigo.mouse_up(MouseButton::Left);
thread::sleep(wait_time);
enigo.mouse_click(MouseButton::Left);
thread::sleep(wait_time);
enigo.mouse_scroll_x(2);
thread::sleep(wait_time);
enigo.mouse_scroll_x(-2);
thread::sleep(wait_time);
enigo.mouse_scroll_y(2);
thread::sleep(wait_time);
enigo.mouse_scroll_y(-2);
thread::sleep(wait_time);
}

View File

@@ -0,0 +1,22 @@
use enigo::{Enigo, Key, KeyboardControllable};
use std::thread;
use std::time::Duration;
use std::time::Instant;
fn main() {
thread::sleep(Duration::from_secs(2));
let mut enigo = Enigo::new();
let now = Instant::now();
// write text
enigo.key_sequence("Hello World! ❤️");
let time = now.elapsed();
println!("{:?}", time);
// select all
enigo.key_down(Key::Control);
enigo.key_click(Key::Layout('a'));
enigo.key_up(Key::Control);
}

1
libs/enigo/rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
wrap_comments = true

184
libs/enigo/src/dsl.rs Normal file
View File

@@ -0,0 +1,184 @@
use crate::{Key, KeyboardControllable};
use std::error::Error;
use std::fmt;
/// An error that can occur when parsing DSL
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
/// When a tag doesn't exist.
/// Example: {+TEST}{-TEST}
/// ^^^^ ^^^^
UnknownTag(String),
/// When a { is encountered inside a {TAG}.
/// Example: {+HELLO{WORLD}
/// ^
UnexpectedOpen,
/// When a { is never matched with a }.
/// Example: {+SHIFT}Hello{-SHIFT
/// ^
UnmatchedOpen,
/// Opposite of UnmatchedOpen.
/// Example: +SHIFT}Hello{-SHIFT}
/// ^
UnmatchedClose,
}
impl Error for ParseError {
fn description(&self) -> &str {
match *self {
ParseError::UnknownTag(_) => "Unknown tag",
ParseError::UnexpectedOpen => "Unescaped open bracket ({) found inside tag name",
ParseError::UnmatchedOpen => "Unmatched open bracket ({). No matching close (})",
ParseError::UnmatchedClose => "Unmatched close bracket (}). No previous open ({)",
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string())
}
}
/// Evaluate the DSL. This tokenizes the input and presses the keys.
pub fn eval<K>(enigo: &mut K, input: &str) -> Result<(), ParseError>
where
K: KeyboardControllable,
{
for token in tokenize(input)? {
match token {
Token::Sequence(buffer) => {
for key in buffer.chars() {
enigo.key_click(Key::Layout(key));
}
}
Token::Unicode(buffer) => enigo.key_sequence(&buffer),
Token::KeyUp(key) => enigo.key_up(key),
Token::KeyDown(key) => enigo.key_down(key).unwrap_or(()),
}
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
enum Token {
Sequence(String),
Unicode(String),
KeyUp(Key),
KeyDown(Key),
}
fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
let mut unicode = false;
let mut tokens = Vec::new();
let mut buffer = String::new();
let mut iter = input.chars().peekable();
fn flush(tokens: &mut Vec<Token>, buffer: String, unicode: bool) {
if !buffer.is_empty() {
if unicode {
tokens.push(Token::Unicode(buffer));
} else {
tokens.push(Token::Sequence(buffer));
}
}
}
while let Some(c) = iter.next() {
if c == '{' {
match iter.next() {
Some('{') => buffer.push('{'),
Some(mut c) => {
flush(&mut tokens, buffer, unicode);
buffer = String::new();
let mut tag = String::new();
loop {
tag.push(c);
match iter.next() {
Some('{') => match iter.peek() {
Some(&'{') => {
iter.next();
c = '{'
}
_ => return Err(ParseError::UnexpectedOpen),
},
Some('}') => match iter.peek() {
Some(&'}') => {
iter.next();
c = '}'
}
_ => break,
},
Some(new) => c = new,
None => return Err(ParseError::UnmatchedOpen),
}
}
match &*tag {
"+UNICODE" => unicode = true,
"-UNICODE" => unicode = false,
"+SHIFT" => tokens.push(Token::KeyDown(Key::Shift)),
"-SHIFT" => tokens.push(Token::KeyUp(Key::Shift)),
"+CTRL" => tokens.push(Token::KeyDown(Key::Control)),
"-CTRL" => tokens.push(Token::KeyUp(Key::Control)),
"+META" => tokens.push(Token::KeyDown(Key::Meta)),
"-META" => tokens.push(Token::KeyUp(Key::Meta)),
"+ALT" => tokens.push(Token::KeyDown(Key::Alt)),
"-ALT" => tokens.push(Token::KeyUp(Key::Alt)),
_ => return Err(ParseError::UnknownTag(tag)),
}
}
None => return Err(ParseError::UnmatchedOpen),
}
} else if c == '}' {
match iter.next() {
Some('}') => buffer.push('}'),
_ => return Err(ParseError::UnmatchedClose),
}
} else {
buffer.push(c);
}
}
flush(&mut tokens, buffer, unicode);
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn success() {
assert_eq!(
tokenize("{{Hello World!}} {+CTRL}hi{-CTRL}"),
Ok(vec![
Token::Sequence("{Hello World!} ".into()),
Token::KeyDown(Key::Control),
Token::Sequence("hi".into()),
Token::KeyUp(Key::Control)
])
);
}
#[test]
fn unexpected_open() {
assert_eq!(tokenize("{hello{}world}"), Err(ParseError::UnexpectedOpen));
}
#[test]
fn unmatched_open() {
assert_eq!(
tokenize("{this is going to fail"),
Err(ParseError::UnmatchedOpen)
);
}
#[test]
fn unmatched_close() {
assert_eq!(
tokenize("{+CTRL}{{this}} is going to fail}"),
Err(ParseError::UnmatchedClose)
);
}
}

525
libs/enigo/src/lib.rs Normal file
View File

@@ -0,0 +1,525 @@
//! Enigo lets you simulate mouse and keyboard input-events as if they were
//! made by the actual hardware. The goal is to make it available on different
//! operating systems like Linux, macOS and Windows possibly many more but
//! [Redox](https://redox-os.org/) and *BSD are planned. Please see the
//! [Repo](https://github.com/enigo-rs/enigo) for the current status.
//!
//! I consider this library in an early alpha status, the API will change in
//! in the future. The keyboard handling is far from being very usable. I plan
//! to build a simple
//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)
//! that will resemble something like:
//!
//! `"hello {+SHIFT}world{-SHIFT} and break line{ENTER}"`
//!
//! The current status is that you can just print
//! [unicode](http://unicode.org/)
//! characters like [emoji](http://getemoji.com/) without the `{+SHIFT}`
//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)
//! or any other "special" key on the Linux, macOS and Windows operating system.
//!
//! Possible use cases could be for testing user interfaces on different
//! plattforms,
//! building remote control applications or just automating tasks for user
//! interfaces unaccessible by a public API or scripting laguage.
//!
//! For the keyboard there are currently two modes you can use. The first mode
//! is represented by the [key_sequence]() function
//! its purpose is to simply write unicode characters. This is independent of
//! the keyboardlayout. Please note that
//! you're not be able to use modifier keys like Control
//! to influence the outcome. If you want to use modifier keys to e.g.
//! copy/paste
//! use the Layout variant. Please note that this is indeed layout dependent.
//! # Examples
//! ```no_run
//! use enigo::*;
//! let mut enigo = Enigo::new();
//! //paste
//! enigo.key_down(Key::Control);
//! enigo.key_click(Key::Layout('v'));
//! enigo.key_up(Key::Control);
//! ```
//!
//! ```no_run
//! use enigo::*;
//! let mut enigo = Enigo::new();
//! enigo.mouse_move_to(500, 200);
//! enigo.mouse_down(MouseButton::Left);
//! enigo.mouse_move_relative(100, 100);
//! enigo.mouse_up(MouseButton::Left);
//! enigo.key_sequence("hello world");
//! ```
#![deny(missing_docs)]
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
// TODO(dustin) use interior mutability not &mut self
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
pub use win::Enigo;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::Enigo;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use crate::linux::Enigo;
/// DSL parser module
pub mod dsl;
#[cfg(feature = "with_serde")]
#[macro_use]
extern crate serde_derive;
#[cfg(feature = "with_serde")]
extern crate serde;
///
pub type ResultType = std::result::Result<(), Box<dyn std::error::Error>>;
#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
/// MouseButton represents a mouse button,
/// and is used in for example
/// [mouse_click](trait.MouseControllable.html#tymethod.mouse_click).
/// WARNING: Types with the prefix Scroll
/// IS NOT intended to be used, and may not work on
/// all operating systems.
pub enum MouseButton {
/// Left mouse button
Left,
/// Middle mouse button
Middle,
/// Right mouse button
Right,
/// Scroll up button
ScrollUp,
/// Left right button
ScrollDown,
/// Left right button
ScrollLeft,
/// Left right button
ScrollRight,
}
/// Representing an interface and a set of mouse functions every
/// operating system implementation _should_ implement.
pub trait MouseControllable {
/// Lets the mouse cursor move to the specified x and y coordinates.
///
/// The topleft corner of your monitor screen is x=0 y=0. Move
/// the cursor down the screen by increasing the y and to the right
/// by increasing x coordinate.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_move_to(500, 200);
/// ```
fn mouse_move_to(&mut self, x: i32, y: i32);
/// Lets the mouse cursor move the specified amount in the x and y
/// direction.
///
/// The amount specified in the x and y parameters are added to the
/// current location of the mouse cursor. A positive x values lets
/// the mouse cursor move an amount of `x` pixels to the right. A negative
/// value for `x` lets the mouse cursor go to the left. A positive value
/// of y
/// lets the mouse cursor go down, a negative one lets the mouse cursor go
/// up.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_move_relative(100, 100);
/// ```
fn mouse_move_relative(&mut self, x: i32, y: i32);
/// Push down one of the mouse buttons
///
/// Push down the mouse button specified by the parameter `button` of
/// type [MouseButton](enum.MouseButton.html)
/// and holds it until it is released by
/// [mouse_up](trait.MouseControllable.html#tymethod.mouse_up).
/// Calls to [mouse_move_to](trait.MouseControllable.html#tymethod.
/// mouse_move_to) or
/// [mouse_move_relative](trait.MouseControllable.html#tymethod.
/// mouse_move_relative)
/// will work like expected and will e.g. drag widgets or highlight text.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_down(MouseButton::Left);
/// ```
fn mouse_down(&mut self, button: MouseButton) -> ResultType;
/// Lift up a pushed down mouse button
///
/// Lift up a previously pushed down button (by invoking
/// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down)).
/// If the button was not pushed down or consecutive calls without
/// invoking [mouse_down](trait.MouseControllable.html#tymethod.mouse_down)
/// will emit lift up events. It depends on the
/// operating system whats actually happening my guess is it will just
/// get ignored.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_up(MouseButton::Right);
/// ```
fn mouse_up(&mut self, button: MouseButton);
/// Click a mouse button
///
/// it's esentially just a consecutive invokation of
/// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed
/// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just
/// for
/// convenience.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_click(MouseButton::Right);
/// ```
fn mouse_click(&mut self, button: MouseButton);
/// Scroll the mouse (wheel) left or right
///
/// Positive numbers for length lets the mouse wheel scroll to the right
/// and negative ones to the left. The value that is specified translates
/// to `lines` defined by the operating system and is essentially one 15°
/// (click)rotation on the mouse wheel. How many lines it moves depends
/// on the current setting in the operating system.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_scroll_x(2);
/// ```
fn mouse_scroll_x(&mut self, length: i32);
/// Scroll the mouse (wheel) up or down
///
/// Positive numbers for length lets the mouse wheel scroll down
/// and negative ones up. The value that is specified translates
/// to `lines` defined by the operating system and is essentially one 15°
/// (click)rotation on the mouse wheel. How many lines it moves depends
/// on the current setting in the operating system.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.mouse_scroll_y(2);
/// ```
fn mouse_scroll_y(&mut self, length: i32);
}
/// A key on the keyboard.
/// For alphabetical keys, use Key::Layout for a system independent key.
/// If a key is missing, you can use the raw keycode with Key::Raw.
#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Key {
/// alt key on Linux and Windows (option key on macOS)
Alt,
/// backspace key
Backspace,
/// caps lock key
CapsLock,
#[deprecated(since = "0.0.12", note = "now renamed to Meta")]
/// command key on macOS (super key on Linux, windows key on Windows)
Command,
/// control key
Control,
/// delete key
Delete,
/// down arrow key
DownArrow,
/// end key
End,
/// escape key (esc)
Escape,
/// F1 key
F1,
/// F10 key
F10,
/// F11 key
F11,
/// F12 key
F12,
/// F2 key
F2,
/// F3 key
F3,
/// F4 key
F4,
/// F5 key
F5,
/// F6 key
F6,
/// F7 key
F7,
/// F8 key
F8,
/// F9 key
F9,
/// home key
Home,
/// left arrow key
LeftArrow,
/// meta key (also known as "windows", "super", and "command")
Meta,
/// option key on macOS (alt key on Linux and Windows)
Option,
/// page down key
PageDown,
/// page up key
PageUp,
/// return key
Return,
/// right arrow key
RightArrow,
/// shift key
Shift,
/// space key
Space,
#[deprecated(since = "0.0.12", note = "now renamed to Meta")]
/// super key on linux (command key on macOS, windows key on Windows)
Super,
/// tab key (tabulator)
Tab,
/// up arrow key
UpArrow,
#[deprecated(since = "0.0.12", note = "now renamed to Meta")]
/// windows key on Windows (super key on Linux, command key on macOS)
Windows,
///
Numpad0,
///
Numpad1,
///
Numpad2,
///
Numpad3,
///
Numpad4,
///
Numpad5,
///
Numpad6,
///
Numpad7,
///
Numpad8,
///
Numpad9,
///
Cancel,
///
Clear,
///
Menu,
///
Pause,
///
Kana,
///
Hangul,
///
Junja,
///
Final,
///
Hanja,
///
Kanji,
///
Convert,
///
Select,
///
Print,
///
Execute,
///
Snapshot,
///
Insert,
///
Help,
///
Sleep,
///
Separator,
///
VolumeUp,
///
VolumeDown,
///
Mute,
///
Scroll,
/// scroll lock
NumLock,
///
RWin,
///
Apps,
///
Multiply,
///
Add,
///
Subtract,
///
Decimal,
///
Divide,
///
Equals,
///
NumpadEnter,
///
/// Function, /// mac
/// keyboard layout dependent key
Layout(char),
/// raw keycode eg 0x38
Raw(u16),
}
/// Representing an interface and a set of keyboard functions every
/// operating system implementation _should_ implement.
pub trait KeyboardControllable {
/// Types the string parsed with DSL.
///
/// Typing {+SHIFT}hello{-SHIFT} becomes HELLO.
/// TODO: Full documentation
fn key_sequence_parse(&mut self, sequence: &str)
where
Self: Sized,
{
self.key_sequence_parse_try(sequence)
.expect("Could not parse sequence");
}
/// Same as key_sequence_parse except returns any errors
fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError>
where
Self: Sized,
{
dsl::eval(self, sequence)
}
/// Types the string
///
/// Emits keystrokes such that the given string is inputted.
///
/// You can use many unicode here like: ❤️. This works
/// regadless of the current keyboardlayout.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// enigo.key_sequence("hello world ❤️");
/// ```
fn key_sequence(&mut self, sequence: &str);
/// presses a given key down
fn key_down(&mut self, key: Key) -> ResultType;
/// release a given key formally pressed down by
/// [key_down](trait.KeyboardControllable.html#tymethod.key_down)
fn key_up(&mut self, key: Key);
/// Much like the
/// [key_down](trait.KeyboardControllable.html#tymethod.key_down) and
/// [key_up](trait.KeyboardControllable.html#tymethod.key_up)
/// function they're just invoked consecutively
fn key_click(&mut self, key: Key);
///
fn get_key_state(&mut self, key: Key) -> bool;
}
#[cfg(any(target_os = "android", target_os = "ios"))]
struct Enigo;
impl Enigo {
/// Constructs a new `Enigo` instance.
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut enigo = Enigo::new();
/// ```
pub fn new() -> Self {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Enigo{};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Self::default()
}
}
use std::fmt;
impl fmt::Debug for Enigo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Enigo")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_key_state() {
let mut enigo = Enigo::new();
let keys = [Key::CapsLock, Key::NumLock];
for k in keys.iter() {
enigo.key_click(k.clone());
let a = enigo.get_key_state(k.clone());
enigo.key_click(k.clone());
let b = enigo.get_key_state(k.clone());
assert!(a != b);
}
let keys = [Key::Control, Key::Alt, Key::Shift];
for k in keys.iter() {
enigo.key_down(k.clone()).ok();
let a = enigo.get_key_state(k.clone());
enigo.key_up(k.clone());
let b = enigo.get_key_state(k.clone());
assert!(a != b);
}
}
}

361
libs/enigo/src/linux.rs Normal file
View File

@@ -0,0 +1,361 @@
use libc;
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use self::libc::{c_char, c_int, c_void, useconds_t};
use std::{borrow::Cow, ffi::CString, ptr};
const CURRENT_WINDOW: c_int = 0;
const DEFAULT_DELAY: u64 = 12000;
type Window = c_int;
type Xdo = *const c_void;
#[link(name = "xdo")]
extern "C" {
fn xdo_free(xdo: Xdo);
fn xdo_new(display: *const c_char) -> Xdo;
fn xdo_click_window(xdo: Xdo, window: Window, button: c_int) -> c_int;
fn xdo_mouse_down(xdo: Xdo, window: Window, button: c_int) -> c_int;
fn xdo_mouse_up(xdo: Xdo, window: Window, button: c_int) -> c_int;
fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int;
fn xdo_move_mouse_relative(xdo: Xdo, x: c_int, y: c_int) -> c_int;
fn xdo_enter_text_window(
xdo: Xdo,
window: Window,
string: *const c_char,
delay: useconds_t,
) -> c_int;
fn xdo_send_keysequence_window(
xdo: Xdo,
window: Window,
string: *const c_char,
delay: useconds_t,
) -> c_int;
fn xdo_send_keysequence_window_down(
xdo: Xdo,
window: Window,
string: *const c_char,
delay: useconds_t,
) -> c_int;
fn xdo_send_keysequence_window_up(
xdo: Xdo,
window: Window,
string: *const c_char,
delay: useconds_t,
) -> c_int;
fn xdo_get_input_state(xdo: Xdo) -> u32;
}
fn mousebutton(button: MouseButton) -> c_int {
match button {
MouseButton::Left => 1,
MouseButton::Middle => 2,
MouseButton::Right => 3,
MouseButton::ScrollUp => 4,
MouseButton::ScrollDown => 5,
MouseButton::ScrollLeft => 6,
MouseButton::ScrollRight => 7,
}
}
/// The main struct for handling the event emitting
pub struct Enigo {
xdo: Xdo,
delay: u64,
}
// This is safe, we have a unique pointer.
// TODO: use Unique<c_char> once stable.
unsafe impl Send for Enigo {}
impl Default for Enigo {
/// Create a new Enigo instance
fn default() -> Self {
Self {
xdo: unsafe { xdo_new(ptr::null()) },
delay: DEFAULT_DELAY,
}
}
}
impl Enigo {
/// Get the delay per keypress.
/// Default value is 12000.
/// This is Linux-specific.
pub fn delay(&self) -> u64 {
self.delay
}
/// Set the delay per keypress.
/// This is Linux-specific.
pub fn set_delay(&mut self, delay: u64) {
self.delay = delay;
}
}
impl Drop for Enigo {
fn drop(&mut self) {
if self.xdo.is_null() {
return;
}
unsafe {
xdo_free(self.xdo);
}
}
}
impl MouseControllable for Enigo {
fn mouse_move_to(&mut self, x: i32, y: i32) {
if self.xdo.is_null() {
return;
}
unsafe {
xdo_move_mouse(self.xdo, x as c_int, y as c_int, 0);
}
}
fn mouse_move_relative(&mut self, x: i32, y: i32) {
if self.xdo.is_null() {
return;
}
unsafe {
xdo_move_mouse_relative(self.xdo, x as c_int, y as c_int);
}
}
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
if self.xdo.is_null() {
return Ok(());
}
unsafe {
xdo_mouse_down(self.xdo, CURRENT_WINDOW, mousebutton(button));
}
Ok(())
}
fn mouse_up(&mut self, button: MouseButton) {
if self.xdo.is_null() {
return;
}
unsafe {
xdo_mouse_up(self.xdo, CURRENT_WINDOW, mousebutton(button));
}
}
fn mouse_click(&mut self, button: MouseButton) {
if self.xdo.is_null() {
return;
}
unsafe {
xdo_click_window(self.xdo, CURRENT_WINDOW, mousebutton(button));
}
}
fn mouse_scroll_x(&mut self, length: i32) {
let button;
let mut length = length;
if length < 0 {
button = MouseButton::ScrollLeft;
} else {
button = MouseButton::ScrollRight;
}
if length < 0 {
length = -length;
}
for _ in 0..length {
self.mouse_click(button);
}
}
fn mouse_scroll_y(&mut self, length: i32) {
let button;
let mut length = length;
if length < 0 {
button = MouseButton::ScrollUp;
} else {
button = MouseButton::ScrollDown;
}
if length < 0 {
length = -length;
}
for _ in 0..length {
self.mouse_click(button);
}
}
}
fn keysequence<'a>(key: Key) -> Cow<'a, str> {
if let Key::Layout(c) = key {
return Cow::Owned(format!("U{:X}", c as u32));
}
if let Key::Raw(k) = key {
return Cow::Owned(format!("{}", k as u16));
}
#[allow(deprecated)]
// I mean duh, we still need to support deprecated keys until they're removed
// https://www.rubydoc.info/gems/xdo/XDo/Keyboard
// https://gitlab.com/cunidev/gestures/-/wikis/xdotool-list-of-key-codes
Cow::Borrowed(match key {
Key::Alt => "Alt",
Key::Backspace => "BackSpace",
Key::CapsLock => "Caps_Lock",
Key::Control => "Control",
Key::Delete => "Delete",
Key::DownArrow => "Down",
Key::End => "End",
Key::Escape => "Escape",
Key::F1 => "F1",
Key::F10 => "F10",
Key::F11 => "F11",
Key::F12 => "F12",
Key::F2 => "F2",
Key::F3 => "F3",
Key::F4 => "F4",
Key::F5 => "F5",
Key::F6 => "F6",
Key::F7 => "F7",
Key::F8 => "F8",
Key::F9 => "F9",
Key::Home => "Home",
//Key::Layout(_) => unreachable!(),
Key::LeftArrow => "Left",
Key::Option => "Option",
Key::PageDown => "Page_Down",
Key::PageUp => "Page_Up",
//Key::Raw(_) => unreachable!(),
Key::Return => "Return",
Key::RightArrow => "Right",
Key::Shift => "Shift",
Key::Space => "space",
Key::Tab => "Tab",
Key::UpArrow => "Up",
Key::Numpad0 => "U30", //"KP_0",
Key::Numpad1 => "U31", //"KP_1",
Key::Numpad2 => "U32", //"KP_2",
Key::Numpad3 => "U33", //"KP_3",
Key::Numpad4 => "U34", //"KP_4",
Key::Numpad5 => "U35", //"KP_5",
Key::Numpad6 => "U36", //"KP_6",
Key::Numpad7 => "U37", //"KP_7",
Key::Numpad8 => "U38", //"KP_8",
Key::Numpad9 => "U39", //"KP_9",
Key::Decimal => "U2E", //"KP_Decimal",
Key::Cancel => "Cancel",
Key::Clear => "Clear",
Key::Menu => "Menu",
Key::Pause => "Pause",
Key::Kana => "Kana",
Key::Hangul => "Hangul",
Key::Junja => "",
Key::Final => "",
Key::Hanja => "Hanja",
Key::Kanji => "Kanji",
Key::Convert => "",
Key::Select => "Select",
Key::Print => "Print",
Key::Execute => "Execute",
Key::Snapshot => "3270_PrintScreen",
Key::Insert => "Insert",
Key::Help => "Help",
Key::Sleep => "",
Key::Separator => "KP_Separator",
Key::VolumeUp => "",
Key::VolumeDown => "",
Key::Mute => "",
Key::Scroll => "Scroll_Lock",
Key::NumLock => "Num_Lock",
Key::RWin => "",
Key::Apps => "",
Key::Multiply => "KP_Multiply",
Key::Add => "KP_Add",
Key::Subtract => "KP_Subtract",
Key::Divide => "KP_Divide",
Key::Equals => "KP_Equal",
Key::NumpadEnter => "KP_Enter",
Key::Command | Key::Super | Key::Windows | Key::Meta => "Super",
_ => "",
})
}
impl KeyboardControllable for Enigo {
fn get_key_state(&mut self, key: Key) -> bool {
if self.xdo.is_null() {
return false;
}
let mod_shift = 1 << 0;
let mod_lock = 1 << 1;
let mod_control = 1 << 2;
let mod_alt = 1 << 3;
let mod_numlock = 1 << 4;
let mod_meta = 1 << 6;
let mask = unsafe { xdo_get_input_state(self.xdo) };
// println!("{:b}", mask);
match key {
Key::Shift => mask & mod_shift != 0,
Key::CapsLock => mask & mod_lock != 0,
Key::Control => mask & mod_control != 0,
Key::Alt => mask & mod_alt != 0,
Key::NumLock => mask & mod_numlock != 0,
Key::Meta => mask & mod_meta != 0,
_ => false,
}
}
fn key_sequence(&mut self, sequence: &str) {
if self.xdo.is_null() {
return;
}
if let Ok(string) = CString::new(sequence) {
unsafe {
xdo_enter_text_window(
self.xdo,
CURRENT_WINDOW,
string.as_ptr(),
self.delay as useconds_t,
);
}
}
}
fn key_down(&mut self, key: Key) -> crate::ResultType {
if self.xdo.is_null() {
return Ok(());
}
let string = CString::new(&*keysequence(key))?;
unsafe {
xdo_send_keysequence_window_down(
self.xdo,
CURRENT_WINDOW,
string.as_ptr(),
self.delay as useconds_t,
);
}
Ok(())
}
fn key_up(&mut self, key: Key) {
if self.xdo.is_null() {
return;
}
if let Ok(string) = CString::new(&*keysequence(key)) {
unsafe {
xdo_send_keysequence_window_up(
self.xdo,
CURRENT_WINDOW,
string.as_ptr(),
self.delay as useconds_t,
);
}
}
}
fn key_click(&mut self, key: Key) {
if self.xdo.is_null() {
return;
}
if let Ok(string) = CString::new(&*keysequence(key)) {
unsafe {
xdo_send_keysequence_window(
self.xdo,
CURRENT_WINDOW,
string.as_ptr(),
self.delay as useconds_t,
);
}
}
}
}

View File

@@ -0,0 +1,72 @@
// https://stackoverflow.com/questions/3202629/where-can-i-find-a-list-of-mac-virtual-key-codes
/* keycodes for keys that are independent of keyboard layout */
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
pub const kVK_Return: u16 = 0x24;
pub const kVK_Tab: u16 = 0x30;
pub const kVK_Space: u16 = 0x31;
pub const kVK_Delete: u16 = 0x33;
pub const kVK_Escape: u16 = 0x35;
pub const kVK_Command: u16 = 0x37;
pub const kVK_Shift: u16 = 0x38;
pub const kVK_CapsLock: u16 = 0x39;
pub const kVK_Option: u16 = 0x3A;
pub const kVK_Control: u16 = 0x3B;
pub const kVK_RightShift: u16 = 0x3C;
pub const kVK_RightOption: u16 = 0x3D;
pub const kVK_RightControl: u16 = 0x3E;
pub const kVK_Function: u16 = 0x3F;
pub const kVK_F17: u16 = 0x40;
pub const kVK_VolumeUp: u16 = 0x48;
pub const kVK_VolumeDown: u16 = 0x49;
pub const kVK_Mute: u16 = 0x4A;
pub const kVK_F18: u16 = 0x4F;
pub const kVK_F19: u16 = 0x50;
pub const kVK_F20: u16 = 0x5A;
pub const kVK_F5: u16 = 0x60;
pub const kVK_F6: u16 = 0x61;
pub const kVK_F7: u16 = 0x62;
pub const kVK_F3: u16 = 0x63;
pub const kVK_F8: u16 = 0x64;
pub const kVK_F9: u16 = 0x65;
pub const kVK_F11: u16 = 0x67;
pub const kVK_F13: u16 = 0x69;
pub const kVK_F16: u16 = 0x6A;
pub const kVK_F14: u16 = 0x6B;
pub const kVK_F10: u16 = 0x6D;
pub const kVK_F12: u16 = 0x6F;
pub const kVK_F15: u16 = 0x71;
pub const kVK_Help: u16 = 0x72;
pub const kVK_Home: u16 = 0x73;
pub const kVK_PageUp: u16 = 0x74;
pub const kVK_ForwardDelete: u16 = 0x75;
pub const kVK_F4: u16 = 0x76;
pub const kVK_End: u16 = 0x77;
pub const kVK_F2: u16 = 0x78;
pub const kVK_PageDown: u16 = 0x79;
pub const kVK_F1: u16 = 0x7A;
pub const kVK_LeftArrow: u16 = 0x7B;
pub const kVK_RightArrow: u16 = 0x7C;
pub const kVK_DownArrow: u16 = 0x7D;
pub const kVK_UpArrow: u16 = 0x7E;
pub const kVK_ANSI_Keypad0: u16 = 0x52;
pub const kVK_ANSI_Keypad1: u16 = 0x53;
pub const kVK_ANSI_Keypad2: u16 = 0x54;
pub const kVK_ANSI_Keypad3: u16 = 0x55;
pub const kVK_ANSI_Keypad4: u16 = 0x56;
pub const kVK_ANSI_Keypad5: u16 = 0x57;
pub const kVK_ANSI_Keypad6: u16 = 0x58;
pub const kVK_ANSI_Keypad7: u16 = 0x59;
pub const kVK_ANSI_Keypad8: u16 = 0x5B;
pub const kVK_ANSI_Keypad9: u16 = 0x5C;
pub const kVK_ANSI_KeypadClear: u16 = 0x47;
pub const kVK_ANSI_KeypadDecimal: u16 = 0x41;
pub const kVK_ANSI_KeypadMultiply: u16 = 0x43;
pub const kVK_ANSI_KeypadPlus: u16 = 0x45;
pub const kVK_ANSI_KeypadDivide: u16 = 0x4B;
pub const kVK_ANSI_KeypadEnter: u16 = 0x4C;
pub const kVK_ANSI_KeypadMinus: u16 = 0x4E;
pub const kVK_ANSI_KeypadEquals: u16 = 0x51;

View File

@@ -0,0 +1,680 @@
use core_graphics;
// TODO(dustin): use only the things i need
use self::core_graphics::display::*;
use self::core_graphics::event::*;
use self::core_graphics::event_source::*;
use crate::macos::keycodes::*;
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use objc::runtime::Class;
use std::ffi::CStr;
use std::os::raw::*;
// required for pressedMouseButtons on NSEvent
#[link(name = "AppKit", kind = "framework")]
extern "C" {}
struct MyCGEvent;
#[allow(improper_ctypes)]
#[allow(non_snake_case)]
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent);
// not present in servo/core-graphics
fn CGEventCreateScrollWheelEvent(
source: &CGEventSourceRef,
units: ScrollUnit,
wheelCount: u32,
wheel1: i32,
...
) -> *mut MyCGEvent;
fn CGEventSourceKeyState(stateID: i32, key: u16) -> bool;
}
pub type CFDataRef = *const c_void;
#[repr(C)]
#[derive(Clone, Copy)]
struct NSPoint {
x: f64,
y: f64,
}
#[repr(C)]
pub struct __TISInputSource;
pub type TISInputSourceRef = *const __TISInputSource;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct __CFString([u8; 0]);
pub type CFStringRef = *const __CFString;
pub type Boolean = c_uchar;
pub type UInt8 = c_uchar;
pub type SInt32 = c_int;
pub type UInt16 = c_ushort;
pub type UInt32 = c_uint;
pub type UniChar = UInt16;
pub type UniCharCount = c_ulong;
pub type OptionBits = UInt32;
pub type OSStatus = SInt32;
pub type CFStringEncoding = UInt32;
#[allow(non_upper_case_globals)]
pub const kUCKeyActionDisplay: _bindgen_ty_702 = _bindgen_ty_702::kUCKeyActionDisplay;
#[allow(non_camel_case_types)]
#[repr(u32)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum _bindgen_ty_702 {
// kUCKeyActionDown = 0,
// kUCKeyActionUp = 1,
// kUCKeyActionAutoKey = 2,
kUCKeyActionDisplay = 3,
}
#[allow(non_snake_case)]
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct UCKeyboardTypeHeader {
pub keyboardTypeFirst: UInt32,
pub keyboardTypeLast: UInt32,
pub keyModifiersToTableNumOffset: UInt32,
pub keyToCharTableIndexOffset: UInt32,
pub keyStateRecordsIndexOffset: UInt32,
pub keyStateTerminatorsOffset: UInt32,
pub keySequenceDataIndexOffset: UInt32,
}
#[allow(non_snake_case)]
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct UCKeyboardLayout {
pub keyLayoutHeaderFormat: UInt16,
pub keyLayoutDataVersion: UInt16,
pub keyLayoutFeatureInfoOffset: UInt32,
pub keyboardTypeCount: UInt32,
pub keyboardTypeList: [UCKeyboardTypeHeader; 1usize],
}
#[allow(non_upper_case_globals)]
pub const kUCKeyTranslateNoDeadKeysBit: _bindgen_ty_703 =
_bindgen_ty_703::kUCKeyTranslateNoDeadKeysBit;
#[allow(non_camel_case_types)]
#[repr(u32)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum _bindgen_ty_703 {
kUCKeyTranslateNoDeadKeysBit = 0,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct __CFAllocator([u8; 0]);
pub type CFAllocatorRef = *const __CFAllocator;
// #[repr(u32)]
// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
// pub enum _bindgen_ty_15 {
// kCFStringEncodingMacRoman = 0,
// kCFStringEncodingWindowsLatin1 = 1280,
// kCFStringEncodingISOLatin1 = 513,
// kCFStringEncodingNextStepLatin = 2817,
// kCFStringEncodingASCII = 1536,
// kCFStringEncodingUnicode = 256,
// kCFStringEncodingUTF8 = 134217984,
// kCFStringEncodingNonLossyASCII = 3071,
// kCFStringEncodingUTF16BE = 268435712,
// kCFStringEncodingUTF16LE = 335544576,
// kCFStringEncodingUTF32 = 201326848,
// kCFStringEncodingUTF32BE = 402653440,
// kCFStringEncodingUTF32LE = 469762304,
// }
#[allow(non_upper_case_globals)]
pub const kCFStringEncodingUTF8: u32 = 134_217_984;
#[allow(improper_ctypes)]
#[link(name = "Carbon", kind = "framework")]
extern "C" {
fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef;
// extern void *
// TISGetInputSourceProperty(
// TISInputSourceRef inputSource,
// CFStringRef propertyKey)
#[allow(non_upper_case_globals)]
#[link_name = "kTISPropertyUnicodeKeyLayoutData"]
pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
#[allow(non_snake_case)]
pub fn TISGetInputSourceProperty(
inputSource: TISInputSourceRef,
propertyKey: CFStringRef,
) -> *mut c_void;
#[allow(non_snake_case)]
pub fn CFDataGetBytePtr(theData: CFDataRef) -> *const UInt8;
#[allow(non_snake_case)]
pub fn UCKeyTranslate(
keyLayoutPtr: *const UInt8, //*const UCKeyboardLayout,
virtualKeyCode: UInt16,
keyAction: UInt16,
modifierKeyState: UInt32,
keyboardType: UInt32,
keyTranslateOptions: OptionBits,
deadKeyState: *mut UInt32,
maxStringLength: UniCharCount,
actualStringLength: *mut UniCharCount,
unicodeString: *mut UniChar,
) -> OSStatus;
pub fn LMGetKbdType() -> UInt8;
#[allow(non_snake_case)]
pub fn CFStringCreateWithCharacters(
alloc: CFAllocatorRef,
chars: *const UniChar,
numChars: CFIndex,
) -> CFStringRef;
#[allow(non_upper_case_globals)]
#[link_name = "kCFAllocatorDefault"]
pub static kCFAllocatorDefault: CFAllocatorRef;
#[allow(non_snake_case)]
pub fn CFStringGetCString(
theString: CFStringRef,
buffer: *mut c_char,
bufferSize: CFIndex,
encoding: CFStringEncoding,
) -> Boolean;
}
// not present in servo/core-graphics
#[allow(dead_code)]
#[derive(Debug)]
enum ScrollUnit {
Pixel = 0,
Line = 1,
}
// hack
/// The main struct for handling the event emitting
pub struct Enigo {
event_source: Option<CGEventSource>,
keycode_to_string_map: std::collections::HashMap<String, CGKeyCode>,
double_click_interval: u32,
last_click_time: Option<std::time::Instant>,
multiple_click: i64,
flags: CGEventFlags,
}
impl Enigo {
///
pub fn reset_flag(&mut self) {
self.flags = CGEventFlags::CGEventFlagNull;
}
///
pub fn add_flag(&mut self, key: &Key) {
let flag = match key {
&Key::CapsLock => CGEventFlags::CGEventFlagAlphaShift,
&Key::Shift => CGEventFlags::CGEventFlagShift,
&Key::Control => CGEventFlags::CGEventFlagControl,
&Key::Alt => CGEventFlags::CGEventFlagAlternate,
&Key::Meta => CGEventFlags::CGEventFlagCommand,
&Key::NumLock => CGEventFlags::CGEventFlagNumericPad,
_ => CGEventFlags::CGEventFlagNull,
};
self.flags |= flag;
}
fn post(&self, event: CGEvent) {
event.set_flags(self.flags);
event.post(CGEventTapLocation::HID);
}
}
impl Default for Enigo {
fn default() -> Self {
let mut double_click_interval = 500;
if let Some(ns_event) = Class::get("NSEvent") {
let tm: f64 = unsafe { msg_send![ns_event, doubleClickInterval] };
if tm > 0. {
double_click_interval = (tm * 1000.) as u32;
log::info!("double click interval: {}ms", double_click_interval);
}
}
Self {
// TODO(dustin): return error rather than panic here
event_source: if let Ok(src) =
CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
{
Some(src)
} else {
None
},
keycode_to_string_map: Default::default(),
double_click_interval,
multiple_click: 1,
last_click_time: None,
flags: CGEventFlags::CGEventFlagNull,
}
}
}
impl MouseControllable for Enigo {
fn mouse_move_to(&mut self, x: i32, y: i32) {
let pressed = Self::pressed_buttons();
let event_type = if pressed & 1 > 0 {
CGEventType::LeftMouseDragged
} else if pressed & 2 > 0 {
CGEventType::RightMouseDragged
} else {
CGEventType::MouseMoved
};
let dest = CGPoint::new(x as f64, y as f64);
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) =
CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left)
{
self.post(event);
}
}
}
fn mouse_move_relative(&mut self, x: i32, y: i32) {
let (display_width, display_height) = Self::main_display_size();
let (current_x, y_inv) = Self::mouse_location_raw_coords();
let current_y = (display_height as i32) - y_inv;
let new_x = current_x + x;
let new_y = current_y + y;
if new_x < 0
|| new_x as usize > display_width
|| new_y < 0
|| new_y as usize > display_height
{
return;
}
self.mouse_move_to(new_x, new_y);
}
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
let now = std::time::Instant::now();
if let Some(t) = self.last_click_time {
if t.elapsed().as_millis() as u32 <= self.double_click_interval {
self.multiple_click += 1;
} else {
self.multiple_click = 1;
}
}
self.last_click_time = Some(now);
let (current_x, current_y) = Self::mouse_location();
let (button, event_type) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown),
_ => unimplemented!(),
};
let dest = CGPoint::new(current_x as f64, current_y as f64);
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) {
if self.multiple_click > 1 {
event.set_integer_value_field(
EventField::MOUSE_EVENT_CLICK_STATE,
self.multiple_click,
);
}
self.post(event);
}
}
Ok(())
}
fn mouse_up(&mut self, button: MouseButton) {
let (current_x, current_y) = Self::mouse_location();
let (button, event_type) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp),
_ => unimplemented!(),
};
let dest = CGPoint::new(current_x as f64, current_y as f64);
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) {
if self.multiple_click > 1 {
event.set_integer_value_field(
EventField::MOUSE_EVENT_CLICK_STATE,
self.multiple_click,
);
}
self.post(event);
}
}
}
fn mouse_click(&mut self, button: MouseButton) {
self.mouse_down(button).ok();
self.mouse_up(button);
}
fn mouse_scroll_x(&mut self, length: i32) {
let mut scroll_direction = -1; // 1 left -1 right;
let mut length = length;
if length < 0 {
length *= -1;
scroll_direction *= -1;
}
if let Some(src) = self.event_source.as_ref() {
for _ in 0..length {
unsafe {
let mouse_ev = CGEventCreateScrollWheelEvent(
&src,
ScrollUnit::Line,
2, // CGWheelCount 1 = y 2 = xy 3 = xyz
0,
scroll_direction,
);
CGEventPost(CGEventTapLocation::HID, mouse_ev);
CFRelease(mouse_ev as *const std::ffi::c_void);
}
}
}
}
fn mouse_scroll_y(&mut self, length: i32) {
let mut scroll_direction = -1; // 1 left -1 right;
let mut length = length;
if length < 0 {
length *= -1;
scroll_direction *= -1;
}
if let Some(src) = self.event_source.as_ref() {
for _ in 0..length {
unsafe {
let mouse_ev = CGEventCreateScrollWheelEvent(
&src,
ScrollUnit::Line,
1, // CGWheelCount 1 = y 2 = xy 3 = xyz
scroll_direction,
);
CGEventPost(CGEventTapLocation::HID, mouse_ev);
CFRelease(mouse_ev as *const std::ffi::c_void);
}
}
}
}
}
// https://stackoverflow.
// com/questions/1918841/how-to-convert-ascii-character-to-cgkeycode
impl KeyboardControllable for Enigo {
fn key_sequence(&mut self, sequence: &str) {
// NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68
// TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time
// but i am unsure what would happen for grapheme clusters greater than 20 bytes ...
use unicode_segmentation::UnicodeSegmentation;
let clusters = UnicodeSegmentation::graphemes(sequence, true).collect::<Vec<&str>>();
for cluster in clusters {
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), 0, true) {
event.set_string(cluster);
self.post(event);
}
}
}
}
fn key_click(&mut self, key: Key) {
let keycode = self.key_to_keycode(key);
if keycode == 0 {
return;
}
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, true) {
self.post(event);
}
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, false) {
self.post(event);
}
}
}
fn key_down(&mut self, key: Key) -> crate::ResultType {
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) =
CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), true)
{
self.post(event);
}
}
Ok(())
}
fn key_up(&mut self, key: Key) {
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) =
CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false)
{
self.post(event);
}
}
}
fn get_key_state(&mut self, key: Key) -> bool {
let keycode = self.key_to_keycode(key);
unsafe { CGEventSourceKeyState(1, keycode) }
}
}
impl Enigo {
fn pressed_buttons() -> usize {
if let Some(ns_event) = Class::get("NSEvent") {
unsafe { msg_send![ns_event, pressedMouseButtons] }
} else {
0
}
}
/// Fetches the `(width, height)` in pixels of the main display
pub fn main_display_size() -> (usize, usize) {
let display_id = unsafe { CGMainDisplayID() };
let width = unsafe { CGDisplayPixelsWide(display_id) };
let height = unsafe { CGDisplayPixelsHigh(display_id) };
(width, height)
}
/// Returns the current mouse location in Cocoa coordinates which have Y
/// inverted from the Carbon coordinates used in the rest of the API.
/// This function exists so that mouse_move_relative only has to fetch
/// the screen size once.
fn mouse_location_raw_coords() -> (i32, i32) {
if let Some(ns_event) = Class::get("NSEvent") {
let pt: NSPoint = unsafe { msg_send![ns_event, mouseLocation] };
(pt.x as i32, pt.y as i32)
} else {
(0, 0)
}
}
/// The mouse coordinates in points, only works on the main display
pub fn mouse_location() -> (i32, i32) {
let (x, y_inv) = Self::mouse_location_raw_coords();
let (_, display_height) = Self::main_display_size();
(x, (display_height as i32) - y_inv)
}
fn key_to_keycode(&mut self, key: Key) -> CGKeyCode {
#[allow(deprecated)]
// I mean duh, we still need to support deprecated keys until they're removed
match key {
Key::Alt => kVK_Option,
Key::Backspace => kVK_Delete,
Key::CapsLock => kVK_CapsLock,
Key::Control => kVK_Control,
Key::Delete => kVK_ForwardDelete,
Key::DownArrow => kVK_DownArrow,
Key::End => kVK_End,
Key::Escape => kVK_Escape,
Key::F1 => kVK_F1,
Key::F10 => kVK_F10,
Key::F11 => kVK_F11,
Key::F12 => kVK_F12,
Key::F2 => kVK_F2,
Key::F3 => kVK_F3,
Key::F4 => kVK_F4,
Key::F5 => kVK_F5,
Key::F6 => kVK_F6,
Key::F7 => kVK_F7,
Key::F8 => kVK_F8,
Key::F9 => kVK_F9,
Key::Home => kVK_Home,
Key::LeftArrow => kVK_LeftArrow,
Key::Option => kVK_Option,
Key::PageDown => kVK_PageDown,
Key::PageUp => kVK_PageUp,
Key::Return => kVK_Return,
Key::RightArrow => kVK_RightArrow,
Key::Shift => kVK_Shift,
Key::Space => kVK_Space,
Key::Tab => kVK_Tab,
Key::UpArrow => kVK_UpArrow,
Key::Numpad0 => kVK_ANSI_Keypad0,
Key::Numpad1 => kVK_ANSI_Keypad1,
Key::Numpad2 => kVK_ANSI_Keypad2,
Key::Numpad3 => kVK_ANSI_Keypad3,
Key::Numpad4 => kVK_ANSI_Keypad4,
Key::Numpad5 => kVK_ANSI_Keypad5,
Key::Numpad6 => kVK_ANSI_Keypad6,
Key::Numpad7 => kVK_ANSI_Keypad7,
Key::Numpad8 => kVK_ANSI_Keypad8,
Key::Numpad9 => kVK_ANSI_Keypad9,
Key::Mute => kVK_Mute,
Key::VolumeDown => kVK_VolumeUp,
Key::VolumeUp => kVK_VolumeDown,
Key::Help => kVK_Help,
Key::Snapshot => kVK_F13,
Key::Clear => kVK_ANSI_KeypadClear,
Key::Decimal => kVK_ANSI_KeypadDecimal,
Key::Multiply => kVK_ANSI_KeypadMultiply,
Key::Add => kVK_ANSI_KeypadPlus,
Key::Divide => kVK_ANSI_KeypadDivide,
Key::NumpadEnter => kVK_ANSI_KeypadEnter,
Key::Subtract => kVK_ANSI_KeypadMinus,
Key::Equals => kVK_ANSI_KeypadEquals,
Key::NumLock => kVK_ANSI_KeypadClear,
Key::Raw(raw_keycode) => raw_keycode,
Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()),
Key::Super | Key::Command | Key::Windows | Key::Meta => kVK_Command,
_ => 0,
}
}
fn get_layoutdependent_keycode(&mut self, string: String) -> CGKeyCode {
if self.keycode_to_string_map.is_empty() {
self.init_map();
}
*self.keycode_to_string_map.get(&string).unwrap_or(&0)
}
fn init_map(&mut self) {
self.keycode_to_string_map.insert("".to_owned(), 0);
// loop through every keycode (0 - 127)
for keycode in 0..128 {
// no modifier
if let Some(key_string) = self.keycode_to_string(keycode, 0x100) {
self.keycode_to_string_map.insert(key_string, keycode);
}
// shift modifier
if let Some(key_string) = self.keycode_to_string(keycode, 0x20102) {
self.keycode_to_string_map.insert(key_string, keycode);
}
// alt modifier
// if let Some(string) = self.keycode_to_string(keycode, 0x80120) {
// println!("{:?}", string);
// }
// alt + shift modifier
// if let Some(string) = self.keycode_to_string(keycode, 0xa0122) {
// println!("{:?}", string);
// }
}
}
fn keycode_to_string(&self, keycode: u16, modifier: u32) -> Option<String> {
let cf_string = self.create_string_for_key(keycode, modifier);
unsafe {
if !cf_string.is_null() {
let mut buf: [i8; 255] = [0; 255];
let success = CFStringGetCString(
cf_string,
buf.as_mut_ptr(),
buf.len() as _,
kCFStringEncodingUTF8,
);
if success != 0 {
let name: &CStr = CStr::from_ptr(buf.as_ptr());
if let Ok(name) = name.to_str() {
return Some(name.to_owned());
}
}
}
}
None
}
fn create_string_for_key(&self, keycode: u16, modifier: u32) -> CFStringRef {
let current_keyboard = unsafe { TISCopyCurrentKeyboardInputSource() };
let layout_data = unsafe {
TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData)
};
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
let mut keys_down: UInt32 = 0;
// let mut chars: *mut c_void;//[UniChar; 4];
let mut chars: u16 = 0;
let mut real_length: UniCharCount = 0;
unsafe {
UCKeyTranslate(
keyboard_layout,
keycode,
kUCKeyActionDisplay as u16,
modifier,
LMGetKbdType() as u32,
kUCKeyTranslateNoDeadKeysBit as u32,
&mut keys_down,
8, // sizeof(chars) / sizeof(chars[0]),
&mut real_length,
&mut chars,
);
}
unsafe { CFStringCreateWithCharacters(kCFAllocatorDefault, &chars, 1) }
}
}
unsafe impl Send for Enigo {}

View File

@@ -0,0 +1,4 @@
mod macos_impl;
pub mod keycodes;
pub use self::macos_impl::Enigo;

View File

@@ -0,0 +1,73 @@
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731
pub const EVK_RETURN: u16 = 0x0D;
pub const EVK_TAB: u16 = 0x09;
pub const EVK_SPACE: u16 = 0x20;
pub const EVK_BACK: u16 = 0x08;
pub const EVK_ESCAPE: u16 = 0x1b;
pub const EVK_LWIN: u16 = 0x5b;
pub const EVK_SHIFT: u16 = 0x10;
pub const EVK_CAPITAL: u16 = 0x14;
pub const EVK_MENU: u16 = 0x12;
pub const EVK_LCONTROL: u16 = 0xa2;
pub const EVK_HOME: u16 = 0x24;
pub const EVK_PRIOR: u16 = 0x21;
pub const EVK_NEXT: u16 = 0x22;
pub const EVK_END: u16 = 0x23;
pub const EVK_LEFT: u16 = 0x25;
pub const EVK_RIGHT: u16 = 0x27;
pub const EVK_UP: u16 = 0x26;
pub const EVK_DOWN: u16 = 0x28;
pub const EVK_DELETE: u16 = 0x2E;
pub const EVK_F1: u16 = 0x70;
pub const EVK_F2: u16 = 0x71;
pub const EVK_F3: u16 = 0x72;
pub const EVK_F4: u16 = 0x73;
pub const EVK_F5: u16 = 0x74;
pub const EVK_F6: u16 = 0x75;
pub const EVK_F7: u16 = 0x76;
pub const EVK_F8: u16 = 0x77;
pub const EVK_F9: u16 = 0x78;
pub const EVK_F10: u16 = 0x79;
pub const EVK_F11: u16 = 0x7a;
pub const EVK_F12: u16 = 0x7b;
pub const EVK_NUMPAD0: u16 = 0x60;
pub const EVK_NUMPAD1: u16 = 0x61;
pub const EVK_NUMPAD2: u16 = 0x62;
pub const EVK_NUMPAD3: u16 = 0x63;
pub const EVK_NUMPAD4: u16 = 0x64;
pub const EVK_NUMPAD5: u16 = 0x65;
pub const EVK_NUMPAD6: u16 = 0x66;
pub const EVK_NUMPAD7: u16 = 0x67;
pub const EVK_NUMPAD8: u16 = 0x68;
pub const EVK_NUMPAD9: u16 = 0x69;
pub const EVK_CANCEL: u16 = 0x03;
pub const EVK_CLEAR: u16 = 0x0C;
pub const EVK_PAUSE: u16 = 0x13;
pub const EVK_KANA: u16 = 0x15;
pub const EVK_HANGUL: u16 = 0x15;
pub const EVK_JUNJA: u16 = 0x17;
pub const EVK_FINAL: u16 = 0x18;
pub const EVK_HANJA: u16 = 0x19;
pub const EVK_KANJI: u16 = 0x19;
pub const EVK_CONVERT: u16 = 0x1C;
pub const EVK_SELECT: u16 = 0x29;
pub const EVK_PRINT: u16 = 0x2A;
pub const EVK_EXECUTE: u16 = 0x2B;
pub const EVK_SNAPSHOT: u16 = 0x2C;
pub const EVK_INSERT: u16 = 0x2D;
pub const EVK_HELP: u16 = 0x2F;
pub const EVK_SLEEP: u16 = 0x5F;
pub const EVK_SEPARATOR: u16 = 0x6C;
pub const EVK_VOLUME_MUTE: u16 = 0xAD;
pub const EVK_VOLUME_DOWN: u16 = 0xAE;
pub const EVK_VOLUME_UP: u16 = 0xAF;
pub const EVK_NUMLOCK: u16 = 0x90;
pub const EVK_SCROLL: u16 = 0x91;
pub const EVK_RWIN: u16 = 0x5C;
pub const EVK_APPS: u16 = 0x5D;
pub const EVK_ADD: u16 = 0x6B;
pub const EVK_MULTIPLY: u16 = 0x6A;
pub const EVK_SUBTRACT: u16 = 0x6D;
pub const EVK_DECIMAL: u16 = 0x6E;
pub const EVK_DIVIDE: u16 = 0x6F;

View File

@@ -0,0 +1,4 @@
mod win_impl;
pub mod keycodes;
pub use self::win_impl::Enigo;

View File

@@ -0,0 +1,366 @@
use winapi;
use self::winapi::ctypes::c_int;
use self::winapi::shared::{minwindef::*, windef::*};
use self::winapi::um::winbase::*;
use self::winapi::um::winuser::*;
use crate::win::keycodes::*;
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use std::mem::*;
extern "system" {
pub fn GetLastError() -> DWORD;
}
/// The main struct for handling the event emitting
#[derive(Default)]
pub struct Enigo;
fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD {
let mut input = INPUT {
type_: INPUT_MOUSE,
u: unsafe {
transmute(MOUSEINPUT {
dx,
dy,
mouseData: data,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
})
},
};
unsafe { SendInput(1, &mut input as LPINPUT, size_of::<INPUT>() as c_int) }
}
fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD {
let mut input = INPUT {
type_: INPUT_KEYBOARD,
u: unsafe {
transmute_copy(&KEYBDINPUT {
wVk: vk,
wScan: scan,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
})
},
};
unsafe { SendInput(1, &mut input as LPINPUT, size_of::<INPUT>() as c_int) }
}
fn get_error() -> String {
unsafe {
let buff_size = 256;
let mut buff: Vec<u16> = Vec::with_capacity(buff_size);
buff.resize(buff_size, 0);
let errno = GetLastError();
let chars_copied = FormatMessageW(
FORMAT_MESSAGE_IGNORE_INSERTS
| FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_ARGUMENT_ARRAY,
std::ptr::null(),
errno,
0,
buff.as_mut_ptr(),
(buff_size + 1) as u32,
std::ptr::null_mut(),
);
if chars_copied == 0 {
return "".to_owned();
}
let mut curr_char: usize = chars_copied as usize;
while curr_char > 0 {
let ch = buff[curr_char];
if ch >= ' ' as u16 {
break;
}
curr_char -= 1;
}
let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char);
let err_msg = String::from_utf16(sl);
return err_msg.unwrap_or("".to_owned());
}
}
impl MouseControllable for Enigo {
fn mouse_move_to(&mut self, x: i32, y: i32) {
mouse_event(
MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
0,
(x - unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) }) * 65535
/ unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) },
(y - unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) }) * 65535
/ unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) },
);
}
fn mouse_move_relative(&mut self, x: i32, y: i32) {
mouse_event(MOUSEEVENTF_MOVE, 0, x, y);
}
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
let res = mouse_event(
match button {
MouseButton::Left => MOUSEEVENTF_LEFTDOWN,
MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN,
MouseButton::Right => MOUSEEVENTF_RIGHTDOWN,
_ => unimplemented!(),
},
0,
0,
0,
);
if res == 0 {
let err = get_error();
if !err.is_empty() {
return Err(err.into());
}
}
Ok(())
}
fn mouse_up(&mut self, button: MouseButton) {
mouse_event(
match button {
MouseButton::Left => MOUSEEVENTF_LEFTUP,
MouseButton::Middle => MOUSEEVENTF_MIDDLEUP,
MouseButton::Right => MOUSEEVENTF_RIGHTUP,
_ => unimplemented!(),
},
0,
0,
0,
);
}
fn mouse_click(&mut self, button: MouseButton) {
self.mouse_down(button).ok();
self.mouse_up(button);
}
fn mouse_scroll_x(&mut self, length: i32) {
mouse_event(MOUSEEVENTF_HWHEEL, unsafe { transmute(length * 120) }, 0, 0);
}
fn mouse_scroll_y(&mut self, length: i32) {
mouse_event(MOUSEEVENTF_WHEEL, unsafe { transmute(length * 120) }, 0, 0);
}
}
impl KeyboardControllable for Enigo {
fn key_sequence(&mut self, sequence: &str) {
let mut buffer = [0; 2];
for c in sequence.chars() {
// Windows uses uft-16 encoding. We need to check
// for variable length characters. As such some
// characters can be 32 bit long and those are
// encoded in such called hight and low surrogates
// each 16 bit wide that needs to be send after
// another to the SendInput function without
// being interrupted by "keyup"
let result = c.encode_utf16(&mut buffer);
if result.len() == 1 {
self.unicode_key_click(result[0]);
} else {
for utf16_surrogate in result {
self.unicode_key_down(utf16_surrogate.clone());
}
// do i need to produce a keyup?
// self.unicode_key_up(0);
}
}
}
fn key_click(&mut self, key: Key) {
let scancode = self.key_to_scancode(key);
keybd_event(KEYEVENTF_SCANCODE, 0, scancode);
keybd_event(KEYEVENTF_KEYUP | KEYEVENTF_SCANCODE, 0, scancode);
}
fn key_down(&mut self, key: Key) -> crate::ResultType {
let res = keybd_event(KEYEVENTF_SCANCODE, 0, self.key_to_scancode(key));
if res == 0 {
let err = get_error();
if !err.is_empty() {
return Err(err.into());
}
}
Ok(())
}
fn key_up(&mut self, key: Key) {
keybd_event(
KEYEVENTF_KEYUP | KEYEVENTF_SCANCODE,
0,
self.key_to_scancode(key),
);
}
fn get_key_state(&mut self, key: Key) -> bool {
let keycode = self.key_to_keycode(key);
let x = unsafe { GetKeyState(keycode as _) };
if key == Key::CapsLock || key == Key::NumLock || key == Key::Scroll {
return (x & 0x1) == 0x1;
}
return (x as u16 & 0x8000) == 0x8000;
}
}
impl Enigo {
/// Gets the (width, height) of the main display in screen coordinates (pixels).
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut size = Enigo::main_display_size();
/// ```
pub fn main_display_size() -> (usize, usize) {
let w = unsafe { GetSystemMetrics(SM_CXSCREEN) as usize };
let h = unsafe { GetSystemMetrics(SM_CYSCREEN) as usize };
(w, h)
}
/// Gets the location of mouse in screen coordinates (pixels).
///
/// # Example
///
/// ```no_run
/// use enigo::*;
/// let mut location = Enigo::mouse_location();
/// ```
pub fn mouse_location() -> (i32, i32) {
let mut point = POINT { x: 0, y: 0 };
let result = unsafe { GetCursorPos(&mut point) };
if result != 0 {
(point.x, point.y)
} else {
(0, 0)
}
}
fn unicode_key_click(&self, unicode_char: u16) {
self.unicode_key_down(unicode_char);
self.unicode_key_up(unicode_char);
}
fn unicode_key_down(&self, unicode_char: u16) {
keybd_event(KEYEVENTF_UNICODE, 0, unicode_char);
}
fn unicode_key_up(&self, unicode_char: u16) {
keybd_event(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, unicode_char);
}
fn key_to_keycode(&self, key: Key) -> u16 {
// do not use the codes from crate winapi they're
// wrongly typed with i32 instead of i16 use the
// ones provided by win/keycodes.rs that are prefixed
// with an 'E' infront of the original name
#[allow(deprecated)]
// I mean duh, we still need to support deprecated keys until they're removed
match key {
Key::Alt => EVK_MENU,
Key::Backspace => EVK_BACK,
Key::CapsLock => EVK_CAPITAL,
Key::Control => EVK_LCONTROL,
Key::Delete => EVK_DELETE,
Key::DownArrow => EVK_DOWN,
Key::End => EVK_END,
Key::Escape => EVK_ESCAPE,
Key::F1 => EVK_F1,
Key::F10 => EVK_F10,
Key::F11 => EVK_F11,
Key::F12 => EVK_F12,
Key::F2 => EVK_F2,
Key::F3 => EVK_F3,
Key::F4 => EVK_F4,
Key::F5 => EVK_F5,
Key::F6 => EVK_F6,
Key::F7 => EVK_F7,
Key::F8 => EVK_F8,
Key::F9 => EVK_F9,
Key::Home => EVK_HOME,
Key::LeftArrow => EVK_LEFT,
Key::Option => EVK_MENU,
Key::PageDown => EVK_NEXT,
Key::PageUp => EVK_PRIOR,
Key::Return => EVK_RETURN,
Key::RightArrow => EVK_RIGHT,
Key::Shift => EVK_SHIFT,
Key::Space => EVK_SPACE,
Key::Tab => EVK_TAB,
Key::UpArrow => EVK_UP,
Key::Numpad0 => EVK_NUMPAD0,
Key::Numpad1 => EVK_NUMPAD1,
Key::Numpad2 => EVK_NUMPAD2,
Key::Numpad3 => EVK_NUMPAD3,
Key::Numpad4 => EVK_NUMPAD4,
Key::Numpad5 => EVK_NUMPAD5,
Key::Numpad6 => EVK_NUMPAD6,
Key::Numpad7 => EVK_NUMPAD7,
Key::Numpad8 => EVK_NUMPAD8,
Key::Numpad9 => EVK_NUMPAD9,
Key::Cancel => EVK_CANCEL,
Key::Clear => EVK_CLEAR,
Key::Menu => EVK_MENU,
Key::Pause => EVK_PAUSE,
Key::Kana => EVK_KANA,
Key::Hangul => EVK_HANGUL,
Key::Junja => EVK_JUNJA,
Key::Final => EVK_FINAL,
Key::Hanja => EVK_HANJA,
Key::Kanji => EVK_KANJI,
Key::Convert => EVK_CONVERT,
Key::Select => EVK_SELECT,
Key::Print => EVK_PRINT,
Key::Execute => EVK_EXECUTE,
Key::Snapshot => EVK_SNAPSHOT,
Key::Insert => EVK_INSERT,
Key::Help => EVK_HELP,
Key::Sleep => EVK_SLEEP,
Key::Separator => EVK_SEPARATOR,
Key::Mute => EVK_VOLUME_MUTE,
Key::VolumeDown => EVK_VOLUME_DOWN,
Key::VolumeUp => EVK_VOLUME_UP,
Key::Scroll => EVK_SCROLL,
Key::NumLock => EVK_NUMLOCK,
Key::RWin => EVK_RWIN,
Key::Apps => EVK_APPS,
Key::Add => EVK_ADD,
Key::Multiply => EVK_MULTIPLY,
Key::Decimal => EVK_DECIMAL,
Key::Subtract => EVK_SUBTRACT,
Key::Divide => EVK_DIVIDE,
Key::NumpadEnter => EVK_RETURN,
Key::Equals => '=' as _,
Key::Raw(raw_keycode) => raw_keycode,
Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()),
Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN,
}
}
fn key_to_scancode(&self, key: Key) -> u16 {
let keycode = self.key_to_keycode(key);
unsafe { MapVirtualKeyW(keycode as u32, 0) as u16 }
}
fn get_layoutdependent_keycode(&self, string: String) -> u16 {
// get the first char from the string ignore the rest
// ensure its not a multybyte char
if let Some(chr) = string.chars().nth(0) {
// NOTE VkKeyScanW uses the current keyboard layout
// to specify a layout use VkKeyScanExW and GetKeyboardLayout
// or load one with LoadKeyboardLayoutW
let keycode_and_shiftstate = unsafe { VkKeyScanW(chr as _) };
keycode_and_shiftstate as _
} else {
0
}
}
}

4
libs/hbb_common/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
src/protos/

View File

@@ -0,0 +1,46 @@
[package]
name = "hbb_common"
version = "0.1.0"
authors = ["open-trade <info@opentradesolutions.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
protobuf = { version = "3.0.0-pre", git = "https://github.com/stepancheg/rust-protobuf" }
tokio = { version = "0.2", features = ["full"] }
tokio-util = { version = "0.3", features = ["full"] }
futures = "0.3"
bytes = "0.5"
log = "0.4"
env_logger = "0.8"
socket2 = { version = "0.3", features = ["reuseport"] }
zstd = "0.5"
quinn = {version = "0.6", optional = true }
anyhow = "1.0"
futures-util = "0.3"
directories-next = "2.0"
rand = "0.7"
serde_derive = "1.0"
serde = "1.0"
lazy_static = "1.4"
confy = { git = "https://github.com/open-trade/confy" }
dirs-next = "2.0"
filetime = "0.2"
sodiumoxide = "0.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
[features]
quic = ["quinn"]
[build-dependencies]
protobuf-codegen-pure = { version = "3.0.0-pre", git = "https://github.com/stepancheg/rust-protobuf" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
[dev-dependencies]
toml = "0.5"
serde_json = "1.0"

9
libs/hbb_common/build.rs Normal file
View File

@@ -0,0 +1,9 @@
fn main() {
std::fs::create_dir_all("src/protos").unwrap();
protobuf_codegen_pure::Codegen::new()
.out_dir("src/protos")
.inputs(&["protos/rendezvous.proto", "protos/message.proto"])
.include("protos")
.run()
.expect("Codegen failed.");
}

View File

@@ -0,0 +1,401 @@
syntax = "proto3";
package hbb;
message VP9 {
bytes data = 1;
bool key = 2;
int64 pts = 3;
}
message VP9s { repeated VP9 frames = 1; }
message RGB { bool compress = 1; }
// planes data send directly in binary for better use arraybuffer on web
message YUV {
bool compress = 1;
int32 stride = 2;
}
message VideoFrame {
oneof union {
VP9s vp9s = 6;
RGB rgb = 7;
YUV yuv = 8;
}
}
message DisplayInfo {
sint32 x = 1;
sint32 y = 2;
int32 width = 3;
int32 height = 4;
string name = 5;
bool online = 6;
}
message PortForward {
string host = 1;
int32 port = 2;
}
message FileTransfer {
string dir = 1;
bool show_hidden = 2;
}
message LoginRequest {
string username = 1;
bytes password = 2;
string my_id = 4;
string my_name = 5;
OptionMessage option = 6;
oneof union {
FileTransfer file_transfer = 7;
PortForward port_forward = 8;
}
}
message ChatMessage { string text = 1; }
message PeerInfo {
string username = 1;
string hostname = 2;
string platform = 3;
repeated DisplayInfo displays = 4;
int32 current_display = 5;
bool sas_enabled = 6;
string version = 7;
}
message LoginResponse {
oneof union {
string error = 1;
PeerInfo peer_info = 2;
}
}
message MouseEvent {
int32 mask = 1;
sint32 x = 2;
sint32 y = 3;
repeated ControlKey modifiers = 4;
}
enum ControlKey {
Alt = 1;
Backspace = 2;
CapsLock = 3;
Control = 4;
Delete = 5;
DownArrow = 6;
End = 7;
Escape = 8;
F1 = 9;
F10 = 10;
F11 = 11;
F12 = 12;
F2 = 13;
F3 = 14;
F4 = 15;
F5 = 16;
F6 = 17;
F7 = 18;
F8 = 19;
F9 = 20;
Home = 21;
LeftArrow = 22;
/// meta key (also known as "windows"; "super"; and "command")
Meta = 23;
/// option key on macOS (alt key on Linux and Windows)
Option = 24;
PageDown = 25;
PageUp = 26;
Return = 27;
RightArrow = 28;
Shift = 29;
Space = 30;
Tab = 31;
UpArrow = 32;
Numpad0 = 33;
Numpad1 = 34;
Numpad2 = 35;
Numpad3 = 36;
Numpad4 = 37;
Numpad5 = 38;
Numpad6 = 39;
Numpad7 = 40;
Numpad8 = 41;
Numpad9 = 42;
Cancel = 43;
Clear = 44;
Menu = 45;
Pause = 46;
Kana = 47;
Hangul = 48;
Junja = 49;
Final = 50;
Hanja = 51;
Kanji = 52;
Convert = 53;
Select = 54;
Print = 55;
Execute = 56;
Snapshot = 57;
Insert = 58;
Help = 59;
Sleep = 60;
Separator = 61;
Scroll = 62;
NumLock = 63;
RWin = 64;
Apps = 65;
Multiply = 66;
Add = 67;
Subtract = 68;
Decimal = 69;
Divide = 70;
Equals = 71;
NumpadEnter = 72;
CtrlAltDel = 100;
LockScreen = 101;
}
message KeyEvent {
bool down = 1;
bool press = 2;
oneof union {
ControlKey control_key = 3;
uint32 chr = 4;
uint32 unicode = 5;
string seq = 6;
}
repeated ControlKey modifiers = 8;
}
message CursorData {
uint64 id = 1;
sint32 hotx = 2;
sint32 hoty = 3;
int32 width = 4;
int32 height = 5;
bytes colors = 6;
}
message CursorPosition {
sint32 x = 1;
sint32 y = 2;
}
message Hash {
string salt = 1;
string challenge = 2;
};
message Clipboard {
bool compress = 1;
bytes content = 2;
};
enum FileType {
Dir = 1;
DirLink = 2;
DirDrive = 3;
File = 4;
FileLink = 5;
}
message FileEntry {
FileType entry_type = 1;
string name = 2;
bool is_hidden = 3;
uint64 size = 4;
uint64 modified_time = 5;
}
message FileDirectory {
int32 id = 1;
string path = 2;
repeated FileEntry entries = 3;
}
message ReadDir {
string path = 1;
bool include_hidden = 2;
}
message ReadAllFiles {
int32 id = 1;
string path = 2;
bool include_hidden = 3;
}
message FileAction {
oneof union {
ReadDir read_dir = 1;
FileTransferSendRequest send = 2;
FileTransferReceiveRequest receive = 3;
FileDirCreate create = 4;
FileRemoveDir remove_dir = 5;
FileRemoveFile remove_file = 6;
ReadAllFiles all_files = 7;
FileTransferCancel cancel = 8;
}
}
message FileTransferCancel { int32 id = 1; }
message FileResponse {
oneof union {
FileDirectory dir = 1;
FileTransferBlock block = 2;
FileTransferError error = 3;
FileTransferDone done = 4;
}
}
message FileTransferBlock {
int32 id = 1;
sint32 file_num = 2;
bytes data = 3;
bool compressed = 4;
}
message FileTransferError {
int32 id = 1;
string error = 2;
sint32 file_num = 3;
}
message FileTransferSendRequest {
int32 id = 1;
string path = 2;
bool include_hidden = 3;
}
message FileTransferDone {
int32 id = 1;
sint32 file_num = 2;
}
message FileTransferReceiveRequest {
int32 id = 1;
string path = 2; // path written to
repeated FileEntry files = 3;
}
message FileRemoveDir {
int32 id = 1;
string path = 2;
bool recursive = 3;
}
message FileRemoveFile {
int32 id = 1;
string path = 2;
sint32 file_num = 3;
}
message FileDirCreate {
int32 id = 1;
string path = 2;
}
message SwitchDisplay {
int32 display = 1;
sint32 x = 2;
sint32 y = 3;
int32 width = 4;
int32 height = 5;
}
enum Permission {
Keyboard = 1;
Clipboard = 2;
Audio = 3;
}
message PermissionInfo {
Permission permission = 1;
bool enabled = 2;
}
enum ImageQuality {
NotSet = 0;
Low = 2;
Balanced = 3;
Best = 4;
}
enum BoolOption {
NotSet = 0;
No = 1;
Yes = 2;
}
message OptionMessage {
ImageQuality image_quality = 1;
BoolOption lock_after_session_end = 2;
BoolOption show_remote_cursor = 3;
BoolOption privacy_mode = 4;
BoolOption block_input = 5;
int32 custom_image_quality = 6;
BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8;
}
message TestDelay {
int64 time = 1;
bool from_client = 2;
}
message PublicKey {
bytes asymmetric_value = 1;
bytes symmetric_value = 2;
}
message SignedId {
bytes id = 1;
bytes pk = 2;
}
message AudioFormat {
uint32 sample_rate = 1;
uint32 channels = 2;
}
message AudioFrame { bytes data = 1; }
message Misc {
oneof union {
ChatMessage chat_message = 4;
SwitchDisplay switch_display = 5;
PermissionInfo permission_info = 6;
OptionMessage option = 7;
AudioFormat audio_format = 8;
string close_reason = 9;
bool refresh_video = 10;
}
}
message Message {
oneof union {
SignedId signed_id = 3;
PublicKey public_key = 4;
TestDelay test_delay = 5;
VideoFrame video_frame = 6;
LoginRequest login_request = 7;
LoginResponse login_response = 8;
Hash hash = 9;
MouseEvent mouse_event = 10;
AudioFrame audio_frame = 11;
CursorData cursor_data = 12;
CursorPosition cursor_position = 13;
uint64 cursor_id = 14;
KeyEvent key_event = 15;
Clipboard clipboard = 16;
FileAction file_action = 17;
FileResponse file_response = 18;
Misc misc = 19;
}
}

View File

@@ -0,0 +1,133 @@
syntax = "proto3";
package hbb;
message RegisterPeer {
string id = 1;
int32 serial = 2;
}
message RegisterPeerResponse { bool request_pk = 2; }
message PunchHoleRequest {
string id = 1;
NatType nat_type = 2;
}
message PunchHole {
bytes socket_addr = 1;
string relay_server = 2;
NatType nat_type = 3;
}
message TestNatRequest {
int32 serial = 1;
}
// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative
message TestNatResponse {
int32 port = 1;
ConfigUpdate cu = 2; // for mobile
}
enum NatType {
UNKNOWN_NAT = 0;
ASYMMETRIC = 1;
SYMMETRIC = 2;
}
message PunchHoleSent {
bytes socket_addr = 1;
string id = 2;
string relay_server = 3;
NatType nat_type = 4;
}
message RegisterPk {
string id = 1;
bytes uuid = 2;
bytes pk = 3;
}
message RegisterPkResponse {
enum Result {
OK = 1;
UUID_MISMATCH = 2;
}
Result result = 1;
}
message PunchHoleResponse {
bytes socket_addr = 1;
bytes pk = 2;
enum Failure {
ID_NOT_EXIST = 1;
OFFLINE = 2;
}
Failure failure = 3;
string relay_server = 4;
oneof union {
NatType nat_type = 5;
bool is_local = 6;
}
}
message ConfigUpdate {
int32 serial = 1;
repeated string rendezvous_servers = 2;
}
message RequestRelay {
string id = 1;
string uuid = 2;
bytes socket_addr = 3;
string relay_server = 4;
bool secure = 5;
}
message RelayResponse {
bytes socket_addr = 1;
string uuid = 2;
string relay_server = 3;
oneof union {
string id = 4;
bytes pk = 5;
}
}
message SoftwareUpdate { string url = 1; }
// if in same intranet, punch hole won't work both for udp and tcp,
// even some router has below connection error if we connect itself,
// { kind: Other, error: "could not resolve to any address" },
// so we request local address to connect.
message FetchLocalAddr {
bytes socket_addr = 1;
string relay_server = 2;
}
message LocalAddr {
bytes socket_addr = 1;
bytes local_addr = 2;
string relay_server = 3;
}
message RendezvousMessage {
oneof union {
RegisterPeer register_peer = 6;
RegisterPeerResponse register_peer_response = 7;
PunchHoleRequest punch_hole_request = 8;
PunchHole punch_hole = 9;
PunchHoleSent punch_hole_sent = 10;
PunchHoleResponse punch_hole_response = 11;
FetchLocalAddr fetch_local_addr = 12;
LocalAddr local_addr = 13;
ConfigUpdate configure_update = 14;
RegisterPk register_pk = 15;
RegisterPkResponse register_pk_response = 16;
SoftwareUpdate software_update = 17;
RequestRelay request_relay = 18;
RelayResponse relay_response = 19;
TestNatRequest test_nat_request = 20;
TestNatResponse test_nat_response = 21;
}
}

View File

@@ -0,0 +1,274 @@
use bytes::{Buf, BufMut, Bytes, BytesMut};
use std::io;
use tokio_util::codec::{Decoder, Encoder};
#[derive(Debug, Clone, Copy)]
pub struct BytesCodec {
state: DecodeState,
raw: bool,
max_packet_length: usize,
}
#[derive(Debug, Clone, Copy)]
enum DecodeState {
Head,
Data(usize),
}
impl BytesCodec {
pub fn new() -> Self {
Self {
state: DecodeState::Head,
raw: false,
max_packet_length: usize::MAX,
}
}
pub fn set_raw(&mut self) {
self.raw = true;
}
pub fn set_max_packet_length(&mut self, n: usize) {
self.max_packet_length = n;
}
fn decode_head(&mut self, src: &mut BytesMut) -> io::Result<Option<usize>> {
if src.is_empty() {
return Ok(None);
}
let head_len = ((src[0] & 0x3) + 1) as usize;
if src.len() < head_len {
return Ok(None);
}
let mut n = src[0] as usize;
if head_len > 1 {
n |= (src[1] as usize) << 8;
}
if head_len > 2 {
n |= (src[2] as usize) << 16;
}
if head_len > 3 {
n |= (src[3] as usize) << 24;
}
n >>= 2;
if n > self.max_packet_length {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet"));
}
src.advance(head_len);
src.reserve(n);
return Ok(Some(n));
}
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
if src.len() < n {
return Ok(None);
}
Ok(Some(src.split_to(n)))
}
}
impl Decoder for BytesCodec {
type Item = BytesMut;
type Error = io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<BytesMut>, io::Error> {
if self.raw {
if !src.is_empty() {
let len = src.len();
return Ok(Some(src.split_to(len)));
} else {
return Ok(None);
}
}
let n = match self.state {
DecodeState::Head => match self.decode_head(src)? {
Some(n) => {
self.state = DecodeState::Data(n);
n
}
None => return Ok(None),
},
DecodeState::Data(n) => n,
};
match self.decode_data(n, src)? {
Some(data) => {
self.state = DecodeState::Head;
Ok(Some(data))
}
None => Ok(None),
}
}
}
impl Encoder<Bytes> for BytesCodec {
type Error = io::Error;
fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> {
if self.raw {
buf.reserve(data.len());
buf.put(data);
return Ok(());
}
if data.len() <= 0x3F {
buf.put_u8((data.len() << 2) as u8);
} else if data.len() <= 0x3FFF {
buf.put_u16_le((data.len() << 2) as u16 | 0x1);
} else if data.len() <= 0x3FFFFF {
let h = (data.len() << 2) as u32 | 0x2;
buf.put_u16_le((h & 0xFFFF) as u16);
buf.put_u8((h >> 16) as u8);
} else if data.len() <= 0x3FFFFFFF {
buf.put_u32_le((data.len() << 2) as u32 | 0x3);
} else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
}
buf.extend(data);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec1() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3F, 1);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3F + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
assert!(false);
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
}
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
}
buf2.extend(&buf_saved[1..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
assert!(false);
}
}
#[test]
fn test_codec2() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
assert!(!codec.encode("".into(), &mut buf).is_err());
assert_eq!(buf.len(), 1);
bytes.resize(0x3F + 1, 2);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert_eq!(buf.len(), 0x3F + 2 + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0);
} else {
assert!(false);
}
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F + 1);
assert_eq!(res[0], 2);
} else {
assert!(false);
}
}
#[test]
fn test_codec3() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3F - 1, 3);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert_eq!(buf.len(), 0x3F + 1 - 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F - 1);
assert_eq!(res[0], 3);
} else {
assert!(false);
}
}
#[test]
fn test_codec4() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFF, 4);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert_eq!(buf.len(), 0x3FFF + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFF);
assert_eq!(res[0], 4);
} else {
assert!(false);
}
}
#[test]
fn test_codec5() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFFFF, 5);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert_eq!(buf.len(), 0x3FFFFF + 3);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF);
assert_eq!(res[0], 5);
} else {
assert!(false);
}
}
#[test]
fn test_codec6() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFFFF + 1, 6);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3FFFFF + 4 + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
assert!(false);
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
}
buf2.extend(&buf_saved[1..6]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
}
buf2.extend(&buf_saved[6..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
assert!(false);
}
}
}

View File

@@ -0,0 +1,50 @@
use std::cell::RefCell;
use zstd::block::{Compressor, Decompressor};
thread_local! {
static COMPRESSOR: RefCell<Compressor> = RefCell::new(Compressor::new());
static DECOMPRESSOR: RefCell<Decompressor> = RefCell::new(Decompressor::new());
}
/// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
/// which is currently 22. Levels >= 20
/// Default level is ZSTD_CLEVEL_DEFAULT==3.
/// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
pub fn compress(data: &[u8], level: i32) -> Vec<u8> {
let mut out = Vec::new();
COMPRESSOR.with(|c| {
if let Ok(mut c) = c.try_borrow_mut() {
match c.compress(data, level) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to compress: {}", err);
}
}
}
});
out
}
pub fn decompress(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
DECOMPRESSOR.with(|d| {
if let Ok(mut d) = d.try_borrow_mut() {
const MAX: usize = 1024 * 1024 * 64;
const MIN: usize = 1024 * 1024;
let mut n = 30 * data.len();
if n > MAX {
n = MAX;
}
if n < MIN {
n = MIN;
}
match d.decompress(data, n) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to decompress: {}", err);
}
}
}
});
out
}

View File

@@ -0,0 +1,688 @@
use crate::log;
use directories_next::ProjectDirs;
use rand::Rng;
use serde_derive::{Deserialize, Serialize};
use sodiumoxide::crypto::sign;
use std::{
collections::HashMap,
fs,
net::SocketAddr,
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
time::SystemTime,
};
pub const APP_NAME: &str = "RustDesk";
pub const BIND_INTERFACE: &str = "0.0.0.0";
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
pub const CONNECT_TIMEOUT: u64 = 18_000;
pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 0;
// 128x128
#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII=
";
#[cfg(windows)] // windows, 32x32, bigger very ugly after shrink
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAolBMVEUAAAAAcf8Acf8Acf8AcP8Acf8Acf8Acf8AcP8Acf/////9/v/7/f+gyv9wr/8Ld/8GdP/A3P+nzv+Qwf9mqv8lhv8UfP+Kvv88k/8zjv/x+P/Y6f/U5//J4f/F3/+x1P+pz/+izP+Cuv9Zov9Qnv9FmP8gg//f7f+42P96tv9fpv8ui//1+f/q8//o8v/Q5f+ax/+Zxv+VxP+Fu/9rrf8rif+x1o3FAAAACXRSTlMAv/RPTPKHioRsIqhAAAABNklEQVQ4y4WT6XaCQAyFB7S2GUD2RaxSKGDV7sv7v1on4djEluL9Ncn5biaZk1FKzSwbRmVbM2V0DRNaGD9Maq6sacBS9jRwpUTw1Ww7SEOvkwQDaeVofQeJ1nrXjgD3pTaqCDCg/xs4OBrlwUGTHvxzoHAx69Y9+Hk5oGdAsCNXSoHfEN1JYEme4KcfvO9WAmtMBNx6jIaCgSPGLbAyvKRhAEcrAxAKTSZiACu+gNSbyWwZeKdQKj91yRXWIOWZTMgATun6EtjgczNQaKMEWD0+xJ6B4AnvECVqdKwYoJ50hIPyw25AANkzpsLB00cYOIUEoHU0uVaQVcMxBwlgVZJ3Orz+3ahH6gP2tBgfYzsZmdo1fGIzRx5Irn2WxKaHeJnKtb/4cS5/PbWACd0oo/n/39/4vwGFYSxtSYV4OAAAAABJRU5ErkJggg==
";
#[cfg(target_os = "linux")] // 128x128 no padding
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA7VBMVEUAAAAAcf8Acf8Acf8Adf8Acf8Acf8AcP8Acv8AcP8Acf8Acf8Acf8Acv8Acf8Acf8Ab/8AcP8Acf8Acf8Acf/////7/f8Dc/8TfP/1+f/n8v9Hmf/u9v+Uw//Q5f9hp/8Yfv8Qev8Ld/+52P+z1f+s0f81j/8wjP8Hdf/3+/8mh/8fg//x9//h7//H4P9xsP9rrf9oq/8rif/r9P/D3v+92/+Duv9bpP/d7f/U5/9NnP8/lP8jhP/L4v/B3P+OwP9+t/95tf9Rn/8bgf/Z6v+Zx/90sv9lqf85kf+hy/9UoP+Wxf+kzP+dyP+Lvv/H4q8IAAAAFHRSTlMA+u6bB6x5XR4V0+S4i4k5N+a81W8MiAQAAAVcSURBVHjazdvpWtpAGIbhgEutdW3fL2GHsMsiq4KI+66t5384XahF/GbizJAy3j/1Ah5CJhNCxpm1vbryLRrBfxKJrq+sbjtSa5u7WIDdzTVH5PNSBAsSWfrsMJ+iWKDoJ2fW8hIWbGl55vW/YuE2XhUsb8CCr9OCJVix9G//gyWf/o6/KCyJfrbwAfAPYS0CayK/j4mbsGjrV8AXWLTrONuwasdZhVWrzgqsWnG+wap1Jwqrok4EVkUcmKhdVvBaOVnzYEY/oJpMD4mo6ONF/ZSIUsX2FZjQA7xRqUET+y/v2W/Sy59u62DCDMgdJmhqgIk7eqWQBBNWwPhmj147w8QTzTjKVsGEEBBLuzSrhIkivTF8DD/Aa6forQNMHBD/VyXkgHGfuBN5ALln1TADOnESyGCiT8L/1kILqD6Q0BEm9kkofhdSwNUJiV1jQvZ/SnthBNSaJJGZbgGJUnX+gEqCZPpsJ2T2Y/MGVBrE8eOAvCA/X8A4QXLnmEhTgIPqPAG5IQU4fhmkFOT7HAFenwIU8Jd/TUEODQIUtu1eOj/dUD9cknOTpgEDkup3YrOfVStDUomcWcBVisTiNxVw3TPpgCl4RgFFybZ/9iHmn8uS2yYBA8m7qUEu9oOEejH9gHxC+PazCHbcFM8K+gGHJNAs4z2xgnAkVHQDcnG1IzvnCSfvom7AM3EZ9voah4+KXoAvGFJHMSgqEfegF3BBTKoOVfkMMXFfJ8AT7MuXUDeOE9PWCUiKBpKOlmAP1gngH2LChw7vhJgr9YD8Hnt0BxrE27CtHnDJR4AHTX1+KFAP4Ef0LHTxN9HwlAMSbAjmoavKZ8ayakDXYAhwN3wzqgZk2UPvwRjshmeqATeCT09f3mWnEqoBGf4NxAB/moRqADuOtmDiid6KqQVcsQeOYOKW3uqqBRwL5nITj/yrlFpAVrDpTJT5llQLaLMHwshY7UDgvD+VujDC96WWWsBtSAE5FnChFnAeUkDMdAvw88EqTNT5SYXpTlgPaRQM1AIGorkolNnoUS1gJHigCX48SaoF3Asuspg4Mz0U8+FTgIkCG01V09kwBQP8xG5ofD5AXeirkPEJSUlwSVIfP5ykVQNaggvz+k7prTvVgDKF8BnUXP4kqgEe/257E8Ig7EE1gA8g2stBTz7FLxqrB3SIeYaeQ2IG6gE5l2+Cmt5MGOfP4KsGiH8DOYWOoujnDY2ALHF3810goZFOQDVBTFx9Uj7eI6bp6QTgnLjeGGq6KeJuoRUQixN3pDYWyz1Rva8XIL5UPFQZCsmG3gV7R+dieS+Jd3iHLglce7oBuCOhp3zwHLxPQpfQDvBOSKjZqUIml3ZJ6AD6AajFSZJwewWR8ZPsEY26SQDaJOMeZP23w6bTJ6kBjAJQILm9hzqm7otu4G+nhgGxIQUlPLKzL7GhbxqAboMCuN2XXd+lAL0ajAMwclV+FD6jAPEy5ghAlhfwX2FODX445gHKxyN++fs64PUHmDMAbbYN2DlKk2QaScwdgMs4SZxMv4OJJSoIIQBl2Qtk3gk4qiOUANRPJQHB+0A6j5AC4J27QQEZ4eZPAsYBXFk0N/YD7iUrxRBqALxOTzoMC3x8lCFlfkMjuz8iLfk6fzQCQgjg8q3ZEd8RzUVuKelBh96Nzcc3qelL1V+2zfRv1xc56Ino3tpdPT7cd//MspfTrD/7R6p4W4O2qLMObfnyIHvvYcrPtkZjDybW7d/eb32Bg/UlHnYXuXz5CMt8rC90sr7Uy/5iN+vL/ewveLS/5NNKwcbyR1r2a3/h8wdY+v3L2tZC5oUvW2uO1M7qyvp/Xv6/48z4CTxjJEfyjEaMAAAAAElFTkSuQmCC
";
#[cfg(target_os = "macos")]
pub const ORG: &str = "com.carriez";
type Size = (i32, i32, i32, i32);
lazy_static::lazy_static! {
static ref CONFIG: Arc<RwLock<Config>> = Arc::new(RwLock::new(Config::load()));
static ref CONFIG2: Arc<RwLock<Config2>> = Arc::new(RwLock::new(Config2::load()));
pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = Default::default();
}
#[cfg(any(target_os = "android", target_os = "ios"))]
lazy_static::lazy_static! {
pub static ref APP_DIR: Arc<RwLock<String>> = Default::default();
}
const CHARS: &'static [char] = &[
'2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
];
pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[
"rs-sg.rustdesk.com",
"rs-cn.rustdesk.com",
];
pub const RENDEZVOUS_PORT: i32 = 21116;
pub const RELAY_PORT: i32 = 21117;
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Config {
#[serde(default)]
id: String,
#[serde(default)]
password: String,
#[serde(default)]
salt: String,
#[serde(default)]
key_pair: (Vec<u8>, Vec<u8>), // sk, pk
#[serde(default)]
key_confirmed: bool,
#[serde(default)]
keys_confirmed: HashMap<String, bool>,
}
// more variable configs
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Config2 {
#[serde(default)]
remote_id: String, // latest used one
#[serde(default)]
size: Size,
#[serde(default)]
rendezvous_server: String,
#[serde(default)]
nat_type: i32,
#[serde(default)]
serial: i32,
// the other scalar value must before this
#[serde(default)]
pub options: HashMap<String, String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct PeerConfig {
#[serde(default)]
pub password: Vec<u8>,
#[serde(default)]
pub size: Size,
#[serde(default)]
pub size_ft: Size,
#[serde(default)]
pub size_pf: Size,
#[serde(default)]
pub view_style: String, // original (default), scale
#[serde(default)]
pub image_quality: String,
#[serde(default)]
pub custom_image_quality: Vec<i32>,
#[serde(default)]
pub show_remote_cursor: bool,
#[serde(default)]
pub lock_after_session_end: bool,
#[serde(default)]
pub privacy_mode: bool,
#[serde(default)]
pub port_forwards: Vec<(i32, String, i32)>,
#[serde(default)]
pub direct_failures: i32,
#[serde(default)]
pub disable_audio: bool,
#[serde(default)]
pub disable_clipboard: bool,
// the other scalar value must before this
#[serde(default)]
pub options: HashMap<String, String>,
#[serde(default)]
pub info: PeerInfoSerde,
}
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
pub struct PeerInfoSerde {
#[serde(default)]
pub username: String,
#[serde(default)]
pub hostname: String,
#[serde(default)]
pub platform: String,
}
fn patch(path: PathBuf) -> PathBuf {
if let Some(_tmp) = path.to_str() {
#[cfg(windows)]
return _tmp
.replace(
"system32\\config\\systemprofile",
"ServiceProfiles\\LocalService",
)
.into();
#[cfg(target_os = "macos")]
return _tmp.replace("Application Support", "Preferences").into();
}
path
}
impl Config2 {
fn load() -> Config2 {
Config::load_::<Config2>("2")
}
fn store(&self) {
Config::store_(self, "2");
}
}
impl Config {
fn load_<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
suffix: &str,
) -> T {
let file = Self::file_(suffix);
log::debug!("Configuration path: {}", file.display());
let cfg = match confy::load_path(&file) {
Ok(config) => config,
Err(err) => {
log::error!("Failed to load config: {}", err);
T::default()
}
};
if suffix.is_empty() {
log::debug!("{:?}", cfg);
}
cfg
}
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
let file = Self::file_(suffix);
if let Err(err) = confy::store_path(file, config) {
log::error!("Failed to store config: {}", err);
}
}
fn load() -> Config {
Config::load_::<Config>("")
}
fn store(&self) {
Config::store_(self, "");
}
pub fn file() -> PathBuf {
Self::file_("")
}
pub fn import(from: &str) {
log::info!("import {}", from);
// load first to create path
Self::load();
crate::allow_err!(std::fs::copy(from, Self::file()));
crate::allow_err!(std::fs::copy(
from.replace(".toml", "2.toml"),
Self::file_("2")
));
}
pub fn save_tmp() -> String {
let _lock = CONFIG.read().unwrap(); // do not use let _, which will be dropped immediately
let path = Self::file_("2").to_str().unwrap_or("").to_owned();
let path2 = format!("{}_tmp", path);
crate::allow_err!(std::fs::copy(&path, &path2));
let path = Self::file().to_str().unwrap_or("").to_owned();
let path2 = format!("{}_tmp", path);
crate::allow_err!(std::fs::copy(&path, &path2));
path2
}
fn file_(suffix: &str) -> PathBuf {
let name = format!("{}{}", APP_NAME, suffix);
Self::path(name).with_extension("toml")
}
pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Self::path("");
if let Some(path) = dirs_next::home_dir() {
patch(path)
} else if let Ok(path) = std::env::current_dir() {
path
} else {
std::env::temp_dir()
}
}
fn path<P: AsRef<Path>>(p: P) -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let mut path: PathBuf = APP_DIR.read().unwrap().clone().into();
path.push(p);
return path;
}
#[cfg(not(target_os = "macos"))]
let org = "";
#[cfg(target_os = "macos")]
let org = ORG;
// /var/root for root
if let Some(project) = ProjectDirs::from("", org, APP_NAME) {
let mut path = patch(project.config_dir().to_path_buf());
path.push(p);
return path;
}
return "".into();
}
pub fn log_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
if let Some(path) = dirs_next::home_dir().as_mut() {
path.push(format!("Library/Logs/{}", APP_NAME));
return path.clone();
}
}
#[cfg(target_os = "linux")]
{
if let Some(path) = dirs_next::home_dir().as_mut() {
path.push(format!(".local/share/logs/{}", APP_NAME));
std::fs::create_dir_all(&path).ok();
return path.clone();
}
}
if let Some(path) = Self::path("").parent() {
let mut path: PathBuf = path.into();
path.push("log");
return path;
}
"".into()
}
pub fn ipc_path(postfix: &str) -> String {
#[cfg(windows)]
{
// \\ServerName\pipe\PipeName
// where ServerName is either the name of a remote computer or a period, to specify the local computer.
// https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names
format!("\\\\.\\pipe\\{}\\query{}", APP_NAME, postfix)
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
let mut path: PathBuf = format!("/tmp/{}", APP_NAME).into();
fs::create_dir(&path).ok();
fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok();
path.push(format!("ipc{}", postfix));
path.to_str().unwrap_or("").to_owned()
}
}
pub fn icon_path() -> PathBuf {
let mut path = Self::path("icons");
if fs::create_dir_all(&path).is_err() {
path = std::env::temp_dir();
}
path
}
#[inline]
pub fn get_any_listen_addr() -> SocketAddr {
format!("{}:0", BIND_INTERFACE).parse().unwrap()
}
pub fn get_rendezvous_server() -> SocketAddr {
let mut rendezvous_server = Self::get_option("custom-rendezvous-server");
if rendezvous_server.is_empty() {
rendezvous_server = CONFIG2.write().unwrap().rendezvous_server.clone();
}
if rendezvous_server.is_empty() {
rendezvous_server = Self::get_rendezvous_servers()
.drain(..)
.next()
.unwrap_or("".to_owned());
}
if !rendezvous_server.contains(":") {
rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT);
}
if let Ok(addr) = crate::to_socket_addr(&rendezvous_server) {
addr
} else {
Self::get_any_listen_addr()
}
}
pub fn get_rendezvous_servers() -> Vec<String> {
let s = Self::get_option("custom-rendezvous-server");
if !s.is_empty() {
return vec![s];
}
let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL;
if serial_obsolute {
let ss: Vec<String> = Self::get_option("rendezvous-servers")
.split(",")
.filter(|x| x.contains("."))
.map(|x| x.to_owned())
.collect();
if !ss.is_empty() {
return ss;
}
}
return RENDEZVOUS_SERVERS.iter().map(|x| x.to_string()).collect();
}
pub fn reset_online() {
*ONLINE.lock().unwrap() = Default::default();
}
pub fn update_latency(host: &str, latency: i64) {
ONLINE.lock().unwrap().insert(host.to_owned(), latency);
let mut host = "".to_owned();
let mut delay = i64::MAX;
for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() {
if tmp_delay > &0 && tmp_delay < &delay {
delay = tmp_delay.clone();
host = tmp_host.to_string();
}
}
if !host.is_empty() {
let mut config = CONFIG2.write().unwrap();
if host != config.rendezvous_server {
log::debug!("Update rendezvous_server in config to {}", host);
log::debug!("{:?}", *ONLINE.lock().unwrap());
config.rendezvous_server = host;
config.store();
}
}
}
pub fn set_id(id: &str) {
let mut config = CONFIG.write().unwrap();
if id == config.id {
return;
}
config.id = id.into();
config.store();
}
pub fn set_nat_type(nat_type: i32) {
let mut config = CONFIG2.write().unwrap();
if nat_type == config.nat_type {
return;
}
config.nat_type = nat_type;
config.store();
}
pub fn get_nat_type() -> i32 {
CONFIG2.read().unwrap().nat_type
}
pub fn set_serial(serial: i32) {
let mut config = CONFIG2.write().unwrap();
if serial == config.serial {
return;
}
config.serial = serial;
config.store();
}
pub fn get_serial() -> i32 {
std::cmp::max(CONFIG2.read().unwrap().serial, SERIAL)
}
fn get_auto_id() -> Option<String> {
#[cfg(any(target_os = "android", target_os = "ios"))]
return None;
let mut id = 0u32;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(Some(ma)) = mac_address::get_mac_address() {
for x in &ma.bytes()[2..] {
id = (id << 8) | (*x as u32);
}
id = id & 0x1FFFFFFF;
Some(id.to_string())
} else {
None
}
}
pub fn get_auto_password() -> String {
let mut rng = rand::thread_rng();
(0..6)
.map(|_| CHARS[rng.gen::<usize>() % CHARS.len()])
.collect()
}
pub fn get_key_confirmed() -> bool {
CONFIG.read().unwrap().key_confirmed
}
pub fn set_key_confirmed(v: bool) {
let mut config = CONFIG.write().unwrap();
if config.key_confirmed == v {
return;
}
config.key_confirmed = v;
if !v {
config.keys_confirmed = Default::default();
}
config.store();
}
pub fn get_host_key_confirmed(host: &str) -> bool {
if let Some(true) = CONFIG.read().unwrap().keys_confirmed.get(host) {
true
} else {
false
}
}
pub fn set_host_key_confirmed(host: &str, v: bool) {
if Self::get_host_key_confirmed(host) == v {
return;
}
let mut config = CONFIG.write().unwrap();
config.keys_confirmed.insert(host.to_owned(), v);
config.store();
}
pub fn set_key_pair(pair: (Vec<u8>, Vec<u8>)) {
let mut config = CONFIG.write().unwrap();
config.key_pair = pair;
config.store();
}
pub fn get_key_pair() -> (Vec<u8>, Vec<u8>) {
// lock here to make sure no gen_keypair more than once
let mut config = CONFIG.write().unwrap();
if config.key_pair.0.is_empty() {
let (pk, sk) = sign::gen_keypair();
config.key_pair = (sk.0.to_vec(), pk.0.into());
config.store();
}
config.key_pair.clone()
}
pub fn get_id() -> String {
let mut id = CONFIG.read().unwrap().id.clone();
if id.is_empty() {
if let Some(tmp) = Config::get_auto_id() {
id = tmp;
Config::set_id(&id);
}
}
id
}
pub fn get_options() -> HashMap<String, String> {
CONFIG2.read().unwrap().options.clone()
}
pub fn set_options(v: HashMap<String, String>) {
let mut config = CONFIG2.write().unwrap();
config.options = v;
config.store();
}
pub fn get_option(k: &str) -> String {
if let Some(v) = CONFIG2.read().unwrap().options.get(k) {
v.clone()
} else {
"".to_owned()
}
}
pub fn set_option(k: String, v: String) {
let mut config = CONFIG2.write().unwrap();
if k == "custom-rendezvous-server" {
config.rendezvous_server = "".to_owned();
}
let v2 = if v.is_empty() { None } else { Some(&v) };
if v2 != config.options.get(&k) {
if v2.is_none() {
config.options.remove(&k);
} else {
config.options.insert(k, v);
}
config.store();
}
}
pub fn update_id() {
// to-do: how about if one ip register a lot of ids?
let id = Self::get_id();
let mut rng = rand::thread_rng();
let new_id = rng.gen_range(1_000_000_000, 2_000_000_000).to_string();
Config::set_id(&new_id);
log::info!("id updated from {} to {}", id, new_id);
}
pub fn set_password(password: &str) {
let mut config = CONFIG.write().unwrap();
if password == config.password {
return;
}
config.password = password.into();
config.store();
}
pub fn get_password() -> String {
let mut password = CONFIG.read().unwrap().password.clone();
if password.is_empty() {
password = Config::get_auto_password();
Config::set_password(&password);
}
password
}
pub fn set_salt(salt: &str) {
let mut config = CONFIG.write().unwrap();
if salt == config.salt {
return;
}
config.salt = salt.into();
config.store();
}
pub fn get_salt() -> String {
let mut salt = CONFIG.read().unwrap().salt.clone();
if salt.is_empty() {
salt = Config::get_auto_password();
Config::set_salt(&salt);
}
salt
}
pub fn get_size() -> Size {
CONFIG2.read().unwrap().size
}
pub fn set_size(x: i32, y: i32, w: i32, h: i32) {
let mut config = CONFIG2.write().unwrap();
let size = (x, y, w, h);
if size == config.size || size.2 < 300 || size.3 < 300 {
return;
}
config.size = size;
config.store();
}
pub fn set_remote_id(remote_id: &str) {
let mut config = CONFIG2.write().unwrap();
if remote_id == config.remote_id {
return;
}
config.remote_id = remote_id.into();
config.store();
}
pub fn get_remote_id() -> String {
CONFIG2.read().unwrap().remote_id.clone()
}
}
const PEERS: &str = "peers";
impl PeerConfig {
pub fn load(id: &str) -> PeerConfig {
let _ = CONFIG.read().unwrap(); // for lock
match confy::load_path(&Self::path(id)) {
Ok(config) => config,
Err(err) => {
log::error!("Failed to load config: {}", err);
Default::default()
}
}
}
pub fn store(&self, id: &str) {
let _ = CONFIG.read().unwrap(); // for lock
if let Err(err) = confy::store_path(Self::path(id), self) {
log::error!("Failed to store config: {}", err);
}
}
pub fn remove(id: &str) {
fs::remove_file(&Self::path(id)).ok();
}
fn path(id: &str) -> PathBuf {
let path: PathBuf = [PEERS, id].iter().collect();
Config::path(path).with_extension("toml")
}
pub fn peers() -> Vec<(String, SystemTime, PeerInfoSerde)> {
if let Ok(peers) = Config::path(PEERS).read_dir() {
if let Ok(peers) = peers
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, _>>()
{
let mut peers: Vec<_> = peers
.iter()
.filter(|p| {
p.is_file()
&& p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml")
})
.map(|p| {
let t = fs::metadata(p)
.map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH))
.unwrap_or(SystemTime::UNIX_EPOCH);
let id = p
.file_stem()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned();
let info = PeerConfig::load(&id).info;
if info.platform.is_empty() {
fs::remove_file(&p).ok();
}
(id, t, info)
})
.filter(|p| !p.2.platform.is_empty())
.collect();
peers.sort_unstable_by(|a, b| b.1.cmp(&a.1));
return peers;
}
}
Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize() {
let cfg: Config = Default::default();
let res = toml::to_string_pretty(&cfg);
assert!(res.is_ok());
let cfg: PeerConfig = Default::default();
let res = toml::to_string_pretty(&cfg);
assert!(res.is_ok());
}
}

554
libs/hbb_common/src/fs.rs Normal file
View File

@@ -0,0 +1,554 @@
use crate::{bail, message_proto::*, ResultType};
use std::path::{Path, PathBuf};
// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
use crate::{
compress::{compress, decompress},
config::{Config, COMPRESS_LEVEL},
};
#[cfg(windows)]
use std::os::windows::prelude::*;
use tokio::{fs::File, prelude::*};
pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType<FileDirectory> {
let mut dir = FileDirectory {
path: get_string(&path),
..Default::default()
};
#[cfg(windows)]
if "/" == &get_string(&path) {
let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() };
for i in 0..32 {
if drives & (1 << i) != 0 {
let name = format!(
"{}:",
std::char::from_u32('A' as u32 + i as u32).unwrap_or('A')
);
dir.entries.push(FileEntry {
name,
entry_type: FileType::DirDrive.into(),
..Default::default()
});
}
}
return Ok(dir);
}
for entry in path.read_dir()? {
if let Ok(entry) = entry {
let p = entry.path();
let name = p
.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned();
if name.is_empty() {
continue;
}
let mut is_hidden = false;
let meta;
if let Ok(tmp) = std::fs::symlink_metadata(&p) {
meta = tmp;
} else {
continue;
}
// docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
#[cfg(windows)]
if meta.file_attributes() & 0x2 != 0 {
is_hidden = true;
}
#[cfg(not(windows))]
if name.find('.').unwrap_or(usize::MAX) == 0 {
is_hidden = true;
}
if is_hidden && !include_hidden {
continue;
}
let (entry_type, size) = {
if p.is_dir() {
if meta.file_type().is_symlink() {
(FileType::DirLink.into(), 0)
} else {
(FileType::Dir.into(), 0)
}
} else {
if meta.file_type().is_symlink() {
(FileType::FileLink.into(), 0)
} else {
(FileType::File.into(), meta.len())
}
}
};
let modified_time = meta
.modified()
.map(|x| {
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0) as u64;
dir.entries.push(FileEntry {
name: get_file_name(&p),
entry_type,
is_hidden,
size,
modified_time,
..Default::default()
});
}
}
Ok(dir)
}
#[inline]
pub fn get_file_name(p: &PathBuf) -> String {
p.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned()
}
#[inline]
pub fn get_string(path: &PathBuf) -> String {
path.to_str().unwrap_or("").to_owned()
}
#[inline]
pub fn get_path(path: &str) -> PathBuf {
Path::new(path).to_path_buf()
}
#[inline]
pub fn get_home_as_string() -> String {
get_string(&Config::get_home())
}
fn read_dir_recursive(
path: &PathBuf,
prefix: &PathBuf,
include_hidden: bool,
) -> ResultType<Vec<FileEntry>> {
let mut files = Vec::new();
if path.is_dir() {
// to-do: symbol link handling, cp the link rather than the content
// to-do: file mode, for unix
let fd = read_dir(&path, include_hidden)?;
for entry in fd.entries.iter() {
match entry.entry_type.enum_value() {
Ok(FileType::File) => {
let mut entry = entry.clone();
entry.name = get_string(&prefix.join(entry.name));
files.push(entry);
}
Ok(FileType::Dir) => {
if let Ok(mut tmp) = read_dir_recursive(
&path.join(&entry.name),
&prefix.join(&entry.name),
include_hidden,
) {
for entry in tmp.drain(0..) {
files.push(entry);
}
}
}
_ => {}
}
}
Ok(files)
} else if path.is_file() {
let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) {
(
meta.len(),
meta.modified()
.map(|x| {
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0) as u64,
)
} else {
(0, 0)
};
files.push(FileEntry {
entry_type: FileType::File.into(),
size,
modified_time,
..Default::default()
});
Ok(files)
} else {
bail!("Not exists");
}
}
pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<FileEntry>> {
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
}
#[derive(Default)]
pub struct TransferJob {
id: i32,
path: PathBuf,
files: Vec<FileEntry>,
file_num: i32,
file: Option<File>,
total_size: u64,
finished_size: u64,
transfered: u64,
}
#[inline]
fn get_ext(name: &str) -> &str {
if let Some(i) = name.rfind(".") {
return &name[i + 1..];
}
""
}
#[inline]
fn is_compressed_file(name: &str) -> bool {
let ext = get_ext(name);
ext == "xz"
|| ext == "gz"
|| ext == "zip"
|| ext == "7z"
|| ext == "rar"
|| ext == "bz2"
|| ext == "tgz"
|| ext == "png"
|| ext == "jpg"
}
impl TransferJob {
pub fn new_write(id: i32, path: String, files: Vec<FileEntry>) -> Self {
let total_size = files.iter().map(|x| x.size as u64).sum();
Self {
id,
path: get_path(&path),
files,
total_size,
..Default::default()
}
}
pub fn new_read(id: i32, path: String, include_hidden: bool) -> ResultType<Self> {
let files = get_recursive_files(&path, include_hidden)?;
let total_size = files.iter().map(|x| x.size as u64).sum();
Ok(Self {
id,
path: get_path(&path),
files,
total_size,
..Default::default()
})
}
#[inline]
pub fn files(&self) -> &Vec<FileEntry> {
&self.files
}
#[inline]
pub fn set_files(&mut self, files: Vec<FileEntry>) {
self.files = files;
}
#[inline]
pub fn id(&self) -> i32 {
self.id
}
#[inline]
pub fn total_size(&self) -> u64 {
self.total_size
}
#[inline]
pub fn finished_size(&self) -> u64 {
self.finished_size
}
#[inline]
pub fn transfered(&self) -> u64 {
self.transfered
}
#[inline]
pub fn file_num(&self) -> i32 {
self.file_num
}
pub fn modify_time(&self) {
let file_num = self.file_num as usize;
if file_num < self.files.len() {
let entry = &self.files[file_num];
let path = self.join(&entry.name);
let download_path = format!("{}.download", get_string(&path));
std::fs::rename(&download_path, &path).ok();
filetime::set_file_mtime(
&path,
filetime::FileTime::from_unix_time(entry.modified_time as _, 0),
)
.ok();
}
}
pub fn remove_download_file(&self) {
let file_num = self.file_num as usize;
if file_num < self.files.len() {
let entry = &self.files[file_num];
let path = self.join(&entry.name);
let download_path = format!("{}.download", get_string(&path));
std::fs::remove_file(&download_path).ok();
}
}
pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> {
if block.id != self.id {
bail!("Wrong id");
}
let file_num = block.file_num as usize;
if file_num >= self.files.len() {
bail!("Wrong file number");
}
if file_num != self.file_num as usize || self.file.is_none() {
self.modify_time();
if let Some(file) = self.file.as_mut() {
file.sync_all().await?;
}
self.file_num = block.file_num;
let entry = &self.files[file_num];
let path = self.join(&entry.name);
if let Some(p) = path.parent() {
std::fs::create_dir_all(p).ok();
}
let path = format!("{}.download", get_string(&path));
self.file = Some(File::create(&path).await?);
}
if block.compressed {
let tmp = decompress(&block.data);
self.file.as_mut().unwrap().write_all(&tmp).await?;
self.finished_size += tmp.len() as u64;
} else {
self.file.as_mut().unwrap().write_all(&block.data).await?;
self.finished_size += block.data.len() as u64;
}
self.transfered += block.data.len() as u64;
Ok(())
}
#[inline]
fn join(&self, name: &str) -> PathBuf {
if name.is_empty() {
self.path.clone()
} else {
self.path.join(name)
}
}
pub async fn read(&mut self) -> ResultType<Option<FileTransferBlock>> {
let file_num = self.file_num as usize;
if file_num >= self.files.len() {
self.file.take();
return Ok(None);
}
let name = &self.files[file_num].name;
if self.file.is_none() {
match File::open(self.join(&name)).await {
Ok(file) => {
self.file = Some(file);
}
Err(err) => {
self.file_num += 1;
return Err(err.into());
}
}
}
const BUF_SIZE: usize = 128 * 1024;
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
unsafe {
buf.set_len(BUF_SIZE);
}
let mut compressed = false;
let mut offset: usize = 0;
loop {
match self.file.as_mut().unwrap().read(&mut buf[offset..]).await {
Err(err) => {
self.file_num += 1;
self.file = None;
return Err(err.into());
}
Ok(n) => {
offset += n;
if n == 0 || offset == BUF_SIZE {
break;
}
}
}
}
unsafe { buf.set_len(offset) };
if offset == 0 {
self.file_num += 1;
self.file = None;
} else {
self.finished_size += offset as u64;
if !is_compressed_file(name) {
let tmp = compress(&buf, COMPRESS_LEVEL);
if tmp.len() < buf.len() {
buf = tmp;
compressed = true;
}
}
self.transfered += buf.len() as u64;
}
Ok(Some(FileTransferBlock {
id: self.id,
file_num: file_num as _,
data: buf.into(),
compressed,
..Default::default()
}))
}
}
#[inline]
pub fn new_error<T: std::string::ToString>(id: i32, err: T, file_num: i32) -> Message {
let mut resp = FileResponse::new();
resp.set_error(FileTransferError {
id,
error: err.to_string(),
file_num,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn new_dir(id: i32, files: Vec<FileEntry>) -> Message {
let mut resp = FileResponse::new();
resp.set_dir(FileDirectory {
id,
entries: files.into(),
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn new_block(block: FileTransferBlock) -> Message {
let mut resp = FileResponse::new();
resp.set_block(block);
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest {
id,
path,
files: files.into(),
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_action(action);
msg_out
}
#[inline]
pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message {
let mut action = FileAction::new();
action.set_send(FileTransferSendRequest {
id,
path,
include_hidden,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_action(action);
msg_out
}
#[inline]
pub fn new_done(id: i32, file_num: i32) -> Message {
let mut resp = FileResponse::new();
resp.set_done(FileTransferDone {
id,
file_num,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn remove_job(id: i32, jobs: &mut Vec<TransferJob>) {
*jobs = jobs.drain(0..).filter(|x| x.id() != id).collect();
}
#[inline]
pub fn get_job(id: i32, jobs: &mut Vec<TransferJob>) -> Option<&mut TransferJob> {
jobs.iter_mut().filter(|x| x.id() == id).next()
}
pub async fn handle_read_jobs(
jobs: &mut Vec<TransferJob>,
stream: &mut crate::Stream,
) -> ResultType<()> {
let mut finished = Vec::new();
for job in jobs.iter_mut() {
match job.read().await {
Err(err) => {
stream
.send(&new_error(job.id(), err, job.file_num()))
.await?;
}
Ok(Some(block)) => {
stream.send(&new_block(block)).await?;
}
Ok(None) => {
finished.push(job.id());
stream.send(&new_done(job.id(), job.file_num())).await?;
}
}
}
for id in finished {
remove_job(id, jobs);
}
Ok(())
}
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
let fd = read_dir(path, true)?;
for entry in fd.entries.iter() {
match entry.entry_type.enum_value() {
Ok(FileType::Dir) => {
remove_all_empty_dir(&path.join(&entry.name)).ok();
}
Ok(FileType::DirLink) | Ok(FileType::FileLink) => {
std::fs::remove_file(&path.join(&entry.name)).ok();
}
_ => {}
}
}
std::fs::remove_dir(path).ok();
Ok(())
}
#[inline]
pub fn remove_file(file: &str) -> ResultType<()> {
std::fs::remove_file(get_path(file))?;
Ok(())
}
#[inline]
pub fn create_dir(dir: &str) -> ResultType<()> {
std::fs::create_dir_all(get_path(dir))?;
Ok(())
}

215
libs/hbb_common/src/lib.rs Normal file
View File

@@ -0,0 +1,215 @@
pub mod compress;
#[path = "./protos/message.rs"]
pub mod message_proto;
#[path = "./protos/rendezvous.rs"]
pub mod rendezvous_proto;
pub use bytes;
pub use futures;
pub use protobuf;
use socket2::{Domain, Socket, Type};
use std::{
fs::File,
io::{self, BufRead},
net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs},
path::Path,
time::{self, SystemTime, UNIX_EPOCH},
};
pub use tokio;
pub use tokio_util;
pub mod tcp;
pub mod udp;
pub use env_logger;
pub use log;
pub mod bytes_codec;
#[cfg(feature = "quic")]
pub mod quic;
pub use anyhow::{self, bail};
pub use futures_util;
pub mod config;
pub mod fs;
pub use sodiumoxide;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;
#[cfg(not(feature = "quic"))]
pub type Stream = tcp::FramedStream;
#[inline]
pub async fn sleep(sec: f32) {
tokio::time::delay_for(time::Duration::from_secs_f32(sec)).await;
}
#[macro_export]
macro_rules! allow_err {
($e:expr) => {
if let Err(err) = $e {
log::debug!(
"{:?}, {}:{}:{}:{}",
err,
module_path!(),
file!(),
line!(),
column!()
);
} else {
}
};
}
#[inline]
pub fn timeout<T: std::future::Future>(ms: u64, future: T) -> tokio::time::Timeout<T> {
tokio::time::timeout(std::time::Duration::from_millis(ms), future)
}
fn new_socket(addr: SocketAddr, tcp: bool, reuse: bool) -> Result<Socket, std::io::Error> {
let stype = {
if tcp {
Type::stream()
} else {
Type::dgram()
}
};
let socket = match addr {
SocketAddr::V4(..) => Socket::new(Domain::ipv4(), stype, None),
SocketAddr::V6(..) => Socket::new(Domain::ipv6(), stype, None),
}?;
if reuse {
// windows has no reuse_port, but it's reuse_address
// almost equals to unix's reuse_port + reuse_address,
// though may introduce nondeterministic bahavior
#[cfg(unix)]
socket.set_reuse_port(true)?;
socket.set_reuse_address(true)?;
}
socket.bind(&addr.into())?;
Ok(socket)
}
pub type ResultType<F, E = anyhow::Error> = anyhow::Result<F, E>;
/// Certain router and firewalls scan the packet and if they
/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address
pub struct AddrMangle();
impl AddrMangle {
pub fn encode(addr: SocketAddr) -> Vec<u8> {
match addr {
SocketAddr::V4(addr_v4) => {
let tm = (SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u32) as u128;
let ip = u32::from_ne_bytes(addr_v4.ip().octets()) as u128;
let port = addr.port() as u128;
let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF));
let bytes = v.to_ne_bytes();
let mut n_padding = 0;
for i in bytes.iter().rev() {
if i == &0u8 {
n_padding += 1;
} else {
break;
}
}
bytes[..(16 - n_padding)].to_vec()
}
_ => {
panic!("Only support ipv4");
}
}
}
pub fn decode(bytes: &[u8]) -> SocketAddr {
let mut padded = [0u8; 16];
padded[..bytes.len()].copy_from_slice(&bytes);
let number = u128::from_ne_bytes(padded);
let tm = (number >> 17) & (u32::max_value() as u128);
let ip = (((number >> 49) - tm) as u32).to_ne_bytes();
let port = (number & 0xFFFFFF) - (tm & 0xFFFF);
SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
port as u16,
))
}
}
pub fn get_version_from_url(url: &str) -> String {
let n = url.chars().count();
let a = url
.chars()
.rev()
.enumerate()
.filter(|(_, x)| x == &'-')
.next()
.map(|(i, _)| i);
if let Some(a) = a {
let b = url
.chars()
.rev()
.enumerate()
.filter(|(_, x)| x == &'.')
.next()
.map(|(i, _)| i);
if let Some(b) = b {
if a > b {
if url
.chars()
.skip(n - b)
.collect::<String>()
.parse::<i32>()
.is_ok()
{
return url.chars().skip(n - a).collect();
} else {
return url.chars().skip(n - a).take(a - b - 1).collect();
}
} else {
return url.chars().skip(n - a).collect();
}
}
}
"".to_owned()
}
pub fn to_socket_addr(host: &str) -> ResultType<SocketAddr> {
let addrs: Vec<SocketAddr> = host.to_socket_addrs()?.collect();
if addrs.is_empty() {
bail!("Failed to solve {}", host);
}
Ok(addrs[0])
}
pub fn gen_version() {
let mut file = File::create("./src/version.rs").unwrap();
for line in read_lines("Cargo.toml").unwrap() {
if let Ok(line) = line {
let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect();
if ab.len() == 2 && ab[0] == "version" {
use std::io::prelude::*;
file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes())
.ok();
file.sync_all().ok();
break;
}
}
}
}
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mangle() {
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116));
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
}
}

135
libs/hbb_common/src/quic.rs Normal file
View File

@@ -0,0 +1,135 @@
use crate::{allow_err, anyhow::anyhow, ResultType};
use protobuf::Message;
use std::{net::SocketAddr, sync::Arc};
use tokio::{self, stream::StreamExt, sync::mpsc};
const QUIC_HBB: &[&[u8]] = &[b"hbb"];
const SERVER_NAME: &str = "hbb";
type Sender = mpsc::UnboundedSender<Value>;
type Receiver = mpsc::UnboundedReceiver<Value>;
pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> {
let mut transport_config = quinn::TransportConfig::default();
transport_config.stream_window_uni(0);
let mut server_config = quinn::ServerConfig::default();
server_config.transport = Arc::new(transport_config);
let mut server_config = quinn::ServerConfigBuilder::new(server_config);
server_config.protocols(QUIC_HBB);
// server_config.enable_keylog();
// server_config.use_stateless_retry(true);
let mut endpoint = quinn::Endpoint::builder();
endpoint.listen(server_config.build());
let (end, incoming) = endpoint.with_socket(socket)?;
Ok((Server { incoming }, end.local_addr()?))
}
pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType<Connection> {
let mut endpoint = quinn::Endpoint::builder();
let mut client_config = quinn::ClientConfigBuilder::default();
client_config.protocols(QUIC_HBB);
//client_config.enable_keylog();
endpoint.default_client_config(client_config.build());
let (endpoint, _) = endpoint.bind(local_addr)?;
let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?;
Connection::new_for_client(new_conn.connection).await
}
pub struct Server {
incoming: quinn::Incoming,
}
impl Server {
#[inline]
pub async fn next(&mut self) -> ResultType<Option<Connection>> {
Connection::new_for_server(&mut self.incoming).await
}
}
pub struct Connection {
conn: quinn::Connection,
tx: quinn::SendStream,
rx: Receiver,
}
type Value = ResultType<Vec<u8>>;
impl Connection {
async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType<Option<Self>> {
if let Some(conn) = incoming.next().await {
let quinn::NewConnection {
connection: conn,
// uni_streams,
mut bi_streams,
..
} = conn.await?;
let (tx, rx) = mpsc::unbounded_channel::<Value>();
tokio::spawn(async move {
loop {
let stream = bi_streams.next().await;
if let Some(stream) = stream {
let stream = match stream {
Err(e) => {
tx.send(Err(e.into())).ok();
break;
}
Ok(s) => s,
};
let cloned = tx.clone();
tokio::spawn(async move {
allow_err!(handle_request(stream.1, cloned).await);
});
} else {
tx.send(Err(anyhow!("Reset by the peer"))).ok();
break;
}
}
log::info!("Exit connection outer loop");
});
let tx = conn.open_uni().await?;
Ok(Some(Self { conn, tx, rx }))
} else {
Ok(None)
}
}
async fn new_for_client(conn: quinn::Connection) -> ResultType<Self> {
let (tx, rx_quic) = conn.open_bi().await?;
let (tx_mpsc, rx) = mpsc::unbounded_channel::<Value>();
tokio::spawn(async move {
allow_err!(handle_request(rx_quic, tx_mpsc).await);
});
Ok(Self { conn, tx, rx })
}
#[inline]
pub async fn next(&mut self) -> Option<Value> {
// None is returned when all Sender halves have dropped,
// indicating that no further values can be sent on the channel.
self.rx.recv().await
}
#[inline]
pub fn remote_address(&self) -> SocketAddr {
self.conn.remote_address()
}
#[inline]
pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> {
self.tx.write_all(bytes).await?;
Ok(())
}
#[inline]
pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> {
match msg.write_to_bytes() {
Ok(bytes) => self.send_raw(&bytes).await?,
err => allow_err!(err),
}
Ok(())
}
}
async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> {
Ok(())
}

146
libs/hbb_common/src/tcp.rs Normal file
View File

@@ -0,0 +1,146 @@
use crate::{bail, bytes_codec::BytesCodec, ResultType};
use bytes::{BufMut, Bytes, BytesMut};
use futures::SinkExt;
use protobuf::Message;
use sodiumoxide::crypto::secretbox::{self, Key, Nonce};
use std::{
io::{Error, ErrorKind},
ops::{Deref, DerefMut},
};
use tokio::{
net::{TcpListener, TcpStream, ToSocketAddrs},
stream::StreamExt,
};
use tokio_util::codec::Framed;
pub struct FramedStream(Framed<TcpStream, BytesCodec>, Option<(Key, u64, u64)>);
impl Deref for FramedStream {
type Target = Framed<TcpStream, BytesCodec>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FramedStream {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FramedStream {
pub async fn new<T: ToSocketAddrs, T2: ToSocketAddrs>(
remote_addr: T,
local_addr: T2,
ms_timeout: u64,
) -> ResultType<Self> {
for local_addr in local_addr.to_socket_addrs().await? {
for remote_addr in remote_addr.to_socket_addrs().await? {
if let Ok(stream) = super::timeout(
ms_timeout,
TcpStream::connect_std(
super::new_socket(local_addr, true, true)?.into_tcp_stream(),
&remote_addr,
),
)
.await?
{
return Ok(Self(Framed::new(stream, BytesCodec::new()), None));
}
}
}
bail!("could not resolve to any address");
}
pub fn from(stream: TcpStream) -> Self {
Self(Framed::new(stream, BytesCodec::new()), None)
}
pub fn set_raw(&mut self) {
self.0.codec_mut().set_raw();
self.1 = None;
}
pub fn is_secured(&self) -> bool {
self.1.is_some()
}
#[inline]
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
self.send_raw(msg.write_to_bytes()?).await
}
#[inline]
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
let mut msg = msg;
if let Some(key) = self.1.as_mut() {
key.1 += 1;
let nonce = Self::get_nonce(key.1);
msg = secretbox::seal(&msg, &nonce, &key.0);
}
self.0.send(bytes::Bytes::from(msg)).await?;
Ok(())
}
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
self.0.send(bytes).await?;
Ok(())
}
#[inline]
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
let mut res = self.0.next().await;
if let Some(key) = self.1.as_mut() {
if let Some(Ok(bytes)) = res.as_mut() {
key.2 += 1;
let nonce = Self::get_nonce(key.2);
match secretbox::open(&bytes, &nonce, &key.0) {
Ok(res) => {
bytes.clear();
bytes.put_slice(&res);
}
Err(()) => {
return Some(Err(Error::new(ErrorKind::Other, "decryption error")));
}
}
}
}
res
}
#[inline]
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
if let Ok(res) = super::timeout(ms, self.next()).await {
res
} else {
None
}
}
pub fn set_key(&mut self, key: Key) {
self.1 = Some((key, 0, 0));
}
fn get_nonce(seqnum: u64) -> Nonce {
let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]);
nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_ne_bytes());
nonce
}
}
const DEFAULT_BACKLOG: i32 = 128;
#[allow(clippy::never_loop)]
pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<TcpListener> {
if !reuse {
Ok(TcpListener::bind(addr).await?)
} else {
for addr in addr.to_socket_addrs().await? {
let socket = super::new_socket(addr, true, true)?;
socket.listen(DEFAULT_BACKLOG)?;
return Ok(TcpListener::from_std(socket.into_tcp_listener())?);
}
bail!("could not resolve to any address");
}
}

View File

@@ -0,0 +1,75 @@
use crate::{bail, ResultType};
use bytes::BytesMut;
use futures::SinkExt;
use protobuf::Message;
use std::{
io::Error,
net::SocketAddr,
ops::{Deref, DerefMut},
};
use tokio::{net::ToSocketAddrs, net::UdpSocket, stream::StreamExt};
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
pub struct FramedSocket(UdpFramed<BytesCodec>);
impl Deref for FramedSocket {
type Target = UdpFramed<BytesCodec>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FramedSocket {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FramedSocket {
pub async fn new<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
let socket = UdpSocket::bind(addr).await?;
Ok(Self(UdpFramed::new(socket, BytesCodec::new())))
}
#[allow(clippy::never_loop)]
pub async fn new_reuse<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
for addr in addr.to_socket_addrs().await? {
return Ok(Self(UdpFramed::new(
UdpSocket::from_std(super::new_socket(addr, false, true)?.into_udp_socket())?,
BytesCodec::new(),
)));
}
bail!("could not resolve to any address");
}
#[inline]
pub async fn send(&mut self, msg: &impl Message, addr: SocketAddr) -> ResultType<()> {
self.0
.send((bytes::Bytes::from(msg.write_to_bytes().unwrap()), addr))
.await?;
Ok(())
}
#[inline]
pub async fn send_raw(&mut self, msg: &'static [u8], addr: SocketAddr) -> ResultType<()> {
self.0.send((bytes::Bytes::from(msg), addr)).await?;
Ok(())
}
#[inline]
pub async fn next(&mut self) -> Option<Result<(BytesMut, SocketAddr), Error>> {
self.0.next().await
}
#[inline]
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<(BytesMut, SocketAddr), Error>> {
if let Ok(res) =
tokio::time::timeout(std::time::Duration::from_millis(ms), self.0.next()).await
{
res
} else {
None
}
}
}

2
libs/magnum-opus/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
target
Cargo.lock

View File

@@ -0,0 +1,18 @@
[package]
name = "magnum-opus"
version = "0.3.4"
authors = ["Tad Hardesty <tad@platymuus.com>", "Sergey Duck <sergeypechnikov326@gmail.com>"]
edition = "2018"
description = "Safe Rust bindings for libopus"
readme = "README.md"
license = "MIT/Apache-2.0"
keywords = ["opus", "codec", "voice", "sound", "audio"]
categories = ["api-bindings", "encoding", "compression",
"multimedia::audio", "multimedia::encoding"]
repository = "https://github.com/DuckerMan/magnum-opus"
documentation = "https://docs.rs/magnum-opus"
[build-dependencies]
target_build_utils = "0.3"
bindgen = "0.53"

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,19 @@
Copyright (c) 2016 Tad Hardesty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,22 @@
# magnum-opus [![](https://meritbadge.herokuapp.com/magnum-opus)](https://crates.io/crates/magnum-opus) [![](https://img.shields.io/badge/docs-online-2020ff.svg)](https://docs.rs/magnum-opus)
### This is the fork of @SpaceManiac repo, which now is abandoned
Safe Rust bindings for libopus. The rustdoc (available through `cargo doc`)
includes brief descriptions for methods, and detailed API information can be
found at the [libopus documentation](https://opus-codec.org/docs/opus_api-1.1.2/).
## License
Licensed under either of
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.

89
libs/magnum-opus/build.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::{
env,
path::{Path, PathBuf},
};
fn find_package(name: &str) -> Vec<PathBuf> {
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: PathBuf = vcpkg_root.into();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "x86_64" {
target_arch = "x64".to_owned();
} else if target_arch == "aarch64" {
target_arch = "arm64".to_owned();
} else {
target_arch = "arm".to_owned();
}
let target = if target_os == "macos" {
"x64-osx".to_owned()
} else if target_os == "windows" {
"x64-windows-static".to_owned()
} else if target_os == "android" {
format!("{}-android-static", target_arch)
} else {
"x64-linux".to_owned()
};
println!("cargo:info={}", target);
path.push("installed");
path.push(target);
println!(
"{}",
format!("cargo:rustc-link-lib={}", name.trim_start_matches("lib"))
);
println!(
"{}",
format!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
)
);
let include = path.join("include");
println!("{}", format!("cargo:include={}", include.to_str().unwrap()));
vec![include]
}
fn generate_bindings(ffi_header: &Path, include_paths: &[PathBuf], ffi_rs: &Path) {
#[derive(Debug)]
struct ParseCallbacks;
impl bindgen::callbacks::ParseCallbacks for ParseCallbacks {
fn int_macro(&self, name: &str, _value: i64) -> Option<bindgen::callbacks::IntKind> {
if name.starts_with("OPUS") {
Some(bindgen::callbacks::IntKind::Int)
} else {
None
}
}
}
let mut b = bindgen::Builder::default()
.header(ffi_header.to_str().unwrap())
.parse_callbacks(Box::new(ParseCallbacks))
.generate_comments(false);
for dir in include_paths {
b = b.clang_arg(format!("-I{}", dir.display()));
}
b.generate().unwrap().write_to_file(ffi_rs).unwrap();
}
fn gen_opus() {
let includes = find_package("opus");
let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let src_dir = Path::new(&src_dir);
let out_dir = env::var_os("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir);
let ffi_header = src_dir.join("opus_ffi.h");
println!("rerun-if-changed={}", ffi_header.display());
for dir in &includes {
println!("rerun-if-changed={}", dir.display());
}
let ffi_rs = out_dir.join("opus_ffi.rs");
generate_bindings(&ffi_header, &includes, &ffi_rs);
}
fn main() {
gen_opus()
}

View File

@@ -0,0 +1 @@
#include <opus/opus_multistream.h>

843
libs/magnum-opus/src/lib.rs Normal file
View File

@@ -0,0 +1,843 @@
// Copyright 2016 Tad Hardesty
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! High-level bindings for libopus.
//!
//! Only brief descriptions are included here. For detailed information, consult
//! the [libopus documentation](https://opus-codec.org/docs/opus_api-1.1.2/).
#![warn(missing_docs)]
mod opus_ffi;
use opus_ffi as ffi;
use std::ffi::CStr;
use std::marker::PhantomData;
use std::os::raw::c_int;
// ============================================================================
// Constants
// Generic CTLs
const OPUS_RESET_STATE: c_int = 4028; // void
const OPUS_GET_FINAL_RANGE: c_int = 4031; // out *u32
const OPUS_GET_BANDWIDTH: c_int = 4009; // out *i32
const OPUS_GET_SAMPLE_RATE: c_int = 4029; // out *i32
// Encoder CTLs
const OPUS_SET_BITRATE: c_int = 4002; // in i32
const OPUS_GET_BITRATE: c_int = 4003; // out *i32
const OPUS_SET_VBR: c_int = 4006; // in i32
const OPUS_GET_VBR: c_int = 4007; // out *i32
const OPUS_SET_VBR_CONSTRAINT: c_int = 4020; // in i32
const OPUS_GET_VBR_CONSTRAINT: c_int = 4021; // out *i32
const OPUS_SET_INBAND_FEC: c_int = 4012; // in i32
const OPUS_GET_INBAND_FEC: c_int = 4013; // out *i32
const OPUS_SET_PACKET_LOSS_PERC: c_int = 4014; // in i32
const OPUS_GET_PACKET_LOSS_PERC: c_int = 4015; // out *i32
const OPUS_GET_LOOKAHEAD: c_int = 4027; // out *i32
// Decoder CTLs
const OPUS_SET_GAIN: c_int = 4034; // in i32
const OPUS_GET_GAIN: c_int = 4045; // out *i32
const OPUS_GET_LAST_PACKET_DURATION: c_int = 4039; // out *i32
const OPUS_GET_PITCH: c_int = 4033; // out *i32
// Bitrate
const OPUS_AUTO: c_int = -1000;
const OPUS_BITRATE_MAX: c_int = -1;
/// The possible applications for the codec.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Application {
/// Best for most VoIP/videoconference applications where listening quality
/// and intelligibility matter most.
Voip = 2048,
/// Best for broadcast/high-fidelity application where the decoded audio
/// should be as close as possible to the input.
Audio = 2049,
/// Only use when lowest-achievable latency is what matters most.
LowDelay = 2051,
}
/// The available channel setings.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Channels {
/// One channel.
Mono = 1,
/// Two channels, left and right.
Stereo = 2,
}
/// The available bandwidth level settings.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Bandwidth {
/// Auto/default setting.
Auto = -1000,
/// 4kHz bandpass.
Narrowband = 1101,
/// 6kHz bandpass.
Mediumband = 1102,
/// 8kHz bandpass.
Wideband = 1103,
/// 12kHz bandpass.
Superwideband = 1104,
/// 20kHz bandpass.
Fullband = 1105,
}
impl Bandwidth {
fn from_int(value: i32) -> Option<Bandwidth> {
Some(match value {
-1000 => Bandwidth::Auto,
1101 => Bandwidth::Narrowband,
1102 => Bandwidth::Mediumband,
1103 => Bandwidth::Wideband,
1104 => Bandwidth::Superwideband,
1105 => Bandwidth::Fullband,
_ => return None,
})
}
fn decode(value: i32, what: &'static str) -> Result<Bandwidth> {
match Bandwidth::from_int(value) {
Some(bandwidth) => Ok(bandwidth),
None => Err(Error::bad_arg(what)),
}
}
}
/// Possible error codes.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ErrorCode {
/// One or more invalid/out of range arguments.
BadArg = -1,
/// Not enough bytes allocated in the buffer.
BufferTooSmall = -2,
/// An internal error was detected.
InternalError = -3,
/// The compressed data passed is corrupted.
InvalidPacket = -4,
/// Invalid/unsupported request number.
Unimplemented = -5,
/// An encoder or decoder structure is invalid or already freed.
InvalidState = -6,
/// Memory allocation has failed.
AllocFail = -7,
/// An unknown failure.
Unknown = -8,
}
impl ErrorCode {
fn from_int(value: c_int) -> ErrorCode {
use ErrorCode::*;
match value {
ffi::OPUS_BAD_ARG => BadArg,
ffi::OPUS_BUFFER_TOO_SMALL => BufferTooSmall,
ffi::OPUS_INTERNAL_ERROR => InternalError,
ffi::OPUS_INVALID_PACKET => InvalidPacket,
ffi::OPUS_UNIMPLEMENTED => Unimplemented,
ffi::OPUS_INVALID_STATE => InvalidState,
ffi::OPUS_ALLOC_FAIL => AllocFail,
_ => Unknown,
}
}
/// Get a human-readable error string for this error code.
pub fn description(self) -> &'static str {
// should always be ASCII and non-null for any input
unsafe { CStr::from_ptr(ffi::opus_strerror(self as c_int)) }.to_str().unwrap()
}
}
/// Possible bitrates.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Bitrate {
/// Explicit bitrate choice (in bits/second).
Bits(i32),
/// Maximum bitrate allowed (up to maximum number of bytes for the packet).
Max,
/// Default bitrate decided by the encoder (not recommended).
Auto,
}
/// Get the libopus version string.
///
/// Applications may look for the substring "-fixed" in the version string to
/// determine whether they have a fixed-point or floating-point build at
/// runtime.
pub fn version() -> &'static str {
// verison string should always be ASCII
unsafe { CStr::from_ptr(ffi::opus_get_version_string()) }.to_str().unwrap()
}
macro_rules! ffi {
($f:ident $(, $rest:expr)*) => {
match unsafe { ffi::$f($($rest),*) } {
code if code < 0 => return Err(Error::from_code(stringify!($f), code)),
code => code,
}
}
}
macro_rules! ctl {
($f:ident, $this:ident, $ctl:ident, $($rest:expr),*) => {
match unsafe { ffi::$f($this.ptr, $ctl, $($rest),*) } {
code if code < 0 => return Err(Error::from_code(
concat!(stringify!($f), "(", stringify!($ctl), ")"),
code,
)),
_ => (),
}
}
}
// ============================================================================
// Encoder
macro_rules! enc_ctl {
($this:ident, $ctl:ident $(, $rest:expr)*) => {
ctl!(opus_encoder_ctl, $this, $ctl, $($rest),*)
}
}
/// An Opus encoder with associated state.
#[derive(Debug)]
pub struct Encoder {
ptr: *mut ffi::OpusEncoder,
channels: Channels,
}
impl Encoder {
/// Create and initialize an encoder.
pub fn new(sample_rate: u32, channels: Channels, mode: Application) -> Result<Encoder> {
let mut error = 0;
let ptr = unsafe { ffi::opus_encoder_create(
sample_rate as i32,
channels as c_int,
mode as c_int,
&mut error) };
if error != ffi::OPUS_OK || ptr.is_null() {
Err(Error::from_code("opus_encoder_create", error))
} else {
Ok(Encoder { ptr: ptr, channels: channels })
}
}
/// Encode an Opus frame.
pub fn encode(&mut self, input: &[i16], output: &mut [u8]) -> Result<usize> {
let len = ffi!(opus_encode, self.ptr,
input.as_ptr(), len(input) / self.channels as c_int,
output.as_mut_ptr(), len(output));
Ok(len as usize)
}
/// Encode an Opus frame from floating point input.
pub fn encode_float(&mut self, input: &[f32], output: &mut [u8]) -> Result<usize> {
let len = ffi!(opus_encode_float, self.ptr,
input.as_ptr(), len(input) / self.channels as c_int,
output.as_mut_ptr(), len(output));
Ok(len as usize)
}
/// Encode an Opus frame to a new buffer.
pub fn encode_vec(&mut self, input: &[i16], max_size: usize) -> Result<Vec<u8>> {
let mut output: Vec<u8> = vec![0; max_size];
let result = self.encode(input, output.as_mut_slice())?;
output.truncate(result);
Ok(output)
}
/// Encode an Opus frame from floating point input to a new buffer.
pub fn encode_vec_float(&mut self, input: &[f32], max_size: usize) -> Result<Vec<u8>> {
let mut output: Vec<u8> = vec![0; max_size];
let result = self.encode_float(input, output.as_mut_slice())?;
output.truncate(result);
Ok(output)
}
// ------------
// Generic CTLs
/// Reset the codec state to be equivalent to a freshly initialized state.
pub fn reset_state(&mut self) -> Result<()> {
enc_ctl!(self, OPUS_RESET_STATE);
Ok(())
}
/// Get the final range of the codec's entropy coder.
pub fn get_final_range(&mut self) -> Result<u32> {
let mut value: u32 = 0;
enc_ctl!(self, OPUS_GET_FINAL_RANGE, &mut value);
Ok(value)
}
/// Get the encoder's configured bandpass.
pub fn get_bandwidth(&mut self) -> Result<Bandwidth> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_BANDWIDTH, &mut value);
Bandwidth::decode(value, "opus_encoder_ctl(OPUS_GET_BANDWIDTH)")
}
/// Get the samping rate the encoder was intialized with.
pub fn get_sample_rate(&mut self) -> Result<u32> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_SAMPLE_RATE, &mut value);
Ok(value as u32)
}
// ------------
// Encoder CTLs
/// Set the encoder's bitrate.
pub fn set_bitrate(&mut self, value: Bitrate) -> Result<()> {
let value: i32 = match value {
Bitrate::Auto => OPUS_AUTO,
Bitrate::Max => OPUS_BITRATE_MAX,
Bitrate::Bits(b) => b,
};
enc_ctl!(self, OPUS_SET_BITRATE, value);
Ok(())
}
/// Get the encoder's bitrate.
pub fn get_bitrate(&mut self) -> Result<Bitrate> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_BITRATE, &mut value);
Ok(match value {
OPUS_AUTO => Bitrate::Auto,
OPUS_BITRATE_MAX => Bitrate::Max,
_ => Bitrate::Bits(value),
})
}
/// Enable or disable variable bitrate.
pub fn set_vbr(&mut self, vbr: bool) -> Result<()> {
let value: i32 = if vbr { 1 } else { 0 };
enc_ctl!(self, OPUS_SET_VBR, value);
Ok(())
}
/// Determine if variable bitrate is enabled.
pub fn get_vbr(&mut self) -> Result<bool> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_VBR, &mut value);
Ok(value != 0)
}
/// Enable or disable constrained VBR.
pub fn set_vbr_constraint(&mut self, vbr: bool) -> Result<()> {
let value: i32 = if vbr { 1 } else { 0 };
enc_ctl!(self, OPUS_SET_VBR_CONSTRAINT, value);
Ok(())
}
/// Determine if constrained VBR is enabled.
pub fn get_vbr_constraint(&mut self) -> Result<bool> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_VBR_CONSTRAINT, &mut value);
Ok(value != 0)
}
/// Configures the encoder's use of inband forward error correction (FEC).
pub fn set_inband_fec(&mut self, value: bool) -> Result<()> {
let value: i32 = if value { 1 } else { 0 };
enc_ctl!(self, OPUS_SET_INBAND_FEC, value);
Ok(())
}
/// Gets encoder's configured use of inband forward error correction.
pub fn get_inband_fec(&mut self) -> Result<bool> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_INBAND_FEC, &mut value);
Ok(value != 0)
}
/// Sets the encoder's expected packet loss percentage.
pub fn set_packet_loss_perc(&mut self, value: i32) -> Result<()> {
enc_ctl!(self, OPUS_SET_PACKET_LOSS_PERC, value);
Ok(())
}
/// Gets the encoder's expected packet loss percentage.
pub fn get_packet_loss_perc(&mut self) -> Result<i32> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_PACKET_LOSS_PERC, &mut value);
Ok(value)
}
/// Gets the total samples of delay added by the entire codec.
pub fn get_lookahead(&mut self) -> Result<i32> {
let mut value: i32 = 0;
enc_ctl!(self, OPUS_GET_LOOKAHEAD, &mut value);
Ok(value)
}
// TODO: Encoder-specific CTLs
}
impl Drop for Encoder {
fn drop(&mut self) {
unsafe { ffi::opus_encoder_destroy(self.ptr) }
}
}
// "A single codec state may only be accessed from a single thread at
// a time and any required locking must be performed by the caller. Separate
// streams must be decoded with separate decoder states and can be decoded
// in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK
// defined."
//
// In other words, opus states may be moved between threads at will. A special
// compilation mode intended for embedded platforms forbids multithreaded use
// of the library as a whole rather than on a per-state basis, but the opus-sys
// crate does not use this mode.
unsafe impl Send for Encoder {}
// ============================================================================
// Decoder
macro_rules! dec_ctl {
($this:ident, $ctl:ident $(, $rest:expr)*) => {
ctl!(opus_decoder_ctl, $this, $ctl, $($rest),*)
}
}
/// An Opus decoder with associated state.
#[derive(Debug)]
pub struct Decoder {
ptr: *mut ffi::OpusDecoder,
channels: Channels,
}
impl Decoder {
/// Create and initialize a decoder.
pub fn new(sample_rate: u32, channels: Channels) -> Result<Decoder> {
let mut error = 0;
let ptr = unsafe { ffi::opus_decoder_create(
sample_rate as i32,
channels as c_int,
&mut error) };
if error != ffi::OPUS_OK || ptr.is_null() {
Err(Error::from_code("opus_decoder_create", error))
} else {
Ok(Decoder { ptr: ptr, channels: channels })
}
}
/// Decode an Opus packet.
pub fn decode(&mut self, input: &[u8], output: &mut [i16], fec: bool) -> Result<usize> {
let ptr = match input.len() {
0 => std::ptr::null(),
_ => input.as_ptr(),
};
let len = ffi!(opus_decode, self.ptr,
ptr, len(input),
output.as_mut_ptr(), len(output) / self.channels as c_int,
fec as c_int);
Ok(len as usize)
}
/// Decode an Opus packet with floating point output.
pub fn decode_float(&mut self, input: &[u8], output: &mut [f32], fec: bool) -> Result<usize> {
let ptr = match input.len() {
0 => std::ptr::null(),
_ => input.as_ptr(),
};
let len = ffi!(opus_decode_float, self.ptr,
ptr, len(input),
output.as_mut_ptr(), len(output) / self.channels as c_int,
fec as c_int);
Ok(len as usize)
}
/// Get the number of samples of an Opus packet.
pub fn get_nb_samples(&self, packet: &[u8]) -> Result<usize> {
let len = ffi!(opus_decoder_get_nb_samples, self.ptr,
packet.as_ptr(), packet.len() as i32);
Ok(len as usize)
}
// ------------
// Generic CTLs
/// Reset the codec state to be equivalent to a freshly initialized state.
pub fn reset_state(&mut self) -> Result<()> {
dec_ctl!(self, OPUS_RESET_STATE);
Ok(())
}
/// Get the final range of the codec's entropy coder.
pub fn get_final_range(&mut self) -> Result<u32> {
let mut value: u32 = 0;
dec_ctl!(self, OPUS_GET_FINAL_RANGE, &mut value);
Ok(value)
}
/// Get the decoder's last bandpass.
pub fn get_bandwidth(&mut self) -> Result<Bandwidth> {
let mut value: i32 = 0;
dec_ctl!(self, OPUS_GET_BANDWIDTH, &mut value);
Bandwidth::decode(value, "opus_decoder_ctl(OPUS_GET_BANDWIDTH)")
}
/// Get the samping rate the decoder was intialized with.
pub fn get_sample_rate(&mut self) -> Result<u32> {
let mut value: i32 = 0;
dec_ctl!(self, OPUS_GET_SAMPLE_RATE, &mut value);
Ok(value as u32)
}
// ------------
// Decoder CTLs
/// Configures decoder gain adjustment.
///
/// Scales the decoded output by a factor specified in Q8 dB units. This has
/// a maximum range of -32768 to 32768 inclusive, and returns `BadArg`
/// otherwise. The default is zero indicating no adjustment. This setting
/// survives decoder reset.
///
/// `gain = pow(10, x / (20.0 * 256))`
pub fn set_gain(&mut self, gain: i32) -> Result<()> {
dec_ctl!(self, OPUS_SET_GAIN, gain);
Ok(())
}
/// Gets the decoder's configured gain adjustment.
pub fn get_gain(&mut self) -> Result<i32> {
let mut value: i32 = 0;
dec_ctl!(self, OPUS_GET_GAIN, &mut value);
Ok(value)
}
/// Gets the duration (in samples) of the last packet successfully decoded
/// or concealed.
pub fn get_last_packet_duration(&mut self) -> Result<u32> {
let mut value: i32 = 0;
dec_ctl!(self, OPUS_GET_LAST_PACKET_DURATION, &mut value);
Ok(value as u32)
}
/// Gets the pitch of the last decoded frame, if available.
///
/// This can be used for any post-processing algorithm requiring the use of
/// pitch, e.g. time stretching/shortening. If the last frame was not
/// voiced, or if the pitch was not coded in the frame, then zero is
/// returned.
pub fn get_pitch(&mut self) -> Result<i32> {
let mut value: i32 = 0;
dec_ctl!(self, OPUS_GET_PITCH, &mut value);
Ok(value)
}
}
impl Drop for Decoder {
fn drop(&mut self) {
unsafe { ffi::opus_decoder_destroy(self.ptr) }
}
}
// See `unsafe impl Send for Encoder`.
unsafe impl Send for Decoder {}
// ============================================================================
// Packet Analysis
/// Analyze raw Opus packets.
pub mod packet {
use super::*;
use super::ffi;
use std::{ptr, slice};
/// Get the bandwidth of an Opus packet.
pub fn get_bandwidth(packet: &[u8]) -> Result<Bandwidth> {
if packet.len() < 1 {
return Err(Error::bad_arg("opus_packet_get_bandwidth"));
}
let bandwidth = ffi!(opus_packet_get_bandwidth, packet.as_ptr());
Bandwidth::decode(bandwidth, "opus_packet_get_bandwidth")
}
/// Get the number of channels from an Opus packet.
pub fn get_nb_channels(packet: &[u8]) -> Result<Channels> {
if packet.len() < 1 {
return Err(Error::bad_arg("opus_packet_get_nb_channels"));
}
let channels = ffi!(opus_packet_get_nb_channels, packet.as_ptr());
match channels {
1 => Ok(Channels::Mono),
2 => Ok(Channels::Stereo),
_ => Err(Error::bad_arg("opus_packet_get_nb_channels")),
}
}
/// Get the number of frames in an Opus packet.
pub fn get_nb_frames(packet: &[u8]) -> Result<usize> {
let frames = ffi!(opus_packet_get_nb_frames, packet.as_ptr(), len(packet));
Ok(frames as usize)
}
/// Get the number of samples of an Opus packet.
pub fn get_nb_samples(packet: &[u8], sample_rate: u32) -> Result<usize> {
let frames = ffi!(opus_packet_get_nb_samples,
packet.as_ptr(), len(packet),
sample_rate as c_int);
Ok(frames as usize)
}
/// Get the number of samples per frame from an Opus packet.
pub fn get_samples_per_frame(packet: &[u8], sample_rate: u32) -> Result<usize> {
if packet.len() < 1 {
return Err(Error::bad_arg("opus_packet_get_samples_per_frame"))
}
let samples = ffi!(opus_packet_get_samples_per_frame,
packet.as_ptr(), sample_rate as c_int);
Ok(samples as usize)
}
/// Parse an Opus packet into one or more frames.
pub fn parse(packet: &[u8]) -> Result<Packet> {
let mut toc: u8 = 0;
let mut frames = [ptr::null(); 48];
let mut sizes = [0i16; 48];
let mut payload_offset: i32 = 0;
let num_frames = ffi!(opus_packet_parse,
packet.as_ptr(), len(packet),
&mut toc, frames.as_mut_ptr(),
sizes.as_mut_ptr(), &mut payload_offset);
let mut frames_vec = Vec::with_capacity(num_frames as usize);
for i in 0..num_frames as usize {
frames_vec.push(unsafe { slice::from_raw_parts(frames[i], sizes[i] as usize) });
}
Ok(Packet {
toc: toc,
frames: frames_vec,
payload_offset: payload_offset as usize,
})
}
/// A parsed Opus packet, retuned from `parse`.
#[derive(Debug)]
pub struct Packet<'a> {
/// The TOC byte of the packet.
pub toc: u8,
/// The frames contained in the packet.
pub frames: Vec<&'a [u8]>,
/// The offset into the packet at which the payload is located.
pub payload_offset: usize,
}
/// Pad a given Opus packet to a larger size.
///
/// The packet will be extended from the first `prev_len` bytes of the
/// buffer into the rest of the available space.
pub fn pad(packet: &mut [u8], prev_len: usize) -> Result<usize> {
let result = ffi!(opus_packet_pad, packet.as_mut_ptr(),
check_len(prev_len), len(packet));
Ok(result as usize)
}
/// Remove all padding from a given Opus packet and rewrite the TOC sequence
/// to minimize space usage.
pub fn unpad(packet: &mut [u8]) -> Result<usize> {
let result = ffi!(opus_packet_unpad, packet.as_mut_ptr(), len(packet));
Ok(result as usize)
}
}
// ============================================================================
// Float Soft Clipping
/// Soft-clipping to bring a float signal within the [-1,1] range.
#[derive(Debug)]
pub struct SoftClip {
channels: Channels,
memory: [f32; 2],
}
impl SoftClip {
/// Initialize a new soft-clipping state.
pub fn new(channels: Channels) -> SoftClip {
SoftClip { channels: channels, memory: [0.0; 2] }
}
/// Apply soft-clipping to a float signal.
pub fn apply(&mut self, signal: &mut [f32]) {
unsafe { ffi::opus_pcm_soft_clip(
signal.as_mut_ptr(),
len(signal) / self.channels as c_int,
self.channels as c_int,
self.memory.as_mut_ptr()) };
}
}
// ============================================================================
// Repacketizer
/// A repacketizer used to merge together or split apart multiple Opus packets.
#[derive(Debug)]
pub struct Repacketizer {
ptr: *mut ffi::OpusRepacketizer,
}
impl Repacketizer {
/// Create and initialize a repacketizer.
pub fn new() -> Result<Repacketizer> {
let ptr = unsafe { ffi::opus_repacketizer_create() };
if ptr.is_null() {
Err(Error::from_code("opus_repacketizer_create", ffi::OPUS_ALLOC_FAIL))
} else {
Ok(Repacketizer { ptr: ptr })
}
}
/// Shortcut to combine several smaller packets into one larger one.
pub fn combine(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<usize> {
let mut state = self.begin();
for &packet in input {
state.cat(packet)?;
}
state.out(output)
}
/// Begin using the repacketizer.
pub fn begin<'rp, 'buf>(&'rp mut self) -> RepacketizerState<'rp, 'buf> {
unsafe { ffi::opus_repacketizer_init(self.ptr); }
RepacketizerState { rp: self, phantom: PhantomData }
}
}
impl Drop for Repacketizer {
fn drop(&mut self) {
unsafe { ffi::opus_repacketizer_destroy(self.ptr) }
}
}
// See `unsafe impl Send for Encoder`.
unsafe impl Send for Repacketizer {}
// To understand why these lifetime bounds are needed, imagine that the
// repacketizer keeps an internal Vec<&'buf [u8]>, which is added to by cat()
// and accessed by get_nb_frames(), out(), and out_range(). To prove that these
// lifetime bounds are correct, a dummy implementation with the same signatures
// but a real Vec<&'buf [u8]> rather than unsafe blocks may be substituted.
/// An in-progress repacketization.
#[derive(Debug)]
pub struct RepacketizerState<'rp, 'buf> {
rp: &'rp mut Repacketizer,
phantom: PhantomData<&'buf [u8]>,
}
impl<'rp, 'buf> RepacketizerState<'rp, 'buf> {
/// Add a packet to the current repacketizer state.
pub fn cat(&mut self, packet: &'buf [u8]) -> Result<()> {
ffi!(opus_repacketizer_cat, self.rp.ptr,
packet.as_ptr(), len(packet));
Ok(())
}
/// Add a packet to the current repacketizer state, moving it.
#[inline]
pub fn cat_move<'b2>(self, packet: &'b2 [u8]) -> Result<RepacketizerState<'rp, 'b2>> where 'buf: 'b2 {
let mut shorter = self;
shorter.cat(packet)?;
Ok(shorter)
}
/// Get the total number of frames contained in packet data submitted so
/// far via `cat`.
pub fn get_nb_frames(&mut self) -> usize {
unsafe { ffi::opus_repacketizer_get_nb_frames(self.rp.ptr) as usize }
}
/// Construct a new packet from data previously submitted via `cat`.
///
/// All previously submitted frames are used.
pub fn out(&mut self, buffer: &mut [u8]) -> Result<usize> {
let result = ffi!(opus_repacketizer_out, self.rp.ptr,
buffer.as_mut_ptr(), len(buffer));
Ok(result as usize)
}
/// Construct a new packet from data previously submitted via `cat`, with
/// a manually specified subrange.
///
/// The `end` index should not exceed the value of `get_nb_frames()`.
pub fn out_range(&mut self, begin: usize, end: usize, buffer: &mut [u8]) -> Result<usize> {
let result = ffi!(opus_repacketizer_out_range, self.rp.ptr,
check_len(begin), check_len(end),
buffer.as_mut_ptr(), len(buffer));
Ok(result as usize)
}
}
// ============================================================================
// TODO: Multistream API
// ============================================================================
// Error Handling
/// Opus error Result alias.
pub type Result<T> = std::result::Result<T, Error>;
/// An error generated by the Opus library.
#[derive(Debug)]
pub struct Error {
function: &'static str,
code: ErrorCode,
}
impl Error {
fn bad_arg(what: &'static str) -> Error {
Error { function: what, code: ErrorCode::BadArg }
}
fn from_code(what: &'static str, code: c_int) -> Error {
Error { function: what, code: ErrorCode::from_int(code) }
}
/// Get the name of the Opus function from which the error originated.
#[inline]
pub fn function(&self) -> &'static str { self.function }
/// Get a textual description of the error provided by Opus.
#[inline]
pub fn description(&self) -> &'static str { self.code.description() }
/// Get the Opus error code of the error.
#[inline]
pub fn code(&self) -> ErrorCode { self.code }
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}: {}", self.function, self.description())
}
}
impl std::error::Error for Error {
fn description(&self) -> &str {
self.code.description()
}
}
fn check_len(val: usize) -> c_int {
let len = val as c_int;
if len as usize != val {
panic!("length out of range: {}", val);
}
len
}
#[inline]
fn len<T>(slice: &[T]) -> c_int {
check_len(slice.len())
}

View File

@@ -0,0 +1,6 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/opus_ffi.rs"));

View File

@@ -0,0 +1,10 @@
extern crate opus;
fn main() {
let mut rp = opus::Repacketizer::new().unwrap();
let mut wip = rp.begin().cat_move(
&[1, 2, 3]
//~^ ERROR borrowed value does not live long enough
).unwrap();
wip.out(&mut []);
}

View File

@@ -0,0 +1,13 @@
//! Test that supplying empty packets does forward error correction.
extern crate magnum_opus;
use magnum_opus::*;
#[test]
fn blah() {
let mut magnum_opus = Decoder::new(48000, Channels::Mono).unwrap();
let mut output = vec![0i16; 5760];
let size = magnum_opus.decode(&[], &mut output[..], true).unwrap();
assert_eq!(size, 5760);
}

View File

@@ -0,0 +1,29 @@
// Based on libmagnum_opus/tests/test_magnum_opus_padding.c
/* Check for overflow in reading the padding length.
* http://lists.xiph.org/pipermail/magnum_opus/2012-November/001834.html
*/
extern crate magnum_opus;
#[test]
fn test_overflow() {
const PACKETSIZE: usize = 16909318;
const CHANNELS: magnum_opus::Channels = magnum_opus::Channels::Stereo;
const FRAMESIZE: usize = 5760;
let mut input = vec![0xff; PACKETSIZE];
let mut output = vec![0i16; FRAMESIZE * 2];
input[0] = 0xff;
input[1] = 0x41;
*input.last_mut().unwrap() = 0x0b;
let mut decoder = magnum_opus::Decoder::new(48000, CHANNELS).unwrap();
let result = decoder.decode(&input[..], &mut output[..], false);
drop(decoder);
drop(input);
drop(output);
assert_eq!(result.unwrap_err().code(), magnum_opus::ErrorCode::InvalidPacket);
}

View File

@@ -0,0 +1,127 @@
extern crate magnum_opus;
fn check_ascii(s: &str) -> &str {
for &b in s.as_bytes() {
assert!(b < 0x80, "Non-ASCII character in string");
assert!(b > 0x00, "NUL in string")
}
std::str::from_utf8(s.as_bytes()).unwrap()
}
#[test]
fn strings_ascii() {
use magnum_opus::ErrorCode::*;
println!("\nVersion: {}", check_ascii(magnum_opus::version()));
let codes = [BadArg, BufferTooSmall, InternalError, InvalidPacket,
Unimplemented, InvalidState, AllocFail, Unknown];
for &code in codes.iter() {
println!("{:?}: {}", code, check_ascii(code.description()));
}
}
// 48000Hz * 1 channel * 20 ms / 1000
const MONO_20MS: usize = 48000 * 1 * 20 / 1000;
#[test]
fn encode_mono() {
let mut encoder = magnum_opus::Encoder::new(48000, magnum_opus::Channels::Mono, magnum_opus::Application::Audio).unwrap();
let mut output = [0; 256];
let len = encoder.encode(&[0_i16; MONO_20MS], &mut output).unwrap();
assert_eq!(&output[..len], &[248, 255, 254]);
let len = encoder.encode(&[0_i16; MONO_20MS], &mut output).unwrap();
assert_eq!(&output[..len], &[248, 255, 254]);
let len = encoder.encode(&[1_i16; MONO_20MS], &mut output).unwrap();
assert!(len > 190 && len < 220);
let len = encoder.encode(&[0_i16; MONO_20MS], &mut output).unwrap();
assert!(len > 170 && len < 190);
let myvec = encoder.encode_vec(&[1_i16; MONO_20MS], output.len()).unwrap();
assert!(myvec.len() > 120 && myvec.len() < 140);
}
#[test]
fn encode_stereo() {
let mut encoder = magnum_opus::Encoder::new(48000, magnum_opus::Channels::Stereo, magnum_opus::Application::Audio).unwrap();
let mut output = [0; 512];
let len = encoder.encode(&[0_i16; 2 * MONO_20MS], &mut output).unwrap();
assert_eq!(&output[..len], &[252, 255, 254]);
let len = encoder.encode(&[0_i16; 4 * MONO_20MS], &mut output).unwrap();
assert_eq!(&output[..len], &[253, 255, 254, 255, 254]);
let len = encoder.encode(&[17_i16; 2 * MONO_20MS], &mut output).unwrap();
assert!(len > 240);
let len = encoder.encode(&[0_i16; 2 * MONO_20MS], &mut output).unwrap();
assert!(len > 240);
// Very small buffer should still succeed
let len = encoder.encode(&[95_i16; 2 * MONO_20MS], &mut [0; 20]).unwrap();
assert!(len <= 20);
let myvec = encoder.encode_vec(&[95_i16; 2 * MONO_20MS], 20).unwrap();
assert!(myvec.len() <= 20);
}
#[test]
fn encode_bad_rate() {
match magnum_opus::Encoder::new(48001, magnum_opus::Channels::Mono, magnum_opus::Application::Audio) {
Ok(_) => panic!("Encoder::new did not return BadArg"),
Err(err) => assert_eq!(err.code(), magnum_opus::ErrorCode::BadArg),
}
}
#[test]
fn encode_bad_buffer() {
let mut encoder = magnum_opus::Encoder::new(48000, magnum_opus::Channels::Stereo, magnum_opus::Application::Audio).unwrap();
match encoder.encode(&[1_i16; 2 * MONO_20MS], &mut [0; 0]) {
Ok(_) => panic!("encode with 0-length buffer did not return BadArg"),
Err(err) => assert_eq!(err.code(), magnum_opus::ErrorCode::BadArg),
}
}
#[test]
fn repacketizer() {
let mut rp = magnum_opus::Repacketizer::new().unwrap();
let mut out = [0; 256];
for _ in 0..2 {
let packet1 = [249, 255, 254, 255, 254];
let packet2 = [248, 255, 254];
let mut state = rp.begin();
state.cat(&packet1).unwrap();
state.cat(&packet2).unwrap();
let len = state.out(&mut out).unwrap();
assert_eq!(&out[..len], &[251, 3, 255, 254, 255, 254, 255, 254]);
}
for _ in 0..2 {
let packet = [248, 255, 254];
let state = rp.begin().cat_move(&packet).unwrap();
let packet = [249, 255, 254, 255, 254];
let state = state.cat_move(&packet).unwrap();
let len = {state}.out(&mut out).unwrap();
assert_eq!(&out[..len], &[251, 3, 255, 254, 255, 254, 255, 254]);
}
for _ in 0..2 {
let len = rp.combine(&[
&[249, 255, 254, 255, 254],
&[248, 255, 254],
], &mut out).unwrap();
assert_eq!(&out[..len], &[251, 3, 255, 254, 255, 254, 255, 254]);
}
for _ in 0..2 {
let len = rp.begin()
.cat_move(&[248, 255, 254]).unwrap()
.cat_move(&[248, 71, 71]).unwrap()
.out(&mut out).unwrap();
assert_eq!(&out[..len], &[249, 255, 254, 71, 71]);
}
}

3
libs/parity-tokio-ipc/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target
Cargo.lock
.idea

View File

@@ -0,0 +1,20 @@
language: rust
rust:
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
after_success:
- |-
[ $TRAVIS_BRANCH = master ] &&
[ $TRAVIS_PULL_REQUEST = false ] &&
cargo doc --all --no-deps &&
echo '<meta http-equiv=refresh content=0;url=parity_tokio_ipc/index.html>' > target/doc/index.html &&
pip install --user ghp-import &&
/home/travis/.local/bin/ghp-import -n target/doc &&
git push -fq https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
env:
global:
secure: eUHPLjVMSVclRcEgwgybIKZQJTBW8QDjcjgIsEhOHZn7Kpzw0+rwJVoKkvr/uqyJwyY7pHy36mWX31J8YDSbSiM3W8jXeI97sk+FTUkleqUffPzXnbnR1D4kHZlKndFIbcuO5Z+rtVgsAv5E/u1w9XR+mvgK2lfaIEay+26gBl6dl/1TxWvrwDBeMvfq1JGVDQH4Etubncpi3LSWhbRkie1AKnVnsDIY9sUYVKSnIqjxx0qW6Z7EiCdwZ8gf04LNnqyIoKDpyldotL+nJ67ZlVI2O2DrbOOt55nliFHsH4BcWZIZOyIAM4PxIwhDl8g9E55FLkkUX9VUpVtqjTu9RWkVl7rzyrSxLoBUEjguIPrpFWBwLo0FSDvplB2XCXt8x035Io5PEg6m5dVxx5iytTIbI6HQwcA0ESTuDPuAdRJMNvJS/9e2UzPukdYYaaxF6g8wSmiIQjLuZU/nGBdmAl7Uw6cFlQnyLc/GXQg0oZ+B/J8sc4W2C/Z64oB8jK72RLNTKeeWs/XSOt8NxQiNkWeFIhGqiYOPJgjBiTCLSKJPY3CUTiBT8QpAcpj1x1gsWi+5fRoXYxNig/CmeTwZjuxKNxfQIu3J+lJbNdt44x7whnwhZ/AKVuLFPNNiC2OBNpa738UY60VYDoNZyhomWSdBnz3E6i1VtdiSnujFFnc=

View File

@@ -0,0 +1,24 @@
[package]
name = "parity-tokio-ipc"
version = "0.7.2"
edition = "2018"
authors = ["NikVolf <nikvolf@gmail.com>"]
license = "MIT/Apache-2.0"
readme = "README.md"
repository = "https://github.com/nikvolf/parity-tokio-ipc"
homepage = "https://github.com/nikvolf/parity-tokio-ipc"
description = """
Interprocess communication library for tokio.
"""
[dependencies]
futures = "0.3"
log = "0.4"
mio-named-pipes = "0.1"
miow = "0.3"
rand = "0.7"
tokio = { version = "0.2", features = ["io-driver", "io-util", "uds", "stream", "rt-core", "macros", "time"] }
libc = "0.2"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winbase", "winnt", "accctrl", "aclapi", "securitybaseapi", "minwinbase", "winbase"] }

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,25 @@
Copyright (c) 2017 Nikolay Volf
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,28 @@
# parity-tokio-ipc
[![Build Status](https://travis-ci.org/NikVolf/parity-tokio-ipc.svg?branch=master)](https://travis-ci.org/NikVolf/parity-tokio-ipc)
[Documentation](https://nikvolf.github.io/parity-tokio-ipc)
This crate abstracts interprocess transport for UNIX/Windows. On UNIX it utilizes unix sockets (`tokio_uds` crate) and named pipe on windows (experimental `tokio-named-pipes` crate).
Endpoint is transport-agnostic interface for incoming connections:
```rust
let endpoint = Endpoint::new(endpoint_addr, handle).unwrap();
endpoint.incoming().for_each(|_| println!("Connection received!"));
```
And IpcStream is transport-agnostic io:
```rust
let endpoint = Endpoint::new(endpoint_addr, handle).unwrap();
endpoint.incoming().for_each(|(ipc_stream: IpcStream, _)| io::write_all(ipc_stream, b"Hello!"));
```
# License
`parity-tokio-ipc` is primarily distributed under the terms of both the MIT
license and the Apache License (Version 2.0), with portions covered by various
BSD-like licenses.
See LICENSE-APACHE, and LICENSE-MIT for details.

View File

@@ -0,0 +1,16 @@
environment:
matrix:
- TARGET: x86_64-pc-windows-msvc
install:
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe -y --default-host %TARGET%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin;C:\MinGW\bin
- rustc -vV
- cargo -vV
build: false
test_script:
- cargo test

View File

@@ -0,0 +1,24 @@
use tokio::{self, prelude::*};
use parity_tokio_ipc::Endpoint;
#[tokio::main]
async fn main() {
let path = std::env::args().nth(1).expect("Run it with server path to connect as argument");
let mut client = Endpoint::connect(&path).await
.expect("Failed to connect client.");
loop {
let mut buf = [0u8; 4];
println!("SEND: PING");
client.write_all(b"ping").await.expect("Unable to write message to client");
client.read_exact(&mut buf[..]).await.expect("Unable to read buffer");
if let Ok("pong") = std::str::from_utf8(&buf[..]) {
println!("RECEIVED: PONG");
} else {
break;
}
tokio::time::delay_for(std::time::Duration::from_secs(2)).await;
}
}

View File

@@ -0,0 +1,47 @@
use futures::StreamExt as _;
use tokio::{
prelude::*,
self,
io::split,
};
use parity_tokio_ipc::{Endpoint, SecurityAttributes};
async fn run_server(path: String) {
let mut endpoint = Endpoint::new(path);
endpoint.set_security_attributes(SecurityAttributes::allow_everyone_create().unwrap());
let mut incoming = endpoint.incoming().expect("failed to open new socket");
while let Some(result) = incoming.next().await
{
match result {
Ok(stream) => {
let (mut reader, mut writer) = split(stream);
tokio::spawn(async move {
loop {
let mut buf = [0u8; 4];
let pong_buf = b"pong";
if let Err(_) = reader.read_exact(&mut buf).await {
println!("Closing socket");
break;
}
if let Ok("ping") = std::str::from_utf8(&buf[..]) {
println!("RECIEVED: PING");
writer.write_all(pong_buf).await.expect("unable to write to socket");
println!("SEND: PONG");
}
}
});
}
_ => unreachable!("ideally")
}
};
}
#[tokio::main]
async fn main() {
let path = std::env::args().nth(1).expect("Run it with server path as argument");
run_server(path).await
}

View File

@@ -0,0 +1,7 @@
#!/bin/bash
echo "Spawning 100 processes"
for i in {1..100} ;
do
( cargo run --example client -- /tmp/test.ipc & )
done

View File

@@ -0,0 +1,154 @@
//! Tokio IPC transport. Under the hood uses Unix Domain Sockets for Linux/Mac
//! and Named Pipes for Windows.
//#![warn(missing_docs)]
//#![deny(rust_2018_idioms)]
#[cfg(windows)]
mod win;
#[cfg(not(windows))]
mod unix;
/// Endpoint for IPC transport
///
/// # Examples
///
/// ```ignore
/// use parity_tokio_ipc::{Endpoint, dummy_endpoint};
/// use futures::{future, Future, Stream, StreamExt};
/// use tokio::runtime::Runtime;
///
/// fn main() {
/// let mut runtime = Runtime::new().unwrap();
/// let mut endpoint = Endpoint::new(dummy_endpoint());
/// let server = endpoint.incoming()
/// .expect("failed to open up a new pipe/socket")
/// .for_each(|_stream| {
/// println!("Connection received");
/// futures::future::ready(())
/// });
/// runtime.block_on(server)
/// }
///```
#[cfg(windows)]
pub use win::{SecurityAttributes, Endpoint, Connection, Incoming};
#[cfg(unix)]
pub use unix::{SecurityAttributes, Endpoint, Connection, Incoming};
/// For testing/examples
pub fn dummy_endpoint() -> String {
let num: u64 = rand::Rng::gen(&mut rand::thread_rng());
if cfg!(windows) {
format!(r"\\.\pipe\my-pipe-{}", num)
} else {
format!(r"/tmp/my-uds-{}", num)
}
}
#[cfg(test)]
mod tests {
use tokio::prelude::*;
use futures::{channel::oneshot, StreamExt as _, FutureExt as _};
use std::time::Duration;
use tokio::{
self,
io::split,
};
use super::{dummy_endpoint, Endpoint, SecurityAttributes};
use std::path::Path;
use futures::future::{Either, select, ready};
async fn run_server(path: String) {
let path = path.to_owned();
let mut endpoint = Endpoint::new(path);
endpoint.set_security_attributes(
SecurityAttributes::empty()
.set_mode(0o777)
.unwrap()
);
let mut incoming = endpoint.incoming().expect("failed to open up a new socket");
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
let (mut reader, mut writer) = split(stream);
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).await.expect("unable to read from socket");
writer.write_all(&buf[..]).await.expect("unable to write to socket");
}
_ => unreachable!("ideally")
}
};
}
#[tokio::test]
async fn smoke_test() {
let path = dummy_endpoint();
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let server = select(Box::pin(run_server(path.clone())), shutdown_rx)
.then(|either| {
match either {
Either::Right((_, server)) => {
drop(server);
}
_ => unreachable!("also ideally")
};
ready(())
});
tokio::spawn(server);
tokio::time::delay_for(Duration::from_secs(2)).await;
println!("Connecting to client 0...");
let mut client_0 = Endpoint::connect(&path).await
.expect("failed to open client_0");
tokio::time::delay_for(Duration::from_secs(2)).await;
println!("Connecting to client 1...");
let mut client_1 = Endpoint::connect(&path).await
.expect("failed to open client_1");
let msg = b"hello";
let mut rx_buf = vec![0u8; msg.len()];
client_0.write_all(msg).await.expect("Unable to write message to client");
client_0.read_exact(&mut rx_buf).await.expect("Unable to read message from client");
let mut rx_buf2 = vec![0u8; msg.len()];
client_1.write_all(msg).await.expect("Unable to write message to client");
client_1.read_exact(&mut rx_buf2).await.expect("Unable to read message from client");
assert_eq!(rx_buf, msg);
assert_eq!(rx_buf2, msg);
// shutdown server
if let Ok(()) = shutdown_tx.send(()) {
// wait one second for the file to be deleted.
tokio::time::delay_for(Duration::from_secs(1)).await;
let path = Path::new(&path);
// assert that it has
assert!(!path.exists());
}
}
#[cfg(windows)]
fn create_pipe_with_permissions(attr: SecurityAttributes) -> ::std::io::Result<()> {
let path = dummy_endpoint();
let mut endpoint = Endpoint::new(path);
endpoint.set_security_attributes(attr);
endpoint.incoming().map(|_| ())
}
#[cfg(windows)]
#[tokio::test]
async fn test_pipe_permissions() {
create_pipe_with_permissions(SecurityAttributes::empty())
.expect("failed with no attributes");
create_pipe_with_permissions(SecurityAttributes::allow_everyone_create().unwrap())
.expect("failed with attributes for creating");
create_pipe_with_permissions(SecurityAttributes::empty().allow_everyone_connect().unwrap())
.expect("failed with attributes for connecting");
}
}

View File

@@ -0,0 +1,163 @@
use libc::chmod;
use std::ffi::CString;
use std::io::{self, Error};
use tokio::prelude::*;
use tokio::net::{UnixListener, UnixStream};
use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::mem::MaybeUninit;
/// Socket permissions and ownership on UNIX
pub struct SecurityAttributes {
// read/write permissions for owner, group and others in unix octal.
mode: Option<u16>
}
impl SecurityAttributes {
/// New default security attributes.
pub fn empty() -> Self {
SecurityAttributes {
mode: None
}
}
/// New security attributes that allow everyone to connect.
pub fn allow_everyone_connect(mut self) -> io::Result<Self> {
self.mode = Some(0o777);
Ok(self)
}
/// Set a custom permission on the socket
pub fn set_mode(mut self, mode: u16) -> io::Result<Self> {
self.mode = Some(mode);
Ok(self)
}
/// New security attributes that allow everyone to create.
pub fn allow_everyone_create() -> io::Result<Self> {
Ok(SecurityAttributes {
mode: None
})
}
/// called in unix, after server socket has been created
/// will apply security attributes to the socket.
pub(crate) unsafe fn apply_permissions(&self, path: &str) -> io::Result<()> {
let path = CString::new(path.to_owned())?;
if let Some(mode) = self.mode {
if chmod(path.as_ptr(), mode as _) == -1 {
return Err(Error::last_os_error())
}
}
Ok(())
}
}
/// Endpoint implementation for unix systems
pub struct Endpoint {
path: String,
security_attributes: SecurityAttributes,
}
pub struct Incoming {
socket: UnixListener,
}
impl Incoming {
pub async fn next(&mut self) -> Option<io::Result<Connection>> {
match self.socket.accept().await {
Ok((stream, _)) => Some(Ok(Connection::wrap(stream))),
Err(err) => Some(Err(err))
}
}
}
impl Endpoint {
/// Stream of incoming connections
pub fn incoming(&mut self) -> io::Result<Incoming> {
unsafe {
// the call to bind in `inner()` creates the file
// `apply_permission()` will set the file permissions.
self.security_attributes.apply_permissions(&self.path)?;
};
let socket = self.inner()?;
Ok(Incoming { socket })
}
/// Inner platform-dependant state of the endpoint
fn inner(&self) -> io::Result<UnixListener> {
UnixListener::bind(&self.path)
}
/// Set security attributes for the connection
pub fn set_security_attributes(&mut self, security_attributes: SecurityAttributes) {
self.security_attributes = security_attributes;
}
/// Make new connection using the provided path and running event pool
pub async fn connect<P: AsRef<Path>>(path: P) -> io::Result<Connection> {
Ok(Connection::wrap(UnixStream::connect(path.as_ref()).await?))
}
/// Returns the path of the endpoint.
pub fn path(&self) -> &str {
&self.path
}
/// New IPC endpoint at the given path
pub fn new(path: String) -> Self {
Endpoint {
path,
security_attributes: SecurityAttributes::empty(),
}
}
}
/// IPC connection.
pub struct Connection {
inner: UnixStream,
}
impl Connection {
fn wrap(stream: UnixStream) -> Self {
Self { inner: stream }
}
}
impl AsyncRead for Connection {
unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [MaybeUninit<u8>]) -> bool {
self.inner.prepare_uninitialized_buffer(buf)
}
fn poll_read(
self: Pin<&mut Self>,
ctx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_read(ctx, buf)
}
}
impl AsyncWrite for Connection {
fn poll_write(
self: Pin<&mut Self>,
ctx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_write(ctx, buf)
}
fn poll_flush(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_flush(ctx)
}
fn poll_shutdown(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_shutdown(ctx)
}
}

View File

@@ -0,0 +1,488 @@
use winapi::shared::winerror::ERROR_SUCCESS;
use winapi::um::accctrl::*;
use winapi::um::aclapi::*;
use winapi::um::minwinbase::{LPTR, PSECURITY_ATTRIBUTES, SECURITY_ATTRIBUTES};
use winapi::um::securitybaseapi::*;
use winapi::um::winbase::{LocalAlloc, LocalFree};
use winapi::um::winnt::*;
use std::io;
use std::marker;
use std::mem;
use std::ptr;
use futures::Stream;
use tokio::prelude::*;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::path::Path;
use std::mem::MaybeUninit;
use tokio::io::PollEvented;
type NamedPipe = PollEvented<mio_named_pipes::NamedPipe>;
const PIPE_AVAILABILITY_TIMEOUT: u64 = 5000;
/// Endpoint implementation for windows
pub struct Endpoint {
path: String,
security_attributes: SecurityAttributes,
}
impl Endpoint {
/// Stream of incoming connections
pub fn incoming(mut self) -> io::Result<Incoming> {
let pipe = self.inner()?;
Ok(Incoming {
path: self.path.clone(),
inner: NamedPipeSupport {
path: self.path,
pipe,
security_attributes: self.security_attributes,
},
})
}
/// Inner platform-dependant state of the endpoint
fn inner(&mut self) -> io::Result<NamedPipe> {
use miow::pipe::NamedPipeBuilder;
use std::os::windows::io::*;
let raw_handle = unsafe {
NamedPipeBuilder::new(&self.path)
.first(true)
.inbound(true)
.outbound(true)
.out_buffer_size(65536)
.in_buffer_size(65536)
.with_security_attributes(self.security_attributes.as_ptr())?
.into_raw_handle()
};
let mio_pipe = unsafe { mio_named_pipes::NamedPipe::from_raw_handle(raw_handle) };
NamedPipe::new(mio_pipe)
}
/// Set security attributes for the connection
pub fn set_security_attributes(&mut self, security_attributes: SecurityAttributes) {
self.security_attributes = security_attributes;
}
/// Returns the path of the endpoint.
pub fn path(&self) -> &str {
&self.path
}
/// Make new connection using the provided path and running event pool.
pub async fn connect<P: AsRef<Path>>(path: P) -> io::Result<Connection> {
Ok(Connection::wrap(Self::connect_inner(path.as_ref())?))
}
fn connect_inner(path: &Path) -> io::Result<NamedPipe> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::{FromRawHandle, IntoRawHandle};
use winapi::um::winbase::FILE_FLAG_OVERLAPPED;
// Wait for the pipe to become available or fail after 5 seconds.
miow::pipe::NamedPipe::wait(
path,
Some(std::time::Duration::from_millis(PIPE_AVAILABILITY_TIMEOUT)),
)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(FILE_FLAG_OVERLAPPED)
.open(path)?;
let mio_pipe =
unsafe { mio_named_pipes::NamedPipe::from_raw_handle(file.into_raw_handle()) };
let pipe = NamedPipe::new(mio_pipe)?;
Ok(pipe)
}
/// New IPC endpoint at the given path
pub fn new(path: String) -> Self {
Endpoint {
path,
security_attributes: SecurityAttributes::empty(),
}
}
}
struct NamedPipeSupport {
path: String,
pipe: NamedPipe,
security_attributes: SecurityAttributes,
}
impl NamedPipeSupport {
fn replacement_pipe(&mut self) -> io::Result<NamedPipe> {
use miow::pipe::NamedPipeBuilder;
use std::os::windows::io::*;
let raw_handle = unsafe {
NamedPipeBuilder::new(&self.path)
.first(false)
.inbound(true)
.outbound(true)
.out_buffer_size(65536)
.in_buffer_size(65536)
.with_security_attributes(self.security_attributes.as_ptr())?
.into_raw_handle()
};
let mio_pipe = unsafe { mio_named_pipes::NamedPipe::from_raw_handle(raw_handle) };
NamedPipe::new(mio_pipe)
}
}
/// Stream of incoming connections
pub struct Incoming {
#[allow(dead_code)]
path: String,
inner: NamedPipeSupport,
}
impl Stream for Incoming {
type Item = tokio::io::Result<Connection>;
fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.inner.pipe.get_ref().connect() {
Ok(()) => {
log::trace!("Incoming connection polled successfully");
let new_listener = self.inner.replacement_pipe()?;
Poll::Ready(
Some(Ok(Connection::wrap(std::mem::replace(&mut self.inner.pipe, new_listener))))
)
}
Err(e) => {
if e.kind() == io::ErrorKind::WouldBlock {
self.inner.pipe.clear_write_ready(ctx);
Poll::Pending
} else {
Poll::Ready(Some(Err(e)))
}
}
}
}
}
/// IPC connection.
pub struct Connection {
inner: NamedPipe,
}
impl Connection {
pub fn wrap(pipe: NamedPipe) -> Self {
Self { inner: pipe }
}
}
impl AsyncRead for Connection {
unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [MaybeUninit<u8>]) -> bool {
self.inner.prepare_uninitialized_buffer(buf)
}
fn poll_read(
self: Pin<&mut Self>,
ctx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_read(ctx, buf)
}
}
impl AsyncWrite for Connection {
fn poll_write(
self: Pin<&mut Self>,
ctx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_write(ctx, buf)
}
fn poll_flush(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_flush(ctx)
}
fn poll_shutdown(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let this = Pin::into_inner(self);
Pin::new(&mut this.inner).poll_shutdown(ctx)
}
}
/// Security attributes.
pub struct SecurityAttributes {
attributes: Option<InnerAttributes>,
}
impl SecurityAttributes {
/// New default security attributes.
pub fn empty() -> SecurityAttributes {
SecurityAttributes { attributes: None }
}
/// New default security attributes that allow everyone to connect.
pub fn allow_everyone_connect(&self) -> io::Result<SecurityAttributes> {
let attributes = Some(InnerAttributes::allow_everyone(
GENERIC_READ | FILE_WRITE_DATA,
)?);
Ok(SecurityAttributes { attributes })
}
/// Set a custom permission on the socket
pub fn set_mode(self, _mode: u32) -> io::Result<Self> {
// for now, does nothing.
Ok(self)
}
/// New default security attributes that allow everyone to create.
pub fn allow_everyone_create() -> io::Result<SecurityAttributes> {
let attributes = Some(InnerAttributes::allow_everyone(
GENERIC_READ | GENERIC_WRITE,
)?);
Ok(SecurityAttributes { attributes })
}
/// Return raw handle of security attributes.
pub(crate) unsafe fn as_ptr(&mut self) -> PSECURITY_ATTRIBUTES {
match self.attributes.as_mut() {
Some(attributes) => attributes.as_ptr(),
None => ptr::null_mut(),
}
}
}
unsafe impl Send for SecurityAttributes {}
struct Sid {
sid_ptr: PSID,
}
impl Sid {
fn everyone_sid() -> io::Result<Sid> {
let mut sid_ptr = ptr::null_mut();
let result = unsafe {
AllocateAndInitializeSid(
SECURITY_WORLD_SID_AUTHORITY.as_mut_ptr() as *mut _,
1,
SECURITY_WORLD_RID,
0,
0,
0,
0,
0,
0,
0,
&mut sid_ptr,
)
};
if result == 0 {
Err(io::Error::last_os_error())
} else {
Ok(Sid { sid_ptr })
}
}
// Unsafe - the returned pointer is only valid for the lifetime of self.
unsafe fn as_ptr(&self) -> PSID {
self.sid_ptr
}
}
impl Drop for Sid {
fn drop(&mut self) {
if !self.sid_ptr.is_null() {
unsafe {
FreeSid(self.sid_ptr);
}
}
}
}
struct AceWithSid<'a> {
explicit_access: EXPLICIT_ACCESS_W,
_marker: marker::PhantomData<&'a Sid>,
}
impl<'a> AceWithSid<'a> {
fn new(sid: &'a Sid, trustee_type: u32) -> AceWithSid<'a> {
let mut explicit_access = unsafe { mem::zeroed::<EXPLICIT_ACCESS_W>() };
explicit_access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
explicit_access.Trustee.TrusteeType = trustee_type;
explicit_access.Trustee.ptstrName = unsafe { sid.as_ptr() as *mut _ };
AceWithSid {
explicit_access,
_marker: marker::PhantomData,
}
}
fn set_access_mode(&mut self, access_mode: u32) -> &mut Self {
self.explicit_access.grfAccessMode = access_mode;
self
}
fn set_access_permissions(&mut self, access_permissions: u32) -> &mut Self {
self.explicit_access.grfAccessPermissions = access_permissions;
self
}
fn allow_inheritance(&mut self, inheritance_flags: u32) -> &mut Self {
self.explicit_access.grfInheritance = inheritance_flags;
self
}
}
struct Acl {
acl_ptr: PACL,
}
impl Acl {
fn empty() -> io::Result<Acl> {
Self::new(&mut [])
}
fn new(entries: &mut [AceWithSid<'_>]) -> io::Result<Acl> {
let mut acl_ptr = ptr::null_mut();
let result = unsafe {
SetEntriesInAclW(
entries.len() as u32,
entries.as_mut_ptr() as *mut _,
ptr::null_mut(),
&mut acl_ptr,
)
};
if result != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(result as i32));
}
Ok(Acl { acl_ptr })
}
unsafe fn as_ptr(&self) -> PACL {
self.acl_ptr
}
}
impl Drop for Acl {
fn drop(&mut self) {
if !self.acl_ptr.is_null() {
unsafe { LocalFree(self.acl_ptr as *mut _) };
}
}
}
struct SecurityDescriptor {
descriptor_ptr: PSECURITY_DESCRIPTOR,
}
impl SecurityDescriptor {
fn new() -> io::Result<Self> {
let descriptor_ptr = unsafe { LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH) };
if descriptor_ptr.is_null() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Failed to allocate security descriptor",
));
}
if unsafe {
InitializeSecurityDescriptor(descriptor_ptr, SECURITY_DESCRIPTOR_REVISION) == 0
} {
return Err(io::Error::last_os_error());
};
Ok(SecurityDescriptor { descriptor_ptr })
}
fn set_dacl(&mut self, acl: &Acl) -> io::Result<()> {
if unsafe {
SetSecurityDescriptorDacl(self.descriptor_ptr, true as i32, acl.as_ptr(), false as i32)
== 0
} {
return Err(io::Error::last_os_error());
}
Ok(())
}
unsafe fn as_ptr(&self) -> PSECURITY_DESCRIPTOR {
self.descriptor_ptr
}
}
impl Drop for SecurityDescriptor {
fn drop(&mut self) {
if !self.descriptor_ptr.is_null() {
unsafe { LocalFree(self.descriptor_ptr) };
self.descriptor_ptr = ptr::null_mut();
}
}
}
struct InnerAttributes {
descriptor: SecurityDescriptor,
acl: Acl,
attrs: SECURITY_ATTRIBUTES,
}
impl InnerAttributes {
fn empty() -> io::Result<InnerAttributes> {
let descriptor = SecurityDescriptor::new()?;
let mut attrs = unsafe { mem::zeroed::<SECURITY_ATTRIBUTES>() };
attrs.nLength = mem::size_of::<SECURITY_ATTRIBUTES>() as u32;
attrs.lpSecurityDescriptor = unsafe { descriptor.as_ptr() };
attrs.bInheritHandle = false as i32;
let acl = Acl::empty().expect("this should never fail");
Ok(InnerAttributes {
acl,
descriptor,
attrs,
})
}
fn allow_everyone(permissions: u32) -> io::Result<InnerAttributes> {
let mut attributes = Self::empty()?;
let sid = Sid::everyone_sid()?;
let mut everyone_ace = AceWithSid::new(&sid, TRUSTEE_IS_WELL_KNOWN_GROUP);
everyone_ace
.set_access_mode(SET_ACCESS)
.set_access_permissions(permissions)
.allow_inheritance(false as u32);
let mut entries = vec![everyone_ace];
attributes.acl = Acl::new(&mut entries)?;
attributes.descriptor.set_dacl(&attributes.acl)?;
Ok(attributes)
}
unsafe fn as_ptr(&mut self) -> PSECURITY_ATTRIBUTES {
&mut self.attrs as *mut _
}
}
#[cfg(test)]
mod test {
use super::SecurityAttributes;
#[test]
fn test_allow_everyone_everything() {
SecurityAttributes::allow_everyone_create()
.expect("failed to create security attributes that allow everyone to create a pipe");
}
#[test]
fn test_allow_eveyone_read_write() {
SecurityAttributes::empty()
.allow_everyone_connect()
.expect("failed to create security attributes that allow everyone to read and write to/from a pipe");
}
}

4
libs/pulsectl/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
**/target
/target
**/*.rs.bk
.idea/

129
libs/pulsectl/Cargo.lock generated Normal file
View File

@@ -0,0 +1,129 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "libc"
version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8"
[[package]]
name = "libpulse-binding"
version = "2.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe925a4d3a96961316c9c1488f97a95938a6093f0d4691eec888776057ce965e"
dependencies = [
"libc",
"libpulse-sys",
"num-derive",
"num-traits",
"winapi",
]
[[package]]
name = "libpulse-sys"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9073c83dda6aff9b611dc368e8db6e0aa29027546d8800a18b4417e182b4d5"
dependencies = [
"libc",
"num-derive",
"num-traits",
"pkg-config",
"winapi",
]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "pkg-config"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
[[package]]
name = "proc-macro2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rust-pulsectl"
version = "0.2.9"
dependencies = [
"libpulse-binding",
]
[[package]]
name = "syn"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b4f34193997d92804d359ed09953e25d5138df6bcc055a71bf68ee89fdf9223"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

19
libs/pulsectl/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "rust-pulsectl"
version = "0.2.10"
authors = ["Kristopher Ruzic <krruzic@gmail.com>"]
edition = "2018"
license = "GPL-3.0+"
description = "A higher level API for libpulse_binding"
readme = "README.md"
keywords = ["pulse", "pulseaudio", "binding", "audio", "api"]
categories = ["api-bindings", "multimedia::audio"]
homepage = "https://github.com/krruzic/pulsectl"
repository = "https://github.com/krruzic/pulsectl"
[lib]
name = "pulsectl"
path = "src/lib.rs"
[dependencies]
libpulse-binding = "2.21"

13
libs/pulsectl/LICENSE.md Normal file
View File

@@ -0,0 +1,13 @@
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

43
libs/pulsectl/README.md Normal file
View File

@@ -0,0 +1,43 @@
Rust PulsecAudio API
====================
`pulsectl-rust` is a API wrapper for `libpulse_binding` to make pulseaudio application development easier.
This is a wrapper around the introspector, and thus this library is only capable of modifying PulseAudio data (changing volume, routing applications and muting right now).
### Usage
Add this to your `Cargo.toml`:
```toml
[dependencies]
rust-pulsectl = "0.2.6"
```
Then, connect to PulseAudio by creating a `SinkController` for audio playback devices and apps or a `SourceController` for audio recording devices and apps.
```rust
// Simple application that lists all playback devices and their status
// See examples/change_device_vol.rs for a more complete example
extern crate pulsectl;
use std::io;
use pulsectl::controllers::SinkController;
use pulsectl::controllers::DeviceControl;
fn main() {
// create handler that calls functions on playback devices and apps
let mut handler = SinkController::create();
let devices = handler
.list_devices()
.expect("Could not get list of playback devices");
println!("Playback Devices");
for dev in devices.clone() {
println!(
"[{}] {}, [Volume: {}]",
dev.index,
dev.description.as_ref().unwrap(),
dev.volume.print()
);
}
}
```

View File

@@ -0,0 +1,34 @@
extern crate pulsectl;
use std::io;
use pulsectl::controllers::DeviceControl;
use pulsectl::controllers::SinkController;
fn main() {
// create handler that calls functions on playback devices and apps
let mut handler = SinkController::create().unwrap();
let devices = handler
.list_devices()
.expect("Could not get list of playback devices");
println!("Playback Devices");
for dev in devices.clone() {
println!(
"[{}] {}, [Volume: {}]",
dev.index,
dev.description.as_ref().unwrap(),
dev.volume.print()
);
}
let mut selection = String::new();
io::stdin()
.read_line(&mut selection)
.expect("error: unable to read user input");
for dev in devices.clone() {
if let true = selection.trim() == dev.index.to_string() {
handler.increase_device_volume_by_percent(dev.index, 0.05);
}
}
}

View File

@@ -0,0 +1,51 @@
use std::fmt;
use crate::PulseCtlError;
/// if the error occurs within the Mainloop, we bubble up the error with
/// this conversion
impl From<PulseCtlError> for ControllerError {
fn from(error: super::errors::PulseCtlError) -> Self {
ControllerError {
error: ControllerErrorType::PulseCtlError,
message: format!("{:?}", error),
}
}
}
impl fmt::Debug for ControllerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut error_string = String::new();
match self.error {
ControllerErrorType::PulseCtlError => {
error_string.push_str("PulseCtlError");
}
ControllerErrorType::GetInfoError => {
error_string.push_str("GetInfoError");
}
}
write!(f, "[{}]: {}", error_string, self.message)
}
}
pub(crate) enum ControllerErrorType {
PulseCtlError,
GetInfoError,
}
/// Error thrown while fetching data from pulseaudio,
/// has two variants: PulseCtlError for when PulseAudio returns an error code
/// and GetInfoError when a request for data fails for whatever reason
pub struct ControllerError {
error: ControllerErrorType,
message: String,
}
impl ControllerError {
pub(crate) fn new(err: ControllerErrorType, msg: &str) -> Self {
ControllerError {
error: err,
message: msg.to_string(),
}
}
}

View File

@@ -0,0 +1,595 @@
/// Source = microphone etc. something that takes in audio
/// Source Output = application consuming that audio
///
/// Sink = headphones etc. something that plays out audio
/// Sink Input = application producing that audio
/// When you create a `SinkController`, you are working with audio playback devices and applications
/// if you want to manipulate recording devices such as microphone volume,
/// you'll need to use a `SourceController`. Both of these implement the same api, defined by
/// the traits DeviceControl and AppControl
use std::cell::RefCell;
use std::clone::Clone;
use std::rc::Rc;
use pulse::{
callbacks::ListResult,
context::introspect,
volume::{ChannelVolumes, Volume},
};
use errors::{ControllerError, ControllerErrorType::*};
use types::{ApplicationInfo, DeviceInfo, ServerInfo};
use crate::Handler;
pub(crate) mod errors;
pub mod types;
pub trait DeviceControl<T> {
fn get_default_device(&mut self) -> Result<T, ControllerError>;
fn set_default_device(&mut self, name: &str) -> Result<bool, ControllerError>;
fn list_devices(&mut self) -> Result<Vec<T>, ControllerError>;
fn get_device_by_index(&mut self, index: u32) -> Result<T, ControllerError>;
fn get_device_by_name(&mut self, name: &str) -> Result<T, ControllerError>;
fn set_device_volume_by_index(&mut self, index: u32, volume: &ChannelVolumes);
fn set_device_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes);
fn increase_device_volume_by_percent(&mut self, index: u32, delta: f64);
fn decrease_device_volume_by_percent(&mut self, index: u32, delta: f64);
}
pub trait AppControl<T> {
fn list_applications(&mut self) -> Result<Vec<T>, ControllerError>;
fn get_app_by_index(&mut self, index: u32) -> Result<T, ControllerError>;
fn increase_app_volume_by_percent(&mut self, index: u32, delta: f64);
fn decrease_app_volume_by_percent(&mut self, index: u32, delta: f64);
fn move_app_by_index(
&mut self,
stream_index: u32,
device_index: u32,
) -> Result<bool, ControllerError>;
fn move_app_by_name(
&mut self,
stream_index: u32,
device_name: &str,
) -> Result<bool, ControllerError>;
fn set_app_mute(&mut self, index: u32, mute: bool) -> Result<bool, ControllerError>;
}
fn volume_from_percent(volume: f64) -> f64 {
(volume * 100.0) * (f64::from(pulse::volume::VOLUME_NORM.0) / 100.0)
}
pub struct SinkController {
pub handler: Handler,
}
impl SinkController {
pub fn create() -> Result<Self, ControllerError> {
let handler = Handler::connect("SinkController")?;
Ok(SinkController { handler })
}
pub fn get_server_info(&mut self) -> Result<ServerInfo, ControllerError> {
let server = Rc::new(RefCell::new(Some(None)));
let server_ref = server.clone();
let op = self.handler.introspect.get_server_info(move |res| {
server_ref
.borrow_mut()
.as_mut()
.unwrap()
.replace(res.into());
});
self.handler.wait_for_operation(op)?;
let mut result = server.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting information about the server",
))
}
}
impl DeviceControl<DeviceInfo> for SinkController {
fn get_default_device(&mut self) -> Result<DeviceInfo, ControllerError> {
let server_info = self.get_server_info();
match server_info {
Ok(info) => self.get_device_by_name(info.default_sink_name.unwrap().as_ref()),
Err(e) => Err(e),
}
}
fn set_default_device(&mut self, name: &str) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self
.handler
.context
.borrow_mut()
.set_default_sink(name, move |res| success_ref.borrow_mut().clone_from(&res));
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
fn list_devices(&mut self) -> Result<Vec<DeviceInfo>, ControllerError> {
let list = Rc::new(RefCell::new(Some(Vec::new())));
let list_ref = list.clone();
let op = self.handler.introspect.get_sink_info_list(
move |sink_list: ListResult<&introspect::SinkInfo>| {
if let ListResult::Item(item) = sink_list {
list_ref.borrow_mut().as_mut().unwrap().push(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = list.borrow_mut();
result.take().ok_or(ControllerError::new(
GetInfoError,
"Error getting device list",
))
}
fn get_device_by_index(&mut self, index: u32) -> Result<DeviceInfo, ControllerError> {
let device = Rc::new(RefCell::new(Some(None)));
let dev_ref = device.clone();
let op = self.handler.introspect.get_sink_info_by_index(
index,
move |sink_list: ListResult<&introspect::SinkInfo>| {
if let ListResult::Item(item) = sink_list {
dev_ref.borrow_mut().as_mut().unwrap().replace(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = device.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting requested device",
))
}
fn get_device_by_name(&mut self, name: &str) -> Result<DeviceInfo, ControllerError> {
let device = Rc::new(RefCell::new(Some(None)));
let dev_ref = device.clone();
let op = self.handler.introspect.get_sink_info_by_name(
name,
move |sink_list: ListResult<&introspect::SinkInfo>| {
if let ListResult::Item(item) = sink_list {
dev_ref.borrow_mut().as_mut().unwrap().replace(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = device.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting requested device",
))
}
fn set_device_volume_by_index(&mut self, index: u32, volume: &ChannelVolumes) {
let op = self
.handler
.introspect
.set_sink_volume_by_index(index, volume, None);
self.handler.wait_for_operation(op).ok();
}
fn set_device_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes) {
let op = self
.handler
.introspect
.set_sink_volume_by_name(name, volume, None);
self.handler.wait_for_operation(op).ok();
}
fn increase_device_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut dev_ref) = self.get_device_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = dev_ref.volume.increase(new_vol) {
let op = self
.handler
.introspect
.set_sink_volume_by_index(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
fn decrease_device_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut dev_ref) = self.get_device_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = dev_ref.volume.decrease(new_vol) {
let op = self
.handler
.introspect
.set_sink_volume_by_index(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
}
impl AppControl<ApplicationInfo> for SinkController {
fn list_applications(&mut self) -> Result<Vec<ApplicationInfo>, ControllerError> {
let list = Rc::new(RefCell::new(Some(Vec::new())));
let list_ref = list.clone();
let op = self.handler.introspect.get_sink_input_info_list(
move |sink_list: ListResult<&introspect::SinkInputInfo>| {
if let ListResult::Item(item) = sink_list {
list_ref.borrow_mut().as_mut().unwrap().push(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = list.borrow_mut();
result.take().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
fn get_app_by_index(&mut self, index: u32) -> Result<ApplicationInfo, ControllerError> {
let app = Rc::new(RefCell::new(Some(None)));
let app_ref = app.clone();
let op = self.handler.introspect.get_sink_input_info(
index,
move |sink_list: ListResult<&introspect::SinkInputInfo>| {
if let ListResult::Item(item) = sink_list {
app_ref.borrow_mut().as_mut().unwrap().replace(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = app.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting requested app",
))
}
fn increase_app_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut app_ref) = self.get_app_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = app_ref.volume.increase(new_vol) {
let op = self
.handler
.introspect
.set_sink_input_volume(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
fn decrease_app_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut app_ref) = self.get_app_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = app_ref.volume.decrease(new_vol) {
let op = self
.handler
.introspect
.set_sink_input_volume(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
fn move_app_by_index(
&mut self,
stream_index: u32,
device_index: u32,
) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self.handler.introspect.move_sink_input_by_index(
stream_index,
device_index,
Some(Box::new(move |res| {
success_ref.borrow_mut().clone_from(&res)
})),
);
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
fn move_app_by_name(
&mut self,
stream_index: u32,
device_name: &str,
) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self.handler.introspect.move_sink_input_by_name(
stream_index,
device_name,
Some(Box::new(move |res| {
success_ref.borrow_mut().clone_from(&res)
})),
);
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
fn set_app_mute(&mut self, index: u32, mute: bool) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self.handler.introspect.set_sink_input_mute(
index,
mute,
Some(Box::new(move |res| {
success_ref.borrow_mut().clone_from(&res)
})),
);
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
}
pub struct SourceController {
pub handler: Handler,
}
impl SourceController {
pub fn create() -> Result<Self, ControllerError> {
let handler = Handler::connect("SourceController")?;
Ok(SourceController { handler })
}
pub fn get_server_info(&mut self) -> Result<ServerInfo, ControllerError> {
let server = Rc::new(RefCell::new(Some(None)));
let server_ref = server.clone();
let op = self.handler.introspect.get_server_info(move |res| {
server_ref
.borrow_mut()
.as_mut()
.unwrap()
.replace(res.into());
});
self.handler.wait_for_operation(op)?;
let mut result = server.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
}
impl DeviceControl<DeviceInfo> for SourceController {
fn get_default_device(&mut self) -> Result<DeviceInfo, ControllerError> {
let server_info = self.get_server_info();
match server_info {
Ok(info) => self.get_device_by_name(info.default_sink_name.unwrap().as_ref()),
Err(e) => Err(e),
}
}
fn set_default_device(&mut self, name: &str) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self
.handler
.context
.borrow_mut()
.set_default_source(name, move |res| success_ref.borrow_mut().clone_from(&res));
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
fn list_devices(&mut self) -> Result<Vec<DeviceInfo>, ControllerError> {
let list = Rc::new(RefCell::new(Some(Vec::new())));
let list_ref = list.clone();
let op = self.handler.introspect.get_source_info_list(
move |sink_list: ListResult<&introspect::SourceInfo>| {
if let ListResult::Item(item) = sink_list {
list_ref.borrow_mut().as_mut().unwrap().push(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = list.borrow_mut();
result.take().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
fn get_device_by_index(&mut self, index: u32) -> Result<DeviceInfo, ControllerError> {
let device = Rc::new(RefCell::new(Some(None)));
let dev_ref = device.clone();
let op = self.handler.introspect.get_source_info_by_index(
index,
move |sink_list: ListResult<&introspect::SourceInfo>| {
if let ListResult::Item(item) = sink_list {
dev_ref.borrow_mut().as_mut().unwrap().replace(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = device.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
fn get_device_by_name(&mut self, name: &str) -> Result<DeviceInfo, ControllerError> {
let device = Rc::new(RefCell::new(Some(None)));
let dev_ref = device.clone();
let op = self.handler.introspect.get_source_info_by_name(
name,
move |sink_list: ListResult<&introspect::SourceInfo>| {
if let ListResult::Item(item) = sink_list {
dev_ref.borrow_mut().as_mut().unwrap().replace(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = device.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
fn set_device_volume_by_index(&mut self, index: u32, volume: &ChannelVolumes) {
let op = self
.handler
.introspect
.set_source_volume_by_index(index, volume, None);
self.handler.wait_for_operation(op).ok();
}
fn set_device_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes) {
let op = self
.handler
.introspect
.set_source_volume_by_name(name, volume, None);
self.handler.wait_for_operation(op).ok();
}
fn increase_device_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut dev_ref) = self.get_device_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = dev_ref.volume.increase(new_vol) {
let op = self
.handler
.introspect
.set_source_volume_by_index(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
fn decrease_device_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut dev_ref) = self.get_device_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = dev_ref.volume.decrease(new_vol) {
let op = self
.handler
.introspect
.set_source_volume_by_index(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
}
impl AppControl<ApplicationInfo> for SourceController {
fn list_applications(&mut self) -> Result<Vec<ApplicationInfo>, ControllerError> {
let list = Rc::new(RefCell::new(Some(Vec::new())));
let list_ref = list.clone();
let op = self.handler.introspect.get_source_output_info_list(
move |sink_list: ListResult<&introspect::SourceOutputInfo>| {
if let ListResult::Item(item) = sink_list {
list_ref.borrow_mut().as_mut().unwrap().push(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = list.borrow_mut();
result.take().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
fn get_app_by_index(&mut self, index: u32) -> Result<ApplicationInfo, ControllerError> {
let app = Rc::new(RefCell::new(Some(None)));
let app_ref = app.clone();
let op = self.handler.introspect.get_source_output_info(
index,
move |sink_list: ListResult<&introspect::SourceOutputInfo>| {
if let ListResult::Item(item) = sink_list {
app_ref.borrow_mut().as_mut().unwrap().replace(item.into());
}
},
);
self.handler.wait_for_operation(op)?;
let mut result = app.borrow_mut();
result.take().unwrap().ok_or(ControllerError::new(
GetInfoError,
"Error getting application list",
))
}
fn increase_app_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut app_ref) = self.get_app_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = app_ref.volume.increase(new_vol) {
let op = self
.handler
.introspect
.set_source_output_volume(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
fn decrease_app_volume_by_percent(&mut self, index: u32, delta: f64) {
if let Ok(mut app_ref) = self.get_app_by_index(index) {
let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32));
if let Some(volumes) = app_ref.volume.decrease(new_vol) {
let op = self
.handler
.introspect
.set_source_output_volume(index, &volumes, None);
self.handler.wait_for_operation(op).ok();
}
}
}
fn move_app_by_index(
&mut self,
stream_index: u32,
device_index: u32,
) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self.handler.introspect.move_source_output_by_index(
stream_index,
device_index,
Some(Box::new(move |res| {
success_ref.borrow_mut().clone_from(&res)
})),
);
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
fn move_app_by_name(
&mut self,
stream_index: u32,
device_name: &str,
) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self.handler.introspect.move_source_output_by_name(
stream_index,
device_name,
Some(Box::new(move |res| {
success_ref.borrow_mut().clone_from(&res)
})),
);
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
fn set_app_mute(&mut self, index: u32, mute: bool) -> Result<bool, ControllerError> {
let success = Rc::new(RefCell::new(false));
let success_ref = success.clone();
let op = self.handler.introspect.set_source_mute_by_index(
index,
mute,
Some(Box::new(move |res| {
success_ref.borrow_mut().clone_from(&res)
})),
);
self.handler.wait_for_operation(op)?;
let result = success.borrow_mut().clone();
Ok(result)
}
}

View File

@@ -0,0 +1,354 @@
use pulse::{
channelmap,
context::introspect,
def,
def::PortAvailable,
format,
proplist::Proplist,
sample,
time::MicroSeconds,
volume::{ChannelVolumes, Volume},
};
/// These structs are direct representations of what libpulse_binding gives
/// created to be copyable / cloneable for use in and out of callbacks
/// This is a wrapper around SinkPortInfo and SourcePortInfo as they have the same members
#[derive(Clone)]
pub struct DevicePortInfo {
/// Name of the sink.
pub name: Option<String>,
/// Description of this sink.
pub description: Option<String>,
/// The higher this value is, the more useful this port is as a default.
pub priority: u32,
/// A flag indicating availability status of this port.
pub available: PortAvailable,
}
impl<'a> From<&'a Box<introspect::SinkPortInfo<'a>>> for DevicePortInfo {
fn from(item: &'a Box<introspect::SinkPortInfo<'a>>) -> Self {
DevicePortInfo {
name: item.name.as_ref().map(|cow| cow.to_string()),
description: item.description.as_ref().map(|cow| cow.to_string()),
priority: item.priority,
available: item.available,
}
}
}
impl<'a> From<&'a introspect::SinkPortInfo<'a>> for DevicePortInfo {
fn from(item: &'a introspect::SinkPortInfo<'a>) -> Self {
DevicePortInfo {
name: item.name.as_ref().map(|cow| cow.to_string()),
description: item.description.as_ref().map(|cow| cow.to_string()),
priority: item.priority,
available: item.available,
}
}
}
impl<'a> From<&'a Box<introspect::SourcePortInfo<'a>>> for DevicePortInfo {
fn from(item: &'a Box<introspect::SourcePortInfo<'a>>) -> Self {
DevicePortInfo {
name: item.name.as_ref().map(|cow| cow.to_string()),
description: item.description.as_ref().map(|cow| cow.to_string()),
priority: item.priority,
available: item.available,
}
}
}
impl<'a> From<&'a introspect::SourcePortInfo<'a>> for DevicePortInfo {
fn from(item: &'a introspect::SourcePortInfo<'a>) -> Self {
DevicePortInfo {
name: item.name.as_ref().map(|cow| cow.to_string()),
description: item.description.as_ref().map(|cow| cow.to_string()),
priority: item.priority,
available: item.available,
}
}
}
/// This is a wrapper around SinkState and SourceState as they have the same values
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum DevState {
/// This state is used when the server does not support sink state introspection.
Invalid = -1,
/// Running, sink is playing and used by at least one non-corked sink-input.
Running = 0,
/// When idle, the sink is playing but there is no non-corked sink-input attached to it.
Idle = 1,
/// When suspended, actual sink access can be closed, for instance.
Suspended = 2,
}
impl<'a> From<def::SourceState> for DevState {
fn from(s: def::SourceState) -> Self {
match s {
def::SourceState::Idle => DevState::Idle,
def::SourceState::Invalid => DevState::Invalid,
def::SourceState::Running => DevState::Running,
def::SourceState::Suspended => DevState::Suspended,
}
}
}
impl<'a> From<def::SinkState> for DevState {
fn from(s: def::SinkState) -> Self {
match s {
def::SinkState::Idle => DevState::Idle,
def::SinkState::Invalid => DevState::Invalid,
def::SinkState::Running => DevState::Running,
def::SinkState::Suspended => DevState::Suspended,
}
}
}
#[derive(Clone)]
pub enum Flags {
SourceFLags(def::SourceFlagSet),
SinkFlags(def::SinkFlagSet),
}
#[derive(Clone)]
pub struct DeviceInfo {
/// Index of the sink.
pub index: u32,
/// Name of the sink.
pub name: Option<String>,
/// Description of this sink.
pub description: Option<String>,
/// Sample spec of this sink.
pub sample_spec: sample::Spec,
/// Channel map.
pub channel_map: channelmap::Map,
/// Index of the owning module of this sink, or `None` if is invalid.
pub owner_module: Option<u32>,
/// Volume of the sink.
pub volume: ChannelVolumes,
/// Mute switch of the sink.
pub mute: bool,
/// Index of the monitor source connected to this sink.
pub monitor: Option<u32>,
/// The name of the monitor source.
pub monitor_name: Option<String>,
/// Length of queued audio in the output buffer.
pub latency: MicroSeconds,
/// Driver name.
pub driver: Option<String>,
/// Flags.
pub flags: Flags,
/// Property list.
pub proplist: Proplist,
/// The latency this device has been configured to.
pub configured_latency: MicroSeconds,
/// Some kind of “base” volume that refers to unamplified/unattenuated volume in the context of
/// the output device.
pub base_volume: Volume,
/// State.
pub state: DevState,
/// Number of volume steps for sinks which do not support arbitrary volumes.
pub n_volume_steps: u32,
/// Card index, or `None` if invalid.
pub card: Option<u32>,
/// Set of available ports.
pub ports: Vec<DevicePortInfo>,
// Pointer to active port in the set, or None.
pub active_port: Option<DevicePortInfo>,
/// Set of formats supported by the sink.
pub formats: Vec<format::Info>,
}
impl<'a> From<&'a introspect::SinkInfo<'a>> for DeviceInfo {
fn from(item: &'a introspect::SinkInfo<'a>) -> Self {
DeviceInfo {
name: item.name.as_ref().map(|cow| cow.to_string()),
index: item.index,
description: item.description.as_ref().map(|cow| cow.to_string()),
sample_spec: item.sample_spec,
channel_map: item.channel_map,
owner_module: item.owner_module,
volume: item.volume,
mute: item.mute,
monitor: Some(item.monitor_source),
monitor_name: item.monitor_source_name.as_ref().map(|cow| cow.to_string()),
latency: item.latency,
driver: item.driver.as_ref().map(|cow| cow.to_string()),
flags: Flags::SinkFlags(item.flags),
proplist: item.proplist.clone(),
configured_latency: item.configured_latency,
base_volume: item.base_volume,
state: DevState::from(item.state),
n_volume_steps: item.n_volume_steps,
card: item.card,
ports: item.ports.iter().map(From::from).collect(),
active_port: item.active_port.as_ref().map(From::from),
formats: item.formats.clone(),
}
}
}
impl<'a> From<&'a introspect::SourceInfo<'a>> for DeviceInfo {
fn from(item: &'a introspect::SourceInfo<'a>) -> Self {
DeviceInfo {
name: item.name.as_ref().map(|cow| cow.to_string()),
index: item.index,
description: item.description.as_ref().map(|cow| cow.to_string()),
sample_spec: item.sample_spec,
channel_map: item.channel_map,
owner_module: item.owner_module,
volume: item.volume,
mute: item.mute,
monitor: item.monitor_of_sink,
monitor_name: item
.monitor_of_sink_name
.as_ref()
.map(|cow| cow.to_string()),
latency: item.latency,
driver: item.driver.as_ref().map(|cow| cow.to_string()),
flags: Flags::SourceFLags(item.flags),
proplist: item.proplist.clone(),
configured_latency: item.configured_latency,
base_volume: item.base_volume,
state: DevState::from(item.state),
n_volume_steps: item.n_volume_steps,
card: item.card,
ports: item.ports.iter().map(From::from).collect(),
active_port: item.active_port.as_ref().map(From::from),
formats: item.formats.clone(),
}
}
}
#[derive(Clone)]
pub struct ApplicationInfo {
/// Index of the sink input.
pub index: u32,
/// Name of the sink input.
pub name: Option<String>,
/// Index of the module this sink input belongs to, or `None` when it does not belong to any
/// module.
pub owner_module: Option<u32>,
/// Index of the client this sink input belongs to, or invalid when it does not belong to any
/// client.
pub client: Option<u32>,
/// Index of the connected sink/source.
pub connection_id: u32,
/// The sample specification of the sink input.
pub sample_spec: sample::Spec,
/// Channel map.
pub channel_map: channelmap::Map,
/// The volume of this sink input.
pub volume: ChannelVolumes,
/// Latency due to buffering in sink input, see
/// [`def::TimingInfo`](../../def/struct.TimingInfo.html) for details.
pub buffer_usec: MicroSeconds,
/// Latency of the sink device, see
/// [`def::TimingInfo`](../../def/struct.TimingInfo.html) for details.
pub connection_usec: MicroSeconds,
/// The resampling method used by this sink input.
pub resample_method: Option<String>,
/// Driver name.
pub driver: Option<String>,
/// Stream muted.
pub mute: bool,
/// Property list.
pub proplist: Proplist,
/// Stream corked.
pub corked: bool,
/// Stream has volume. If not set, then the meaning of this structs volume member is unspecified.
pub has_volume: bool,
/// The volume can be set. If not set, the volume can still change even though clients cant
/// control the volume.
pub volume_writable: bool,
/// Stream format information.
pub format: format::Info,
}
impl<'a> From<&'a introspect::SinkInputInfo<'a>> for ApplicationInfo {
fn from(item: &'a introspect::SinkInputInfo<'a>) -> Self {
ApplicationInfo {
index: item.index,
name: item.name.as_ref().map(|cow| cow.to_string()),
owner_module: item.owner_module,
client: item.client,
connection_id: item.sink,
sample_spec: item.sample_spec,
channel_map: item.channel_map,
volume: item.volume,
buffer_usec: item.buffer_usec,
connection_usec: item.sink_usec,
resample_method: item.resample_method.as_ref().map(|cow| cow.to_string()),
driver: item.driver.as_ref().map(|cow| cow.to_string()),
mute: item.mute,
proplist: item.proplist.clone(),
corked: item.corked,
has_volume: item.has_volume,
volume_writable: item.volume_writable,
format: item.format.clone(),
}
}
}
impl<'a> From<&'a introspect::SourceOutputInfo<'a>> for ApplicationInfo {
fn from(item: &'a introspect::SourceOutputInfo<'a>) -> Self {
ApplicationInfo {
index: item.index,
name: item.name.as_ref().map(|cow| cow.to_string()),
owner_module: item.owner_module,
client: item.client,
connection_id: item.source,
sample_spec: item.sample_spec,
channel_map: item.channel_map,
volume: item.volume,
buffer_usec: item.buffer_usec,
connection_usec: item.source_usec,
resample_method: item.resample_method.as_ref().map(|cow| cow.to_string()),
driver: item.driver.as_ref().map(|cow| cow.to_string()),
mute: item.mute,
proplist: item.proplist.clone(),
corked: item.corked,
has_volume: item.has_volume,
volume_writable: item.volume_writable,
format: item.format.clone(),
}
}
}
pub struct ServerInfo {
/// User name of the daemon process.
pub user_name: Option<String>,
/// Host name the daemon is running on.
pub host_name: Option<String>,
/// Version string of the daemon.
pub server_version: Option<String>,
/// Server package name (usually “pulseaudio”).
pub server_name: Option<String>,
/// Default sample specification.
pub sample_spec: sample::Spec,
/// Name of default sink.
pub default_sink_name: Option<String>,
/// Name of default source.
pub default_source_name: Option<String>,
/// A random cookie for identifying this instance of PulseAudio.
pub cookie: u32,
/// Default channel map.
pub channel_map: channelmap::Map,
}
impl<'a> From<&'a introspect::ServerInfo<'a>> for ServerInfo {
fn from(info: &'a introspect::ServerInfo<'a>) -> Self {
ServerInfo {
user_name: info.user_name.as_ref().map(|cow| cow.to_string()),
host_name: info.host_name.as_ref().map(|cow| cow.to_string()),
server_version: info.server_version.as_ref().map(|cow| cow.to_string()),
server_name: info.server_name.as_ref().map(|cow| cow.to_string()),
sample_spec: info.sample_spec,
default_sink_name: info.default_sink_name.as_ref().map(|cow| cow.to_string()),
default_source_name: info.default_source_name.as_ref().map(|cow| cow.to_string()),
cookie: info.cookie,
channel_map: info.channel_map,
}
}
}

View File

@@ -0,0 +1,54 @@
use std::fmt;
use pulse::error::{PAErr};
impl From<PAErr> for PulseCtlError {
fn from(error: PAErr) -> Self {
PulseCtlError {
error: PulseCtlErrorType::PulseAudioError,
message: format!("PulseAudio returned error: {}", error.to_string().unwrap_or("Unknown".to_owned())),
}
}
}
impl fmt::Debug for PulseCtlError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut error_string = String::new();
match self.error {
PulseCtlErrorType::ConnectError => {
error_string.push_str("ConnectError");
}
PulseCtlErrorType::OperationError => {
error_string.push_str("OperationError");
}
PulseCtlErrorType::PulseAudioError => {
error_string.push_str("PulseAudioError");
}
}
write!(f, "[{}]: {}", error_string, self.message)
}
}
pub(crate) enum PulseCtlErrorType {
ConnectError,
OperationError,
PulseAudioError,
}
/// Error thrown when PulseAudio throws an error code, there are 3 variants
/// `PulseCtlErrorType::ConnectError` when there's an error establishing a connection
/// `PulseCtlErrorType::OperationError` when the requested operation quis unexpecdatly or is cancelled
/// `PulseCtlErrorType::PulseAudioError` when PulseAudio returns an error code in any circumstance
pub struct PulseCtlError {
error: PulseCtlErrorType,
message: String,
}
impl PulseCtlError {
pub(crate) fn new(err: PulseCtlErrorType, msg: &str) -> Self {
PulseCtlError {
error: err,
message: msg.to_string(),
}
}
}

166
libs/pulsectl/src/lib.rs Normal file
View File

@@ -0,0 +1,166 @@
/// `pulsectl` is a high level wrapper around the PulseAudio bindings supplied by
/// `libpulse_binding`. It provides simple access to sinks, inputs, sources and outputs allowing
/// one to write audio control programs with ease.
///
/// ## Quick Example
///
/// The following example demonstrates listing all of the playback devices currently connected
///
/// See examples/change_device_vol.rs for a more complete example
/// ```no_run
/// extern crate pulsectl;
///
/// use std::io;
///
/// use pulsectl::controllers::SinkController;
/// use pulsectl::controllers::DeviceControl;
/// fn main() {
/// // create handler that calls functions on playback devices and apps
/// let mut handler = SinkController::create().unwrap();
/// let devices = handler
/// .list_devices()
/// .expect("Could not get list of playback devices");
///
/// println!("Playback Devices");
/// for dev in devices.clone() {
/// println!(
/// "[{}] {}, Volume: {}",
/// dev.index,
/// dev.description.as_ref().unwrap(),
/// dev.volume.print()
/// );
/// }
/// }
/// ```
extern crate libpulse_binding as pulse;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use pulse::{
context::{introspect, Context},
mainloop::standard::{IterateResult, Mainloop},
operation::{Operation, State},
proplist::Proplist,
};
use crate::errors::{PulseCtlError, PulseCtlErrorType::*};
pub mod controllers;
mod errors;
pub struct Handler {
pub mainloop: Rc<RefCell<Mainloop>>,
pub context: Rc<RefCell<Context>>,
pub introspect: introspect::Introspector,
}
fn connect_error(err: &str) -> PulseCtlError {
PulseCtlError::new(ConnectError, err)
}
impl Handler {
pub fn connect(name: &str) -> Result<Handler, PulseCtlError> {
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(pulse::proplist::properties::APPLICATION_NAME, name)
.unwrap();
let mainloop;
if let Some(m) = Mainloop::new() {
mainloop = Rc::new(RefCell::new(m));
} else {
return Err(connect_error("Failed to create mainloop"));
}
let context;
if let Some(c) =
Context::new_with_proplist(mainloop.borrow().deref(), "MainConn", &proplist)
{
context = Rc::new(RefCell::new(c));
} else {
return Err(connect_error("Failed to create new context"));
}
context
.borrow_mut()
.connect(None, pulse::context::flags::NOFLAGS, None)
.map_err(|_| connect_error("Failed to connect context"))?;
loop {
match mainloop.borrow_mut().iterate(false) {
IterateResult::Err(e) => {
eprintln!("iterate state was not success, quitting...");
return Err(e.into());
}
IterateResult::Success(_) => {}
IterateResult::Quit(_) => {
eprintln!("iterate state was not success, quitting...");
return Err(PulseCtlError::new(
ConnectError,
"Iterate state quit without an error",
));
}
}
match context.borrow().get_state() {
pulse::context::State::Ready => break,
pulse::context::State::Failed | pulse::context::State::Terminated => {
eprintln!("context state failed/terminated, quitting...");
return Err(PulseCtlError::new(
ConnectError,
"Context state failed/terminated without an error",
));
}
_ => {}
}
}
let introspect = context.borrow_mut().introspect();
Ok(Handler {
mainloop,
context,
introspect,
})
}
// loop until the passed operation is completed
pub fn wait_for_operation<G: ?Sized>(
&mut self,
op: Operation<G>,
) -> Result<(), errors::PulseCtlError> {
loop {
match self.mainloop.borrow_mut().iterate(false) {
IterateResult::Err(e) => return Err(e.into()),
IterateResult::Success(_) => {}
IterateResult::Quit(_) => {
return Err(PulseCtlError::new(
OperationError,
"Iterate state quit without an error",
));
}
}
match op.get_state() {
State::Done => {
break;
}
State::Running => {}
State::Cancelled => {
return Err(PulseCtlError::new(
OperationError,
"Operation cancelled without an error",
));
}
}
}
Ok(())
}
}
impl Drop for Handler {
fn drop(&mut self) {
self.context.borrow_mut().disconnect();
self.mainloop.borrow_mut().quit(pulse::def::Retval(0));
}
}

4
libs/scrap/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target/
**/*.rs.bk
Cargo.lock
generated/

32
libs/scrap/Cargo.toml Normal file
View File

@@ -0,0 +1,32 @@
[package]
name = "scrap"
description = "Screen capture made easy."
version = "0.5.0"
repository = "https://github.com/quadrupleslap/scrap"
documentation = "https://docs.rs/scrap"
keywords = ["screen", "capture", "record"]
license = "MIT"
authors = ["Ram <quadrupleslap@gmail.com>"]
edition = "2018"
[dependencies]
block = "0.1"
cfg-if = "1.0"
libc = "0.2"
num_cpus = "1.13"
[dependencies.winapi]
version = "0.3"
default-features = true
features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11"]
[dev-dependencies]
repng = "0.2"
docopt = "1.1"
webm = "1.0"
serde = {version="1.0", features=["derive"]}
quest = "0.3"
[build-dependencies]
target_build_utils = "0.3"
bindgen = "0.53"

60
libs/scrap/README.md Normal file
View File

@@ -0,0 +1,60 @@
# scrap
Scrap records your screen! At least it does if you're on Windows, macOS, or Linux.
## Usage
```toml
[dependencies]
scrap = "0.5"
```
Its API is as simple as it gets!
```rust
struct Display; /// A screen.
struct Frame; /// An array of the pixels that were on-screen.
struct Capturer; /// A recording instance.
impl Capturer {
/// Begin recording.
pub fn new(display: Display) -> io::Result<Capturer>;
/// Try to get a frame.
/// Returns WouldBlock if it's not ready yet.
pub fn frame<'a>(&'a mut self) -> io::Result<Frame<'a>>;
pub fn width(&self) -> usize;
pub fn height(&self) -> usize;
}
impl Display {
/// The primary screen.
pub fn primary() -> io::Result<Display>;
/// All the screens.
pub fn all() -> io::Result<Vec<Display>>;
pub fn width(&self) -> usize;
pub fn height(&self) -> usize;
}
impl<'a> ops::Deref for Frame<'a> {
/// A frame is just an array of bytes.
type Target = [u8];
}
```
## The Frame Format
- The frame format is guaranteed to be **packed BGRA**.
- The width and height are guaranteed to remain constant.
- The stride might be greater than the width, and it may also vary between frames.
## System Requirements
OS | Minimum Requirements
--------|---------------------
macOS | macOS 10.8
Linux | XCB + SHM + RandR
Windows | DirectX 11.1

118
libs/scrap/build.rs Normal file
View File

@@ -0,0 +1,118 @@
use std::{
env, fs,
path::{Path, PathBuf},
};
fn find_package(name: &str) -> Vec<PathBuf> {
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: PathBuf = vcpkg_root.into();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "x86_64" {
target_arch = "x64".to_owned();
} else if target_arch == "aarch64" {
target_arch = "arm64".to_owned();
} else {
target_arch = "arm".to_owned();
}
let target = if target_os == "macos" {
"x64-osx".to_owned()
} else if target_os == "windows" {
"x64-windows-static".to_owned()
} else if target_os == "android" {
format!("{}-android-static", target_arch)
} else {
"x64-linux".to_owned()
};
println!("cargo:info={}", target);
path.push("installed");
path.push(target);
let mut lib = name.trim_start_matches("lib").to_string();
if lib == "vpx" && target_os == "windows" {
lib = format!("{}mt", lib);
}
println!("{}", format!("cargo:rustc-link-lib={}", lib));
println!(
"{}",
format!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
)
);
let include = path.join("include");
println!("{}", format!("cargo:include={}", include.to_str().unwrap()));
vec![include]
}
fn generate_bindings(
ffi_header: &Path,
include_paths: &[PathBuf],
ffi_rs: &Path,
exact_file: &Path,
) {
let mut b = bindgen::builder()
.header(ffi_header.to_str().unwrap())
.whitelist_type("^[vV].*")
.whitelist_var("^[vV].*")
.whitelist_function("^[vV].*")
.rustified_enum("^v.*")
.trust_clang_mangling(false)
.layout_tests(false) // breaks 32/64-bit compat
.generate_comments(false); // vpx comments have prefix /*!\
for dir in include_paths {
b = b.clang_arg(format!("-I{}", dir.display()));
}
b.generate().unwrap().write_to_file(ffi_rs).unwrap();
fs::copy(ffi_rs, exact_file).ok(); // ignore failure
}
fn gen_vpx() {
let includes = find_package("libvpx");
let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let src_dir = Path::new(&src_dir);
let out_dir = env::var_os("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir);
let ffi_header = src_dir.join("vpx_ffi.h");
println!("rerun-if-changed={}", ffi_header.display());
for dir in &includes {
println!("rerun-if-changed={}", dir.display());
}
let ffi_rs = out_dir.join("vpx_ffi.rs");
let exact_file = src_dir.join("generated").join("vpx_ffi.rs");
generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file);
}
fn main() {
// note: all link symbol names in x86 (32-bit) are prefixed wth "_".
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
// please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc",
// then set x64 to default by "rustup default stable-x86_64-pc-windows-msvc"
let target = target_build_utils::TargetInfo::new();
if target.unwrap().target_pointer_width() != "64" {
panic!("Only support 64bit system");
}
env::remove_var("CARGO_CFG_TARGET_FEATURE");
env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
find_package("libyuv");
gen_vpx();
// there is problem with cfg(target_os) in build.rs, so use our workaround
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os == "android" || target_os == "ios" {
// nothing
} else if cfg!(windows) {
// The first choice is Windows because DXGI is amazing.
println!("cargo:rustc-cfg=dxgi");
} else if cfg!(target_os = "macos") {
// Quartz is second because macOS is the (annoying) exception.
println!("cargo:rustc-cfg=quartz");
} else if cfg!(unix) {
// On UNIX we pray that X11 (with XCB) is available.
println!("cargo:rustc-cfg=x11");
}
}

View File

@@ -0,0 +1,51 @@
extern crate scrap;
fn main() {
use scrap::{Capturer, Display};
use std::io::ErrorKind::WouldBlock;
use std::io::Write;
use std::process::{Command, Stdio};
let d = Display::primary().unwrap();
let (w, h) = (d.width(), d.height());
let child = Command::new("ffplay")
.args(&[
"-f",
"rawvideo",
"-pixel_format",
"bgr0",
"-video_size",
&format!("{}x{}", w, h),
"-framerate",
"60",
"-",
])
.stdin(Stdio::piped())
.spawn()
.expect("This example requires ffplay.");
let mut capturer = Capturer::new(d, false).unwrap();
let mut out = child.stdin.unwrap();
loop {
match capturer.frame(0) {
Ok(frame) => {
// Write the frame, removing end-of-row padding.
let stride = frame.len() / h;
let rowlen = 4 * w;
for row in frame.chunks(stride) {
let row = &row[..rowlen];
out.write_all(row).unwrap();
}
}
Err(ref e) if e.kind() == WouldBlock => {
// Wait for the frame.
}
Err(_) => {
// We're done here.
break;
}
}
}
}

View File

@@ -0,0 +1,16 @@
extern crate scrap;
use scrap::Display;
fn main() {
let displays = Display::all().unwrap();
for (i, display) in displays.iter().enumerate() {
println!(
"Display {} [{}x{}]",
i + 1,
display.width(),
display.height()
);
}
}

View File

@@ -0,0 +1,161 @@
extern crate docopt;
extern crate quest;
extern crate repng;
extern crate scrap;
extern crate serde;
extern crate webm;
use std::fs::{File, OpenOptions};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{io, thread};
use docopt::Docopt;
use webm::mux;
use webm::mux::Track;
use scrap::codec as vpx_encode;
use scrap::{Capturer, Display, STRIDE_ALIGN};
const USAGE: &'static str = "
Simple WebM screen capture.
Usage:
record-screen <path> [--time=<s>] [--fps=<fps>] [--bv=<kbps>] [--ba=<kbps>] [--codec CODEC]
record-screen (-h | --help)
Options:
-h --help Show this screen.
--time=<s> Recording duration in seconds.
--fps=<fps> Frames per second [default: 30].
--bv=<kbps> Video bitrate in kilobits per second [default: 5000].
--ba=<kbps> Audio bitrate in kilobits per second [default: 96].
--codec CODEC Configure the codec used. [default: vp9]
Valid values: vp8, vp9.
";
#[derive(Debug, serde::Deserialize)]
struct Args {
arg_path: PathBuf,
flag_codec: Codec,
flag_time: Option<u64>,
flag_fps: u64,
flag_bv: u32,
flag_ba: u32,
}
#[derive(Debug, serde::Deserialize)]
enum Codec {
Vp8,
Vp9,
}
fn main() -> io::Result<()> {
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit());
let duration = args.flag_time.map(Duration::from_secs);
let d = Display::primary().unwrap();
let (width, height) = (d.width() as u32, d.height() as u32);
// Setup the multiplexer.
let out = match {
OpenOptions::new()
.write(true)
.create_new(true)
.open(&args.arg_path)
} {
Ok(file) => file,
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => {
if loop {
quest::ask("Overwrite the existing file? [y/N] ");
if let Some(b) = quest::yesno(false)? {
break b;
}
} {
File::create(&args.arg_path)?
} else {
return Ok(());
}
}
Err(e) => return Err(e.into()),
};
let mut webm =
mux::Segment::new(mux::Writer::new(out)).expect("Could not initialize the multiplexer.");
let (vpx_codec, mux_codec) = match args.flag_codec {
Codec::Vp8 => (vpx_encode::VideoCodecId::VP8, mux::VideoCodecId::VP8),
Codec::Vp9 => (vpx_encode::VideoCodecId::VP9, mux::VideoCodecId::VP9),
};
let mut vt = webm.add_video_track(width, height, None, mux_codec);
// Setup the encoder.
let mut vpx = vpx_encode::Encoder::new(
&vpx_encode::Config {
width,
height,
timebase: [1, 1000],
bitrate: args.flag_bv,
codec: vpx_codec,
rc_min_quantizer: 0,
rc_max_quantizer: 0,
speed: 6,
},
0,
)
.unwrap();
// Start recording.
let start = Instant::now();
let stop = Arc::new(AtomicBool::new(false));
thread::spawn({
let stop = stop.clone();
move || {
let _ = quest::ask("Recording! Press ⏎ to stop.");
let _ = quest::text();
stop.store(true, Ordering::Release);
}
});
let spf = Duration::from_nanos(1_000_000_000 / args.flag_fps);
// Capturer object is expensive, avoiding to create it frequently.
let mut c = Capturer::new(d, true).unwrap();
while !stop.load(Ordering::Acquire) {
let now = Instant::now();
let time = now - start;
if Some(true) == duration.map(|d| time > d) {
break;
}
if let Ok(frame) = c.frame(0) {
let ms = time.as_secs() * 1000 + time.subsec_millis() as u64;
for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() {
vt.add_frame(frame.data, frame.pts as u64 * 1_000_000, frame.key);
}
}
let dt = now.elapsed();
if dt < spf {
thread::sleep(spf - dt);
}
}
// End things.
let _ = webm.finalize(None);
Ok(())
}

View File

@@ -0,0 +1,122 @@
extern crate repng;
extern crate scrap;
use std::fs::File;
use std::io::ErrorKind::WouldBlock;
use std::thread;
use std::time::Duration;
use scrap::{i420_to_rgb, Capturer, Display};
fn main() {
let n = Display::all().unwrap().len();
for i in 0..n {
record(i);
}
}
fn get_display(i: usize) -> Display {
Display::all().unwrap().remove(i)
}
fn record(i: usize) {
let one_second = Duration::new(1, 0);
let one_frame = one_second / 60;
let display = get_display(i);
let mut capturer = Capturer::new(display, false).expect("Couldn't begin capture.");
let (w, h) = (capturer.width(), capturer.height());
loop {
// Wait until there's a frame.
let buffer = match capturer.frame(0) {
Ok(buffer) => buffer,
Err(error) => {
if error.kind() == WouldBlock {
// Keep spinning.
thread::sleep(one_frame);
continue;
} else {
panic!("Error: {}", error);
}
}
};
println!("Captured! Saving...");
// Flip the BGRA image into a RGBA image.
let mut bitflipped = Vec::with_capacity(w * h * 4);
let stride = buffer.len() / h;
for y in 0..h {
for x in 0..w {
let i = stride * y + 4 * x;
bitflipped.extend_from_slice(&[buffer[i + 2], buffer[i + 1], buffer[i], 255]);
}
}
// Save the image.
let name = format!("screenshot{}_1.png", i);
repng::encode(
File::create(name.clone()).unwrap(),
w as u32,
h as u32,
&bitflipped,
)
.unwrap();
println!("Image saved to `{}`.", name);
break;
}
drop(capturer);
let display = get_display(i);
let mut capturer = Capturer::new(display, true).expect("Couldn't begin capture.");
let (w, h) = (capturer.width(), capturer.height());
loop {
// Wait until there's a frame.
let buffer = match capturer.frame(0) {
Ok(buffer) => buffer,
Err(error) => {
if error.kind() == WouldBlock {
// Keep spinning.
thread::sleep(one_frame);
continue;
} else {
panic!("Error: {}", error);
}
}
};
println!("Captured! Saving...");
let mut frame = Default::default();
i420_to_rgb(w, h, &buffer, &mut frame);
let mut bitflipped = Vec::with_capacity(w * h * 4);
let stride = frame.len() / h;
for y in 0..h {
for x in 0..w {
let i = stride * y + 3 * x;
bitflipped.extend_from_slice(&[frame[i], frame[i + 1], frame[i + 2], 255]);
}
}
let name = format!("screenshot{}_2.png", i);
repng::encode(
File::create(name.clone()).unwrap(),
w as u32,
h as u32,
&bitflipped,
)
.unwrap();
println!("Image saved to `{}`.", name);
break;
}
}

View File

@@ -0,0 +1,536 @@
// https://github.com/astraw/vpx-encode
// https://github.com/astraw/env-libvpx-sys
// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs
use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
use std::os::raw::{c_int, c_uint};
use std::{ptr, slice};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum VideoCodecId {
VP8,
VP9,
}
impl Default for VideoCodecId {
fn default() -> VideoCodecId {
VideoCodecId::VP9
}
}
pub struct Encoder {
ctx: vpx_codec_ctx_t,
width: usize,
height: usize,
}
pub struct Decoder {
ctx: vpx_codec_ctx_t,
}
#[derive(Debug)]
pub enum Error {
FailedCall(String),
BadPtr(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{:?}", self)
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
macro_rules! call_vpx {
($x:expr) => {{
let result = unsafe { $x }; // original expression
let result_int = unsafe { std::mem::transmute::<_, i32>(result) };
if result_int != 0 {
return Err(Error::FailedCall(format!(
"errcode={} {}:{}:{}:{}",
result_int,
module_path!(),
file!(),
line!(),
column!()
))
.into());
}
result
}};
}
macro_rules! call_vpx_ptr {
($x:expr) => {{
let result = unsafe { $x }; // original expression
let result_int = unsafe { std::mem::transmute::<_, i64>(result) };
if result_int == 0 {
return Err(Error::BadPtr(format!(
"errcode={} {}:{}:{}:{}",
result_int,
module_path!(),
file!(),
line!(),
column!()
))
.into());
}
result
}};
}
impl Encoder {
pub fn new(config: &Config, num_threads: u32) -> Result<Self> {
let i;
if cfg!(feature = "VP8") {
i = match config.codec {
VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
};
} else {
i = call_vpx_ptr!(vpx_codec_vp9_cx());
}
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
// https://www.webmproject.org/docs/encoder-parameters/
// default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63
// try rc_resize_allowed later
c.g_w = config.width;
c.g_h = config.height;
c.g_timebase.num = config.timebase[0];
c.g_timebase.den = config.timebase[1];
c.rc_target_bitrate = config.bitrate;
c.rc_undershoot_pct = 95;
c.rc_dropframe_thresh = 25;
if config.rc_min_quantizer > 0 {
c.rc_min_quantizer = config.rc_min_quantizer;
}
if config.rc_max_quantizer > 0 {
c.rc_max_quantizer = config.rc_max_quantizer;
}
let mut speed = config.speed;
if speed <= 0 {
speed = 6;
}
c.g_threads = if num_threads == 0 {
num_cpus::get() as _
} else {
num_threads
};
c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
// https://developers.google.com/media/vp9/bitrate-modes/
// Constant Bitrate mode (CBR) is recommended for live streaming with VP9.
c.rc_end_usage = vpx_rc_mode::VPX_CBR;
// c.kf_min_dist = 0;
// c.kf_max_dist = 999999;
c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
/*
VPX encoder支持two-pass encode这是为了rate control的。
对于两遍编码,就是需要整个编码过程做两次,第一次会得到一些新的控制参数来进行第二遍的编码,
这样可以在相同的bitrate下得到最好的PSNR
*/
let mut ctx = Default::default();
call_vpx!(vpx_codec_enc_init_ver(
&mut ctx,
i,
&c,
0,
VPX_ENCODER_ABI_VERSION as _
));
if config.codec == VideoCodecId::VP9 {
// set encoder internal speed settings
// in ffmpeg, it is --speed option
/*
set to 0 or a positive value 1-16, the codec will try to adapt its
complexity depending on the time it spends encoding. Increasing this
number will make the speed go up and the quality go down.
Negative values mean strict enforcement of this
while positive values are adaptive
*/
/* https://developers.google.com/media/vp9/live-encoding
Speed 5 to 8 should be used for live / real-time encoding.
Lower numbers (5 or 6) are higher quality but require more CPU power.
Higher numbers (7 or 8) will be lower quality but more manageable for lower latency
use cases and also for lower CPU power devices such as mobile.
*/
call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, speed,));
// set row level multi-threading
/*
as some people in comments and below have already commented,
more recent versions of libvpx support -row-mt 1 to enable tile row
multi-threading. This can increase the number of tiles by up to 4x in VP9
(since the max number of tile rows is 4, regardless of video height).
To enable this, use -tile-rows N where N is the number of tile rows in
log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile
rows). The total number of active threads will then be equal to
$tile_rows * $tile_columns
*/
call_vpx!(vpx_codec_control_(
&mut ctx,
VP9E_SET_ROW_MT as _,
1 as c_int
));
call_vpx!(vpx_codec_control_(
&mut ctx,
VP9E_SET_TILE_COLUMNS as _,
4 as c_int
));
}
Ok(Self {
ctx,
width: config.width as _,
height: config.height as _,
})
}
pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result<EncodeFrames> {
assert!(2 * data.len() >= 3 * self.width * self.height);
let mut image = Default::default();
call_vpx_ptr!(vpx_img_wrap(
&mut image,
vpx_img_fmt::VPX_IMG_FMT_I420,
self.width as _,
self.height as _,
stride_align as _,
data.as_ptr() as _,
));
call_vpx!(vpx_codec_encode(
&mut self.ctx,
&image,
pts as _,
1, // Duration
0, // Flags
VPX_DL_REALTIME as _,
));
Ok(EncodeFrames {
ctx: &mut self.ctx,
iter: ptr::null(),
})
}
/// Notify the encoder to return any pending packets
pub fn flush(&mut self) -> Result<EncodeFrames> {
call_vpx!(vpx_codec_encode(
&mut self.ctx,
ptr::null(),
-1, // PTS
1, // Duration
0, // Flags
VPX_DL_REALTIME as _,
));
Ok(EncodeFrames {
ctx: &mut self.ctx,
iter: ptr::null(),
})
}
}
impl Drop for Encoder {
fn drop(&mut self) {
unsafe {
let result = vpx_codec_destroy(&mut self.ctx);
if result != VPX_CODEC_OK {
panic!("failed to destroy vpx codec");
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct EncodeFrame<'a> {
/// Compressed data.
pub data: &'a [u8],
/// Whether the frame is a keyframe.
pub key: bool,
/// Presentation timestamp (in timebase units).
pub pts: i64,
}
#[derive(Clone, Copy, Debug)]
pub struct Config {
/// The width (in pixels).
pub width: c_uint,
/// The height (in pixels).
pub height: c_uint,
/// The timebase numerator and denominator (in seconds).
pub timebase: [c_int; 2],
/// The target bitrate (in kilobits per second).
pub bitrate: c_uint,
/// The codec
pub codec: VideoCodecId,
pub rc_min_quantizer: u32,
pub rc_max_quantizer: u32,
pub speed: i32,
}
pub struct EncodeFrames<'a> {
ctx: &'a mut vpx_codec_ctx_t,
iter: vpx_codec_iter_t,
}
impl<'a> Iterator for EncodeFrames<'a> {
type Item = EncodeFrame<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
unsafe {
let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter);
if pkt.is_null() {
return None;
} else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
let f = &(*pkt).data.frame;
return Some(Self::Item {
data: slice::from_raw_parts(f.buf as _, f.sz as _),
key: (f.flags & VPX_FRAME_IS_KEY) != 0,
pts: f.pts,
});
} else {
// Ignore the packet.
}
}
}
}
}
impl Decoder {
/// Create a new decoder
///
/// # Errors
///
/// The function may fail if the underlying libvpx does not provide
/// the VP9 decoder.
pub fn new(codec: VideoCodecId, num_threads: u32) -> Result<Self> {
// This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can
// cause UB if uninitialized.
let i;
if cfg!(feature = "VP8") {
i = match codec {
VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
};
} else {
i = call_vpx_ptr!(vpx_codec_vp9_dx());
}
let mut ctx = Default::default();
let cfg = vpx_codec_dec_cfg_t {
threads: if num_threads == 0 {
num_cpus::get() as _
} else {
num_threads
},
w: 0,
h: 0,
};
/*
unsafe {
println!("{}", vpx_codec_get_caps(i));
}
*/
call_vpx!(vpx_codec_dec_init_ver(
&mut ctx,
i,
&cfg,
0,
VPX_DECODER_ABI_VERSION as _,
));
Ok(Self { ctx })
}
pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result<Vec<u8>> {
let mut img = Image::new();
for frame in self.decode(data)? {
drop(img);
img = frame;
}
for frame in self.flush()? {
drop(img);
img = frame;
}
if img.is_null() {
Ok(Vec::new())
} else {
let mut out = Default::default();
img.rgb(1, rgba, &mut out);
Ok(out)
}
}
/// Feed some compressed data to the encoder
///
/// The `data` slice is sent to the decoder
///
/// It matches a call to `vpx_codec_decode`.
pub fn decode(&mut self, data: &[u8]) -> Result<DecodeFrames> {
call_vpx!(vpx_codec_decode(
&mut self.ctx,
data.as_ptr(),
data.len() as _,
ptr::null_mut(),
0,
));
Ok(DecodeFrames {
ctx: &mut self.ctx,
iter: ptr::null(),
})
}
/// Notify the decoder to return any pending frame
pub fn flush(&mut self) -> Result<DecodeFrames> {
call_vpx!(vpx_codec_decode(
&mut self.ctx,
ptr::null(),
0,
ptr::null_mut(),
0
));
Ok(DecodeFrames {
ctx: &mut self.ctx,
iter: ptr::null(),
})
}
}
impl Drop for Decoder {
fn drop(&mut self) {
unsafe {
let result = vpx_codec_destroy(&mut self.ctx);
if result != VPX_CODEC_OK {
panic!("failed to destroy vpx codec");
}
}
}
}
pub struct DecodeFrames<'a> {
ctx: &'a mut vpx_codec_ctx_t,
iter: vpx_codec_iter_t,
}
impl<'a> Iterator for DecodeFrames<'a> {
type Item = Image;
fn next(&mut self) -> Option<Self::Item> {
let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) };
if img.is_null() {
return None;
} else {
return Some(Image(img));
}
}
}
// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c
pub struct Image(*mut vpx_image_t);
impl Image {
#[inline]
pub fn new() -> Self {
Self(std::ptr::null_mut())
}
#[inline]
pub fn is_null(&self) -> bool {
self.0.is_null()
}
#[inline]
pub fn width(&self) -> usize {
self.inner().d_w as _
}
#[inline]
pub fn height(&self) -> usize {
self.inner().d_h as _
}
#[inline]
pub fn format(&self) -> vpx_img_fmt_t {
// VPX_IMG_FMT_I420
self.inner().fmt
}
#[inline]
pub fn inner(&self) -> &vpx_image_t {
unsafe { &*self.0 }
}
#[inline]
pub fn stride(&self, iplane: usize) -> i32 {
self.inner().stride[iplane]
}
pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec<u8>) {
let h = self.height();
let mut w = self.width();
let bps = if rgba { 4 } else { 3 };
w = (w + stride_align - 1) & !(stride_align - 1);
dst.resize(h * w * bps, 0);
let img = self.inner();
unsafe {
if rgba {
super::I420ToARGB(
img.planes[0],
img.stride[0],
img.planes[1],
img.stride[1],
img.planes[2],
img.stride[2],
dst.as_mut_ptr(),
(w * bps) as _,
self.width() as _,
self.height() as _,
);
} else {
super::I420ToRAW(
img.planes[0],
img.stride[0],
img.planes[1],
img.stride[1],
img.planes[2],
img.stride[2],
dst.as_mut_ptr(),
(w * bps) as _,
self.width() as _,
self.height() as _,
);
}
}
}
#[inline]
pub fn data(&self) -> (&[u8], &[u8], &[u8]) {
unsafe {
let img = self.inner();
let h = (img.d_h as usize + 1) & !1;
let n = img.stride[0] as usize * h;
let y = slice::from_raw_parts(img.planes[0], n);
let n = img.stride[1] as usize * (h >> 1);
let u = slice::from_raw_parts(img.planes[1], n);
let v = slice::from_raw_parts(img.planes[2], n);
(y, u, v)
}
}
}
impl Drop for Image {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { vpx_img_free(self.0) };
}
}
}
unsafe impl Send for vpx_codec_ctx_t {}

View File

@@ -0,0 +1,188 @@
use super::vpx::*;
use std::os::raw::c_int;
extern "C" {
// seems libyuv uses reverse byte order compared with our view
pub fn ARGBRotate(
src_argb: *const u8,
src_stride_argb: c_int,
dst_argb: *mut u8,
dst_stride_argb: c_int,
width: c_int,
height: c_int,
mode: c_int,
) -> c_int;
pub fn ARGBMirror(
src_argb: *const u8,
src_stride_argb: c_int,
dst_argb: *mut u8,
dst_stride_argb: c_int,
width: c_int,
height: c_int,
) -> c_int;
pub fn ARGBToI420(
src_bgra: *const u8,
src_stride_bgra: c_int,
dst_y: *mut u8,
dst_stride_y: c_int,
dst_u: *mut u8,
dst_stride_u: c_int,
dst_v: *mut u8,
dst_stride_v: c_int,
width: c_int,
height: c_int,
) -> c_int;
pub fn NV12ToI420(
src_y: *const u8,
src_stride_y: c_int,
src_uv: *const u8,
src_stride_uv: c_int,
dst_y: *mut u8,
dst_stride_y: c_int,
dst_u: *mut u8,
dst_stride_u: c_int,
dst_v: *mut u8,
dst_stride_v: c_int,
width: c_int,
height: c_int,
) -> c_int;
// I420ToRGB24: RGB little endian (bgr in memory)
// I420ToRaw: RGB big endian (rgb in memory) to RGBA.
pub fn I420ToRAW(
src_y: *const u8,
src_stride_y: c_int,
src_u: *const u8,
src_stride_u: c_int,
src_v: *const u8,
src_stride_v: c_int,
dst_rgba: *mut u8,
dst_stride_raw: c_int,
width: c_int,
height: c_int,
) -> c_int;
pub fn I420ToARGB(
src_y: *const u8,
src_stride_y: c_int,
src_u: *const u8,
src_stride_u: c_int,
src_v: *const u8,
src_stride_v: c_int,
dst_rgba: *mut u8,
dst_stride_rgba: c_int,
width: c_int,
height: c_int,
) -> c_int;
}
// https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c
#[inline]
fn get_vpx_i420_stride(
width: usize,
height: usize,
stride_align: usize,
) -> (usize, usize, usize, usize, usize, usize) {
let mut img = Default::default();
unsafe {
vpx_img_wrap(
&mut img,
vpx_img_fmt::VPX_IMG_FMT_I420,
width as _,
height as _,
stride_align as _,
0x1 as _,
);
}
(
img.w as _,
img.h as _,
img.stride[0] as _,
img.stride[1] as _,
img.planes[1] as usize - img.planes[0] as usize,
img.planes[2] as usize - img.planes[0] as usize,
)
}
pub fn i420_to_rgb(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
let (_, _, src_stride_y, src_stride_uv, u, v) =
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
let src_y = src.as_ptr();
let src_u = src[u..].as_ptr();
let src_v = src[v..].as_ptr();
dst.resize(width * height * 3, 0);
unsafe {
super::I420ToRAW(
src_y,
src_stride_y as _,
src_u,
src_stride_uv as _,
src_v,
src_stride_uv as _,
dst.as_mut_ptr(),
(width * 3) as _,
width as _,
height as _,
);
};
}
pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec<u8>) {
let (_, h, dst_stride_y, dst_stride_uv, u, v) =
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
let bps = 12;
dst.resize(h * dst_stride_y * bps / 8, 0);
let dst_y = dst.as_mut_ptr();
let dst_u = dst[u..].as_mut_ptr();
let dst_v = dst[v..].as_mut_ptr();
unsafe {
ARGBToI420(
src.as_ptr(),
(src.len() / height) as _,
dst_y,
dst_stride_y as _,
dst_u,
dst_stride_uv as _,
dst_v,
dst_stride_uv as _,
width as _,
height as _,
);
}
}
pub unsafe fn nv12_to_i420(
src_y: *const u8,
src_stride_y: c_int,
src_uv: *const u8,
src_stride_uv: c_int,
width: usize,
height: usize,
dst: &mut Vec<u8>,
) {
let (w, h, dst_stride_y, dst_stride_uv, u, v) =
get_vpx_i420_stride(width, height, super::STRIDE_ALIGN);
let bps = 12;
dst.resize(h * w * bps / 8, 0);
let dst_y = dst.as_mut_ptr();
let dst_u = dst[u..].as_mut_ptr();
let dst_v = dst[v..].as_mut_ptr();
NV12ToI420(
src_y,
src_stride_y,
src_uv,
src_stride_uv,
dst_y,
dst_stride_y as _,
dst_u,
dst_stride_uv as _,
dst_v,
dst_stride_uv as _,
width as _,
height as _,
);
}

View File

@@ -0,0 +1,104 @@
use crate::dxgi;
use std::io::ErrorKind::{NotFound, TimedOut, WouldBlock};
use std::{io, ops};
pub struct Capturer {
inner: dxgi::Capturer,
width: usize,
height: usize,
}
impl Capturer {
pub fn new(display: Display, yuv: bool) -> io::Result<Capturer> {
let width = display.width();
let height = display.height();
let inner = dxgi::Capturer::new(display.0, yuv)?;
Ok(Capturer {
inner,
width,
height,
})
}
pub fn is_gdi(&self) -> bool {
self.inner.is_gdi()
}
pub fn set_gdi(&mut self) -> bool {
self.inner.set_gdi()
}
pub fn cancel_gdi(&mut self) {
self.inner.cancel_gdi()
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result<Frame<'a>> {
match self.inner.frame(timeout_ms) {
Ok(frame) => Ok(Frame(frame)),
Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()),
Err(error) => Err(error),
}
}
}
pub struct Frame<'a>(&'a [u8]);
impl<'a> ops::Deref for Frame<'a> {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.0
}
}
pub struct Display(dxgi::Display);
impl Display {
pub fn primary() -> io::Result<Display> {
match dxgi::Displays::new()?.next() {
Some(inner) => Ok(Display(inner)),
None => Err(NotFound.into()),
}
}
pub fn all() -> io::Result<Vec<Display>> {
Ok(dxgi::Displays::new()?.map(Display).collect::<Vec<_>>())
}
pub fn width(&self) -> usize {
self.0.width() as usize
}
pub fn height(&self) -> usize {
self.0.height() as usize
}
pub fn name(&self) -> String {
use std::ffi::OsString;
use std::os::windows::prelude::*;
OsString::from_wide(self.0.name())
.to_string_lossy()
.to_string()
}
pub fn is_online(&self) -> bool {
self.0.is_online()
}
pub fn origin(&self) -> (usize, usize) {
let o = self.0.origin();
(o.0 as usize, o.1 as usize)
}
// to-do: not found primary display api for dxgi
pub fn is_primary(&self) -> bool {
self.name() == Self::primary().unwrap().name()
}
}

View File

@@ -0,0 +1,23 @@
pub use self::codec::*;
cfg_if! {
if #[cfg(quartz)] {
mod quartz;
pub use self::quartz::*;
} else if #[cfg(x11)] {
mod x11;
pub use self::x11::*;
} else if #[cfg(dxgi)] {
mod dxgi;
pub use self::dxgi::*;
} else {
//TODO: Fallback implementation.
}
}
pub mod codec;
mod convert;
pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 16; // commonly used in libvpx vpx_img_alloc caller
mod vpx;

View File

@@ -0,0 +1,125 @@
use crate::quartz;
use std::marker::PhantomData;
use std::sync::{Arc, Mutex, TryLockError};
use std::{io, mem, ops};
pub struct Capturer {
inner: quartz::Capturer,
frame: Arc<Mutex<Option<quartz::Frame>>>,
use_yuv: bool,
i420: Vec<u8>,
}
impl Capturer {
pub fn new(display: Display, use_yuv: bool) -> io::Result<Capturer> {
let frame = Arc::new(Mutex::new(None));
let f = frame.clone();
let inner = quartz::Capturer::new(
display.0,
display.width(),
display.height(),
if use_yuv {
quartz::PixelFormat::YCbCr420Full
} else {
quartz::PixelFormat::Argb8888
},
Default::default(),
move |inner| {
if let Ok(mut f) = f.lock() {
*f = Some(inner);
}
},
)
.map_err(|_| io::Error::from(io::ErrorKind::Other))?;
Ok(Capturer {
inner,
frame,
use_yuv,
i420: Vec::new(),
})
}
pub fn width(&self) -> usize {
self.inner.width()
}
pub fn height(&self) -> usize {
self.inner.height()
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> {
match self.frame.try_lock() {
Ok(mut handle) => {
let mut frame = None;
mem::swap(&mut frame, &mut handle);
match frame {
Some(mut frame) => {
if self.use_yuv {
frame.nv12_to_i420(self.width(), self.height(), &mut self.i420);
}
Ok(Frame(frame, PhantomData))
}
None => Err(io::ErrorKind::WouldBlock.into()),
}
}
Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()),
Err(TryLockError::Poisoned(..)) => Err(io::ErrorKind::Other.into()),
}
}
}
pub struct Frame<'a>(quartz::Frame, PhantomData<&'a [u8]>);
impl<'a> ops::Deref for Frame<'a> {
type Target = [u8];
fn deref(&self) -> &[u8] {
&*self.0
}
}
pub struct Display(quartz::Display);
impl Display {
pub fn primary() -> io::Result<Display> {
Ok(Display(quartz::Display::primary()))
}
pub fn all() -> io::Result<Vec<Display>> {
Ok(quartz::Display::online()
.map_err(|_| io::Error::from(io::ErrorKind::Other))?
.into_iter()
.map(Display)
.collect())
}
pub fn width(&self) -> usize {
self.0.width()
}
pub fn height(&self) -> usize {
self.0.height()
}
pub fn name(&self) -> String {
self.0.id().to_string()
}
pub fn is_online(&self) -> bool {
self.0.is_online()
}
pub fn origin(&self) -> (usize, usize) {
let o = self.0.bounds().origin;
(o.x as usize, o.y as usize)
}
pub fn is_primary(&self) -> bool {
self.0.is_primary()
}
}

View File

@@ -0,0 +1,25 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(improper_ctypes)]
#![allow(dead_code)]
impl Default for vpx_codec_enc_cfg {
fn default() -> Self {
unsafe { std::mem::zeroed() }
}
}
impl Default for vpx_codec_ctx {
fn default() -> Self {
unsafe { std::mem::zeroed() }
}
}
impl Default for vpx_image_t {
fn default() -> Self {
unsafe { std::mem::zeroed() }
}
}
include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs"));

View File

@@ -0,0 +1,87 @@
use crate::x11;
use std::{io, ops};
pub struct Capturer(x11::Capturer);
impl Capturer {
pub fn new(display: Display, yuv: bool) -> io::Result<Capturer> {
x11::Capturer::new(display.0, yuv).map(Capturer)
}
pub fn width(&self) -> usize {
self.0.display().rect().w as usize
}
pub fn height(&self) -> usize {
self.0.display().rect().h as usize
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> {
Ok(Frame(self.0.frame()))
}
}
pub struct Frame<'a>(&'a [u8]);
impl<'a> ops::Deref for Frame<'a> {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.0
}
}
pub struct Display(x11::Display);
impl Display {
pub fn primary() -> io::Result<Display> {
let server = match x11::Server::default() {
Ok(server) => server,
Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()),
};
let mut displays = x11::Server::displays(server);
let mut best = displays.next();
if best.as_ref().map(|x| x.is_default()) == Some(false) {
best = displays.find(|x| x.is_default()).or(best);
}
match best {
Some(best) => Ok(Display(best)),
None => Err(io::ErrorKind::NotFound.into()),
}
}
pub fn all() -> io::Result<Vec<Display>> {
let server = match x11::Server::default() {
Ok(server) => server,
Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()),
};
Ok(x11::Server::displays(server).map(Display).collect())
}
pub fn width(&self) -> usize {
self.0.rect().w as usize
}
pub fn height(&self) -> usize {
self.0.rect().h as usize
}
pub fn origin(&self) -> (usize, usize) {
let r = self.0.rect();
(r.x as _, r.y as _)
}
pub fn is_online(&self) -> bool {
true
}
pub fn is_primary(&self) -> bool {
self.0.is_default()
}
pub fn name(&self) -> String {
"".to_owned()
}
}

212
libs/scrap/src/dxgi/gdi.rs Normal file
View File

@@ -0,0 +1,212 @@
use std::mem::size_of;
use winapi::{
shared::windef::{HBITMAP, HDC},
um::wingdi::{
BitBlt,
CreateCompatibleBitmap,
CreateCompatibleDC,
CreateDCW,
DeleteDC,
DeleteObject,
GetDIBits,
SelectObject,
BITMAPINFO,
BITMAPINFOHEADER,
BI_RGB,
DIB_RGB_COLORS, //CAPTUREBLT,
HGDI_ERROR,
RGBQUAD,
SRCCOPY,
},
};
const PIXEL_WIDTH: i32 = 4;
pub struct CapturerGDI {
screen_dc: HDC,
dc: HDC,
bmp: HBITMAP,
width: i32,
height: i32,
}
impl CapturerGDI {
pub fn new(name: &[u16], width: i32, height: i32) -> Result<Self, Box<dyn std::error::Error>> {
/* or Enumerate monitors with EnumDisplayMonitors,
https://stackoverflow.com/questions/34987695/how-can-i-get-an-hmonitor-handle-from-a-display-device-name
#[no_mangle]
pub extern "C" fn callback(m: HMONITOR, dc: HDC, rect: LPRECT, lp: LPARAM) -> BOOL {}
*/
/*
shared::windef::HMONITOR,
winuser::{GetMonitorInfoW, GetSystemMetrics, MONITORINFOEXW},
let mut mi: MONITORINFOEXW = std::mem::MaybeUninit::uninit().assume_init();
mi.cbSize = size_of::<MONITORINFOEXW>() as _;
if GetMonitorInfoW(m, &mut mi as *mut MONITORINFOEXW as _) == 0 {
return Err(format!("Failed to get monitor information of: {:?}", m).into());
}
*/
unsafe {
if name.is_empty() {
return Err("Empty display name".into());
}
let screen_dc = CreateDCW(&name[0], 0 as _, 0 as _, 0 as _);
if screen_dc.is_null() {
return Err("Failed to create dc from monitor name".into());
}
// Create a Windows Bitmap, and copy the bits into it
let dc = CreateCompatibleDC(screen_dc);
if dc.is_null() {
DeleteDC(screen_dc);
return Err("Can't get a Windows display".into());
}
let bmp = CreateCompatibleBitmap(screen_dc, width, height);
if bmp.is_null() {
DeleteDC(screen_dc);
DeleteDC(dc);
return Err("Can't create a Windows buffer".into());
}
let res = SelectObject(dc, bmp as _);
if res.is_null() || res == HGDI_ERROR {
DeleteDC(screen_dc);
DeleteDC(dc);
DeleteObject(bmp as _);
return Err("Can't select Windows buffer".into());
}
Ok(Self {
screen_dc,
dc,
bmp,
width,
height,
})
}
}
pub fn frame(&self, data: &mut Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
unsafe {
let res = BitBlt(
self.dc,
0,
0,
self.width,
self.height,
self.screen_dc,
0,
0,
SRCCOPY, // | CAPTUREBLT, // CAPTUREBLT enable layered window but also make cursor blinking
);
if res == 0 {
return Err("Failed to copy screen to Windows buffer".into());
}
let stride = self.width * PIXEL_WIDTH;
let size: usize = (stride * self.height) as usize;
let mut data1: Vec<u8> = Vec::with_capacity(size);
data1.set_len(size);
data.resize(size, 0);
let mut bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: size_of::<BITMAPINFOHEADER>() as _,
biWidth: self.width as _,
biHeight: self.height as _,
biPlanes: 1,
biBitCount: (8 * PIXEL_WIDTH) as _,
biCompression: BI_RGB,
biSizeImage: (self.width * self.height * PIXEL_WIDTH) as _,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
},
bmiColors: [RGBQUAD {
rgbBlue: 0,
rgbGreen: 0,
rgbRed: 0,
rgbReserved: 0,
}],
};
// copy bits into Vec
let res = GetDIBits(
self.dc,
self.bmp,
0,
self.height as _,
&mut data[0] as *mut u8 as _,
&mut bmi as _,
DIB_RGB_COLORS,
);
if res == 0 {
return Err("GetDIBits failed".into());
}
crate::common::ARGBMirror(
data.as_ptr(),
stride,
data1.as_mut_ptr(),
stride,
self.width,
self.height,
);
crate::common::ARGBRotate(
data1.as_ptr(),
stride,
data.as_mut_ptr(),
stride,
self.width,
self.height,
180,
);
Ok(())
}
}
}
impl Drop for CapturerGDI {
fn drop(&mut self) {
unsafe {
DeleteDC(self.screen_dc);
DeleteDC(self.dc);
DeleteObject(self.bmp as _);
}
}
}
#[cfg(test)]
mod tests {
use super::super::*;
use super::*;
#[test]
fn test() {
match Displays::new().unwrap().next() {
Some(d) => {
let w = d.width();
let h = d.height();
let c = CapturerGDI::new(d.name(), w, h).unwrap();
let mut data = Vec::new();
c.frame(&mut data).unwrap();
let mut bitflipped = Vec::with_capacity((w * h * 4) as usize);
for y in 0..h {
for x in 0..w {
let i = (w * 4 * y + 4 * x) as usize;
bitflipped.extend_from_slice(&[data[i + 2], data[i + 1], data[i], 255]);
}
}
repng::encode(
std::fs::File::create("gdi_screen.png").unwrap(),
d.width() as u32,
d.height() as u32,
&bitflipped,
)
.unwrap();
}
_ => {
assert!(false);
}
}
}
}

539
libs/scrap/src/dxgi/mod.rs Normal file
View File

@@ -0,0 +1,539 @@
use std::{io, mem, ptr, slice};
pub mod gdi;
pub use gdi::CapturerGDI;
use winapi::{
shared::dxgi::{
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIResource, IDXGISurface,
IID_IDXGIFactory1, IID_IDXGISurface, DXGI_MAP_READ, DXGI_OUTPUT_DESC,
DXGI_RESOURCE_PRIORITY_MAXIMUM,
},
shared::dxgi1_2::IDXGIOutputDuplication,
// shared::dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
shared::dxgi1_2::{IDXGIOutput1, IID_IDXGIOutput1},
shared::dxgitype::DXGI_MODE_ROTATION,
shared::minwindef::{TRUE, UINT},
shared::ntdef::LONG,
shared::windef::HMONITOR,
shared::winerror::{
DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_INVALID_CALL, DXGI_ERROR_NOT_CURRENTLY_AVAILABLE,
DXGI_ERROR_SESSION_DISCONNECTED, DXGI_ERROR_UNSUPPORTED, DXGI_ERROR_WAIT_TIMEOUT,
E_ACCESSDENIED, E_INVALIDARG, S_OK,
},
um::d3d11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, IID_ID3D11Texture2D,
D3D11_CPU_ACCESS_READ, D3D11_SDK_VERSION, D3D11_USAGE_STAGING,
},
um::d3dcommon::D3D_DRIVER_TYPE_UNKNOWN,
um::winnt::HRESULT,
};
//TODO: Split up into files.
pub struct Capturer {
device: *mut ID3D11Device,
display: Display,
context: *mut ID3D11DeviceContext,
duplication: *mut IDXGIOutputDuplication,
fastlane: bool,
surface: *mut IDXGISurface,
data: *const u8,
len: usize,
width: usize,
height: usize,
use_yuv: bool,
yuv: Vec<u8>,
gdi_capturer: Option<CapturerGDI>,
gdi_buffer: Vec<u8>,
}
impl Capturer {
pub fn new(display: Display, use_yuv: bool) -> io::Result<Capturer> {
let mut device = ptr::null_mut();
let mut context = ptr::null_mut();
let mut duplication = ptr::null_mut();
let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() };
let mut gdi_capturer = None;
let mut res = wrap_hresult(unsafe {
D3D11CreateDevice(
display.adapter as *mut _,
D3D_DRIVER_TYPE_UNKNOWN,
ptr::null_mut(), // No software rasterizer.
0, // No device flags.
ptr::null_mut(), // Feature levels.
0, // Feature levels' length.
D3D11_SDK_VERSION,
&mut device,
ptr::null_mut(),
&mut context,
)
});
if res.is_err() {
gdi_capturer = display.create_gdi();
println!("Fallback to GDI");
if gdi_capturer.is_some() {
res = Ok(());
}
} else {
res = wrap_hresult(unsafe {
let hres = (*display.inner).DuplicateOutput(device as *mut _, &mut duplication);
if hres != S_OK {
gdi_capturer = display.create_gdi();
println!("Fallback to GDI");
if gdi_capturer.is_some() {
S_OK
} else {
hres
}
} else {
hres
}
// NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk
// also try high version DXGI for better performance, e.g.
// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/dxgi-1-2-improvements
// dxgi-1-6 may too high, only support win10 (2018)
// https://docs.microsoft.com/zh-cn/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format
// DXGI_FORMAT_420_OPAQUE
// IDXGIOutputDuplication::GetFrameDirtyRects and IDXGIOutputDuplication::GetFrameMoveRects
// can help us update screen incrementally
/* // not supported on my PC, try in the future
let format : Vec<DXGI_FORMAT> = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE];
(*display.inner).DuplicateOutput1(
device as *mut _,
0 as UINT,
2 as UINT,
format.as_ptr(),
&mut duplication
)
*/
// if above not work, I think below should not work either, try later
// https://developer.nvidia.com/capture-sdk deprecated
// examples using directx + nvideo sdk for GPU-accelerated video encoding/decoding
// https://github.com/NVIDIA/video-sdk-samples
});
}
if let Err(err) = res {
unsafe {
if !device.is_null() {
(*device).Release();
}
if !context.is_null() {
(*context).Release();
}
}
return Err(err);
}
if !duplication.is_null() {
unsafe {
(*duplication).GetDesc(&mut desc);
}
}
Ok(Capturer {
device,
context,
duplication,
fastlane: desc.DesktopImageInSystemMemory == TRUE,
surface: ptr::null_mut(),
width: display.width() as usize,
height: display.height() as usize,
display,
data: ptr::null(),
len: 0,
use_yuv,
yuv: Vec::new(),
gdi_capturer,
gdi_buffer: Vec::new(),
})
}
pub fn is_gdi(&self) -> bool {
self.gdi_capturer.is_some()
}
pub fn set_gdi(&mut self) -> bool {
self.gdi_capturer = self.display.create_gdi();
self.is_gdi()
}
pub fn cancel_gdi(&mut self) {
self.gdi_buffer = Vec::new();
self.gdi_capturer.take();
}
unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<()> {
let mut frame = ptr::null_mut();
let mut info = mem::MaybeUninit::uninit().assume_init();
self.data = ptr::null();
wrap_hresult((*self.duplication).AcquireNextFrame(timeout, &mut info, &mut frame))?;
if *info.LastPresentTime.QuadPart() == 0 {
return Err(std::io::ErrorKind::WouldBlock.into());
}
if self.fastlane {
let mut rect = mem::MaybeUninit::uninit().assume_init();
let res = wrap_hresult((*self.duplication).MapDesktopSurface(&mut rect));
(*frame).Release();
if let Err(err) = res {
Err(err)
} else {
self.data = rect.pBits;
self.len = self.height * rect.Pitch as usize;
Ok(())
}
} else {
self.surface = ptr::null_mut();
self.surface = self.ohgodwhat(frame)?;
let mut rect = mem::MaybeUninit::uninit().assume_init();
wrap_hresult((*self.surface).Map(&mut rect, DXGI_MAP_READ))?;
self.data = rect.pBits;
self.len = self.height * rect.Pitch as usize;
Ok(())
}
}
// copy from GPU memory to system memory
unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<*mut IDXGISurface> {
let mut texture: *mut ID3D11Texture2D = ptr::null_mut();
(*frame).QueryInterface(
&IID_ID3D11Texture2D,
&mut texture as *mut *mut _ as *mut *mut _,
);
let mut texture_desc = mem::MaybeUninit::uninit().assume_init();
(*texture).GetDesc(&mut texture_desc);
texture_desc.Usage = D3D11_USAGE_STAGING;
texture_desc.BindFlags = 0;
texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
texture_desc.MiscFlags = 0;
let mut readable = ptr::null_mut();
let res = wrap_hresult((*self.device).CreateTexture2D(
&mut texture_desc,
ptr::null(),
&mut readable,
));
if let Err(err) = res {
(*frame).Release();
(*texture).Release();
(*readable).Release();
Err(err)
} else {
(*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM);
let mut surface = ptr::null_mut();
(*readable).QueryInterface(
&IID_IDXGISurface,
&mut surface as *mut *mut _ as *mut *mut _,
);
(*self.context).CopyResource(readable as *mut _, texture as *mut _);
(*frame).Release();
(*texture).Release();
(*readable).Release();
Ok(surface)
}
}
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<&'a [u8]> {
unsafe {
// Release last frame.
// No error checking needed because we don't care.
// None of the errors crash anyway.
let result = {
if let Some(gdi_capturer) = &self.gdi_capturer {
match gdi_capturer.frame(&mut self.gdi_buffer) {
Ok(_) => &self.gdi_buffer,
Err(err) => {
return Err(io::Error::new(io::ErrorKind::Other, err.to_string()));
}
}
} else {
if self.fastlane {
(*self.duplication).UnMapDesktopSurface();
} else {
if !self.surface.is_null() {
(*self.surface).Unmap();
(*self.surface).Release();
self.surface = ptr::null_mut();
}
}
(*self.duplication).ReleaseFrame();
self.load_frame(timeout)?;
slice::from_raw_parts(self.data, self.len)
}
};
Ok({
if self.use_yuv {
crate::common::bgra_to_i420(
self.width as usize,
self.height as usize,
&result,
&mut self.yuv,
);
&self.yuv[..]
} else {
result
}
})
}
}
}
impl Drop for Capturer {
fn drop(&mut self) {
unsafe {
if !self.surface.is_null() {
(*self.surface).Unmap();
(*self.surface).Release();
}
if !self.duplication.is_null() {
(*self.duplication).Release();
}
if !self.device.is_null() {
(*self.device).Release();
}
if !self.context.is_null() {
(*self.context).Release();
}
}
}
}
pub struct Displays {
factory: *mut IDXGIFactory1,
adapter: *mut IDXGIAdapter1,
/// Index of the CURRENT adapter.
nadapter: UINT,
/// Index of the NEXT display to fetch.
ndisplay: UINT,
}
impl Displays {
pub fn new() -> io::Result<Displays> {
let mut factory = ptr::null_mut();
wrap_hresult(unsafe { CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory) })?;
let factory = factory as *mut IDXGIFactory1;
let mut adapter = ptr::null_mut();
unsafe {
// On error, our adapter is null, so it's fine.
(*factory).EnumAdapters1(0, &mut adapter);
};
Ok(Displays {
factory,
adapter,
nadapter: 0,
ndisplay: 0,
})
}
// No Adapter => Some(None)
// Non-Empty Adapter => Some(Some(OUTPUT))
// End of Adapter => None
fn read_and_invalidate(&mut self) -> Option<Option<Display>> {
// If there is no adapter, there is nothing left for us to do.
if self.adapter.is_null() {
return Some(None);
}
// Otherwise, we get the next output of the current adapter.
let output = unsafe {
let mut output = ptr::null_mut();
(*self.adapter).EnumOutputs(self.ndisplay, &mut output);
output
};
// If the current adapter is done, we free it.
// We return None so the caller gets the next adapter and tries again.
if output.is_null() {
unsafe {
(*self.adapter).Release();
self.adapter = ptr::null_mut();
}
return None;
}
// Advance to the next display.
self.ndisplay += 1;
// We get the display's details.
let desc = unsafe {
let mut desc = mem::MaybeUninit::uninit().assume_init();
(*output).GetDesc(&mut desc);
desc
};
// We cast it up to the version needed for desktop duplication.
let mut inner: *mut IDXGIOutput1 = ptr::null_mut();
unsafe {
(*output).QueryInterface(&IID_IDXGIOutput1, &mut inner as *mut *mut _ as *mut *mut _);
(*output).Release();
}
// If it's null, we have an error.
// So we act like the adapter is done.
if inner.is_null() {
unsafe {
(*self.adapter).Release();
self.adapter = ptr::null_mut();
}
return None;
}
unsafe {
(*self.adapter).AddRef();
}
Some(Some(Display {
inner,
adapter: self.adapter,
desc,
}))
}
}
impl Iterator for Displays {
type Item = Display;
fn next(&mut self) -> Option<Display> {
if let Some(res) = self.read_and_invalidate() {
res
} else {
// We need to replace the adapter.
self.ndisplay = 0;
self.nadapter += 1;
self.adapter = unsafe {
let mut adapter = ptr::null_mut();
(*self.factory).EnumAdapters1(self.nadapter, &mut adapter);
adapter
};
if let Some(res) = self.read_and_invalidate() {
res
} else {
// All subsequent adapters will also be empty.
None
}
}
}
}
impl Drop for Displays {
fn drop(&mut self) {
unsafe {
(*self.factory).Release();
if !self.adapter.is_null() {
(*self.adapter).Release();
}
}
}
}
pub struct Display {
inner: *mut IDXGIOutput1,
adapter: *mut IDXGIAdapter1,
desc: DXGI_OUTPUT_DESC,
}
impl Display {
pub fn width(&self) -> LONG {
self.desc.DesktopCoordinates.right - self.desc.DesktopCoordinates.left
}
pub fn height(&self) -> LONG {
self.desc.DesktopCoordinates.bottom - self.desc.DesktopCoordinates.top
}
pub fn attached_to_desktop(&self) -> bool {
self.desc.AttachedToDesktop != 0
}
pub fn rotation(&self) -> DXGI_MODE_ROTATION {
self.desc.Rotation
}
fn create_gdi(&self) -> Option<CapturerGDI> {
if let Ok(res) = CapturerGDI::new(self.name(), self.width(), self.height()) {
Some(res)
} else {
None
}
}
pub fn hmonitor(&self) -> HMONITOR {
self.desc.Monitor
}
pub fn name(&self) -> &[u16] {
let s = &self.desc.DeviceName;
let i = s.iter().position(|&x| x == 0).unwrap_or(s.len());
&s[..i]
}
pub fn is_online(&self) -> bool {
self.desc.AttachedToDesktop != 0
}
pub fn origin(&self) -> (LONG, LONG) {
(
self.desc.DesktopCoordinates.left,
self.desc.DesktopCoordinates.top,
)
}
}
impl Drop for Display {
fn drop(&mut self) {
unsafe {
(*self.inner).Release();
(*self.adapter).Release();
}
}
}
fn wrap_hresult(x: HRESULT) -> io::Result<()> {
use std::io::ErrorKind::*;
Err((match x {
S_OK => return Ok(()),
DXGI_ERROR_ACCESS_LOST => ConnectionReset,
DXGI_ERROR_WAIT_TIMEOUT => TimedOut,
DXGI_ERROR_INVALID_CALL => InvalidData,
E_ACCESSDENIED => PermissionDenied,
DXGI_ERROR_UNSUPPORTED => ConnectionRefused,
DXGI_ERROR_NOT_CURRENTLY_AVAILABLE => Interrupted,
DXGI_ERROR_SESSION_DISCONNECTED => ConnectionAborted,
E_INVALIDARG => InvalidInput,
_ => {
// 0x8000ffff https://www.auslogics.com/en/articles/windows-10-update-error-0x8000ffff-fixed/
return Err(io::Error::new(Other, format!("Error code: {:#X}", x)));
}
})
.into())
}

20
libs/scrap/src/lib.rs Normal file
View File

@@ -0,0 +1,20 @@
#[cfg(quartz)]
extern crate block;
#[macro_use]
extern crate cfg_if;
pub extern crate libc;
#[cfg(dxgi)]
extern crate winapi;
pub use common::*;
#[cfg(quartz)]
pub mod quartz;
#[cfg(x11)]
pub mod x11;
#[cfg(dxgi)]
pub mod dxgi;
mod common;

Some files were not shown because too many files have changed in this diff Show More