mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-19 07:09:10 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619fba5838 | ||
|
|
9103fd3328 | ||
|
|
b7696a2e8c | ||
|
|
5618b9ec43 | ||
|
|
9342311daa | ||
|
|
74e1bc2878 | ||
|
|
de4bb684af | ||
|
|
0503a4d5b6 | ||
|
|
8873bce514 | ||
|
|
9f81d7f1da | ||
|
|
823f0ab0c8 | ||
|
|
5111dc7fe6 | ||
|
|
059f156c3e | ||
|
|
efa4fe4186 | ||
|
|
8445579fc5 | ||
|
|
f43f5df99b | ||
|
|
d1013487e2 |
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal 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
4296
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
109
Cargo.toml
Normal file
109
Cargo.toml
Normal 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
674
LICENSE
Normal 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>.
|
||||
47
README.md
47
README.md
@@ -1,5 +1,46 @@
|
||||
### RustDesk | Your Remote Desktop Software
|
||||
# RustDesk | Your Remote Desktop Software
|
||||
|
||||
The best open-source remote desktop software, written in Rust. Works out of the box, no configuration required. Great alternative to TeamViewer and AnyDesk! You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/blog/id-relay-set/), or write your own rendezvous/relay server.
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Dependences
|
||||
|
||||
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
|
||||
|
||||
* run `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
|
||||
|
||||
## Snapshot
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
This is a repository used to release RustDesk software and track issues.
|
||||
|
||||
[**DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
74
build.rs
Normal file
74
build.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
#[cfg(windows)]
|
||||
fn build_windows() {
|
||||
cc::Build::new().file("src/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();
|
||||
}
|
||||
25
libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
20
libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
19
libs/enigo/.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
19
libs/enigo/.github/ISSUE_TEMPLATE/question.md
vendored
Normal 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
14
libs/enigo/.gitignore
vendored
Normal 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
15
libs/enigo/.travis.yml
Normal 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
13
libs/enigo/.vscode/launch.json
vendored
Normal 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
41
libs/enigo/Cargo.toml
Normal 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
21
libs/enigo/LICENSE
Normal 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
46
libs/enigo/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
[](https://travis-ci.org/enigo-rs/enigo)
|
||||
[](https://ci.appveyor.com/project/pythoneer/enigo-85xiy)
|
||||
[](https://dependencyci.com/github/pythoneer/enigo)
|
||||
[](https://docs.rs/enigo)
|
||||
[](https://crates.io/crates/enigo)
|
||||
[](https://discord.gg/Eb8CsnN)
|
||||
[](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
121
libs/enigo/appveyor.yml
Normal 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
61
libs/enigo/build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
11
libs/enigo/examples/dsl.rs
Normal file
11
libs/enigo/examples/dsl.rs
Normal 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}");
|
||||
}
|
||||
12
libs/enigo/examples/key.rs
Normal file
12
libs/enigo/examples/key.rs
Normal 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'));
|
||||
}
|
||||
16
libs/enigo/examples/keyboard.rs
Normal file
16
libs/enigo/examples/keyboard.rs
Normal 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);
|
||||
}
|
||||
37
libs/enigo/examples/mouse.rs
Normal file
37
libs/enigo/examples/mouse.rs
Normal 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);
|
||||
}
|
||||
22
libs/enigo/examples/timer.rs
Normal file
22
libs/enigo/examples/timer.rs
Normal 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
1
libs/enigo/rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
wrap_comments = true
|
||||
184
libs/enigo/src/dsl.rs
Normal file
184
libs/enigo/src/dsl.rs
Normal 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
525
libs/enigo/src/lib.rs
Normal 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
361
libs/enigo/src/linux.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
libs/enigo/src/macos/keycodes.rs
Normal file
72
libs/enigo/src/macos/keycodes.rs
Normal 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;
|
||||
680
libs/enigo/src/macos/macos_impl.rs
Normal file
680
libs/enigo/src/macos/macos_impl.rs
Normal 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 {}
|
||||
4
libs/enigo/src/macos/mod.rs
Normal file
4
libs/enigo/src/macos/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
mod macos_impl;
|
||||
|
||||
pub mod keycodes;
|
||||
pub use self::macos_impl::Enigo;
|
||||
73
libs/enigo/src/win/keycodes.rs
Normal file
73
libs/enigo/src/win/keycodes.rs
Normal 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;
|
||||
4
libs/enigo/src/win/mod.rs
Normal file
4
libs/enigo/src/win/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
mod win_impl;
|
||||
|
||||
pub mod keycodes;
|
||||
pub use self::win_impl::Enigo;
|
||||
366
libs/enigo/src/win/win_impl.rs
Normal file
366
libs/enigo/src/win/win_impl.rs
Normal 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
4
libs/hbb_common/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
src/protos/
|
||||
46
libs/hbb_common/Cargo.toml
Normal file
46
libs/hbb_common/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "hbb_common"
|
||||
version = "0.1.0"
|
||||
authors = ["rustdesk<info@rustdesk.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
9
libs/hbb_common/build.rs
Normal 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.");
|
||||
}
|
||||
401
libs/hbb_common/protos/message.proto
Normal file
401
libs/hbb_common/protos/message.proto
Normal 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;
|
||||
}
|
||||
}
|
||||
133
libs/hbb_common/protos/rendezvous.proto
Normal file
133
libs/hbb_common/protos/rendezvous.proto
Normal 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;
|
||||
}
|
||||
}
|
||||
274
libs/hbb_common/src/bytes_codec.rs
Normal file
274
libs/hbb_common/src/bytes_codec.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
libs/hbb_common/src/compress.rs
Normal file
50
libs/hbb_common/src/compress.rs
Normal 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
|
||||
}
|
||||
688
libs/hbb_common/src/config.rs
Normal file
688
libs/hbb_common/src/config.rs
Normal 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
554
libs/hbb_common/src/fs.rs
Normal 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
215
libs/hbb_common/src/lib.rs
Normal 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
135
libs/hbb_common/src/quic.rs
Normal 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
146
libs/hbb_common/src/tcp.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
75
libs/hbb_common/src/udp.rs
Normal file
75
libs/hbb_common/src/udp.rs
Normal 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
2
libs/magnum-opus/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target
|
||||
Cargo.lock
|
||||
18
libs/magnum-opus/Cargo.toml
Normal file
18
libs/magnum-opus/Cargo.toml
Normal 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"
|
||||
201
libs/magnum-opus/LICENSE-APACHE
Normal file
201
libs/magnum-opus/LICENSE-APACHE
Normal 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.
|
||||
19
libs/magnum-opus/LICENSE-MIT
Normal file
19
libs/magnum-opus/LICENSE-MIT
Normal 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.
|
||||
22
libs/magnum-opus/README.md
Normal file
22
libs/magnum-opus/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# magnum-opus [](https://crates.io/crates/magnum-opus) [](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
89
libs/magnum-opus/build.rs
Normal 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()
|
||||
}
|
||||
1
libs/magnum-opus/opus_ffi.h
Normal file
1
libs/magnum-opus/opus_ffi.h
Normal file
@@ -0,0 +1 @@
|
||||
#include <opus/opus_multistream.h>
|
||||
843
libs/magnum-opus/src/lib.rs
Normal file
843
libs/magnum-opus/src/lib.rs
Normal 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())
|
||||
}
|
||||
6
libs/magnum-opus/src/opus_ffi.rs
Normal file
6
libs/magnum-opus/src/opus_ffi.rs
Normal 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"));
|
||||
10
libs/magnum-opus/tests/compile-fail/repacketize.rs
Normal file
10
libs/magnum-opus/tests/compile-fail/repacketize.rs
Normal 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 []);
|
||||
}
|
||||
13
libs/magnum-opus/tests/fec.rs
Normal file
13
libs/magnum-opus/tests/fec.rs
Normal 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);
|
||||
}
|
||||
29
libs/magnum-opus/tests/opus-padding.rs
Normal file
29
libs/magnum-opus/tests/opus-padding.rs
Normal 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);
|
||||
}
|
||||
127
libs/magnum-opus/tests/tests.rs
Normal file
127
libs/magnum-opus/tests/tests.rs
Normal 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
3
libs/parity-tokio-ipc/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target
|
||||
Cargo.lock
|
||||
.idea
|
||||
20
libs/parity-tokio-ipc/.travis.yml
Normal file
20
libs/parity-tokio-ipc/.travis.yml
Normal 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=
|
||||
24
libs/parity-tokio-ipc/Cargo.toml
Normal file
24
libs/parity-tokio-ipc/Cargo.toml
Normal 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"] }
|
||||
201
libs/parity-tokio-ipc/LICENSE-APACHE
Normal file
201
libs/parity-tokio-ipc/LICENSE-APACHE
Normal 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.
|
||||
25
libs/parity-tokio-ipc/LICENSE-MIT
Normal file
25
libs/parity-tokio-ipc/LICENSE-MIT
Normal 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.
|
||||
28
libs/parity-tokio-ipc/README.md
Normal file
28
libs/parity-tokio-ipc/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# parity-tokio-ipc
|
||||
|
||||
[](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.
|
||||
16
libs/parity-tokio-ipc/appveyor.yml
Normal file
16
libs/parity-tokio-ipc/appveyor.yml
Normal 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
|
||||
24
libs/parity-tokio-ipc/examples/client.rs
Normal file
24
libs/parity-tokio-ipc/examples/client.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
47
libs/parity-tokio-ipc/examples/server.rs
Normal file
47
libs/parity-tokio-ipc/examples/server.rs
Normal 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
|
||||
}
|
||||
7
libs/parity-tokio-ipc/examples/spam-clients.sh
Executable file
7
libs/parity-tokio-ipc/examples/spam-clients.sh
Executable 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
|
||||
154
libs/parity-tokio-ipc/src/lib.rs
Normal file
154
libs/parity-tokio-ipc/src/lib.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
163
libs/parity-tokio-ipc/src/unix.rs
Normal file
163
libs/parity-tokio-ipc/src/unix.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
488
libs/parity-tokio-ipc/src/win.rs
Normal file
488
libs/parity-tokio-ipc/src/win.rs
Normal 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
4
libs/pulsectl/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
**/target
|
||||
/target
|
||||
**/*.rs.bk
|
||||
.idea/
|
||||
129
libs/pulsectl/Cargo.lock
generated
Normal file
129
libs/pulsectl/Cargo.lock
generated
Normal 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
19
libs/pulsectl/Cargo.toml
Normal 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
13
libs/pulsectl/LICENSE.md
Normal 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
43
libs/pulsectl/README.md
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
34
libs/pulsectl/examples/change_device_vol.rs
Normal file
34
libs/pulsectl/examples/change_device_vol.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
libs/pulsectl/src/controllers/errors.rs
Normal file
51
libs/pulsectl/src/controllers/errors.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
595
libs/pulsectl/src/controllers/mod.rs
Normal file
595
libs/pulsectl/src/controllers/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
354
libs/pulsectl/src/controllers/types.rs
Normal file
354
libs/pulsectl/src/controllers/types.rs
Normal 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 struct’s volume member is unspecified.
|
||||
pub has_volume: bool,
|
||||
/// The volume can be set. If not set, the volume can still change even though clients can’t
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
54
libs/pulsectl/src/errors.rs
Normal file
54
libs/pulsectl/src/errors.rs
Normal 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
166
libs/pulsectl/src/lib.rs
Normal 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
4
libs/scrap/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
generated/
|
||||
32
libs/scrap/Cargo.toml
Normal file
32
libs/scrap/Cargo.toml
Normal 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
60
libs/scrap/README.md
Normal 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
118
libs/scrap/build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
51
libs/scrap/examples/ffplay.rs
Normal file
51
libs/scrap/examples/ffplay.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
libs/scrap/examples/list.rs
Normal file
16
libs/scrap/examples/list.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
161
libs/scrap/examples/record-screen.rs
Normal file
161
libs/scrap/examples/record-screen.rs
Normal 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(())
|
||||
}
|
||||
122
libs/scrap/examples/screenshot.rs
Normal file
122
libs/scrap/examples/screenshot.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
536
libs/scrap/src/common/codec.rs
Normal file
536
libs/scrap/src/common/codec.rs
Normal 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 {}
|
||||
188
libs/scrap/src/common/convert.rs
Normal file
188
libs/scrap/src/common/convert.rs
Normal 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 _,
|
||||
);
|
||||
}
|
||||
104
libs/scrap/src/common/dxgi.rs
Normal file
104
libs/scrap/src/common/dxgi.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
23
libs/scrap/src/common/mod.rs
Normal file
23
libs/scrap/src/common/mod.rs
Normal 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;
|
||||
125
libs/scrap/src/common/quartz.rs
Normal file
125
libs/scrap/src/common/quartz.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
25
libs/scrap/src/common/vpx.rs
Normal file
25
libs/scrap/src/common/vpx.rs
Normal 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"));
|
||||
87
libs/scrap/src/common/x11.rs
Normal file
87
libs/scrap/src/common/x11.rs
Normal 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
212
libs/scrap/src/dxgi/gdi.rs
Normal 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
539
libs/scrap/src/dxgi/mod.rs
Normal 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
20
libs/scrap/src/lib.rs
Normal 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
Reference in New Issue
Block a user