内容简介:Our goal for tonari is to build a virtual doorway to another space that allows for truly natural human interactions. Nearly two years in development, tonari is, to the best of our knowledge, the lowest-latency high resolution production-ready "teleconferen
T
Our goal for tonari is to build a virtual doorway to another space that allows for truly natural human interactions. Nearly two years in development, tonari is, to the best of our knowledge, the lowest-latency high resolution production-ready "teleconferencing" (we are truly not fond of that word) product available.
- 130ms glass-to-glass latency (the time from light hitting the camera to when it appears on-screen on the other side)
- 3K, 60fps video transmission
- Audiophile-quality stereo audio
Compare this to the typical 315-500ms latency for Zoom and
Since launchingour first pilot in February, we've experienced no software-related downtime (tripping over ethernet cables is a different story). A
In the beginning (or: why we're not WebRTC)
The
While that prototype (and our opinionated vision of the future) got us grant funding, we knew that tonari would be dead on arrival unless we could
We figured, “Okay , so we can just
A week of struggling with WebRTC’s nearly 750,000 LoC behemoth of a codebase revealed just how painful a single small change could be — how hard it was to test, and feel truly safe, with the code you were dealing with.
Let there be light...weight code
So in a furious (read: calm and thoroughly-discussed) rage quit we decided it was easier to re-implement the whole stack from scratch. We wanted to know and understand every line of code being run on our hardware, and it should be designed for the exact hardware we wanted.
Thus began our journey to the depths beyond high-level interfaces like a browser or existing RTC project, and into the world of low-level systems and hardware interaction from scratch.
We needed it to
We discussed and ruled out a handful of alternative approaches:
- Security: C and C++ are memory- and concurrency-unsafe, and their disparate and seemingly infinite build systems make it hard to have a consistent and simple development experience.
- Performance: Java,
C#, and Go' s memory management is opaque and can be difficult to work with in latency-sensitive applications where you want full control over your memory. - Maintainability: Haskell, Nim, D, and a handful of other more bespoke languages tend to be more limited in tooling, community, and hire-ability.
Rust is really the only production-ready language that we found confidently satisfies these needs.
Finding beauty in Rust
Rust's beauty lies in the countless decisions made by the development community that constantly make you feel like you have can ten cakes and eat all of them too.
- Its build system is opinionated, and cleanly designed. It is itself a complete ecosystem that makes introducing new engineers to your project and setting up dev environments remarkably simple.
- The memory and concurrency safety guarantees cannot be over-appreciated. We're confident that we wouldn't have done our first deployment yet if we had continued this in C++ - we'd still probably be stuck on subtle snags.
- Our ability to interact at the lowest level with hardware via APIs like CUDA, oftentimes through existing
crates (Rust's term for a code library), has allowed us to have higher standards about the latency we want from our first production release.
As tonari is getting more advanced, we're now choosing embedded microcontrollers whose firmware can be written in Rust so we don't have to leave our idyllic utopia into the old world of unsafe system programming.
We're not going to cat Cargo.toml
here, instead focusing on some select crates that have earned the prestigious award of a lifetime invitation to each of our birthday parties forever.
"Better-than-std" crates
-
crossbeam
is better for inter-thread communication thanstd::sync::mpsc
in almost every way, and may be merged intostd
eventually. -
parking_lot
has a mutex implementation better thanstd::sync::Mutex
in almost every way, and may be merged into the standard library (one day). It also provides many other useful synchronization primitives. -
bytes
is a more robust, and often more performant, way to play with bytes compared toVec<u8>
. -
socket2
is what you will end up at if you are ever doing lower-level networking optimizations.
-
fern
is a dead-simple way to customize and prettify your logging output. We use it to keep our logs readable and internally standardized. -
structopt
is how you always dreamed CLI arguments would be handled. There's no reason not to use it unless you're going for bare-minimum dependencies.
-
cargo-release
allows us to cut internal releases painlessly. -
cargo-udeps
identifies unused dependencies and allows us to keep our build times minimal. -
cargo tree
(recently integrated in cargo) shows a dependency tree that's useful in many ways, but namely in identifying ways to minimize dependencies. -
cargo-geiger
helps us quickly evaluate external dependencies for possible security (or correctness) concerns. -
cargo-flamegraph
helps us enormously when tracking down performance hot-spots in our code.
The tonari codebase is a monorepo. At its root we have a Cargo workspace with a binaries
crate, and a number of supporting library crates.
Having our crates in one repo makes them easy to reference in our binaries
crate without needing to publish to crates.io or get too fancy with specifying git dependencies in our Cargo.toml
. When the time comes to publish these libraries as open source, it's trivial to break it out into its own repo.
Library, binary, why not both?
We have one main library crate that contains a unified API for talking to hardware, media codecs, network protocols, etc. Outside of that private API, we also have standalone crates in our workspace that we consider candidates for open-sourcing. For example, we’ve written our own actor framework fit for long-running high-throughput actors, as well as our own network protocol for reliable, high-bandwidth, low-latency media streaming. We use separate binaries for different parts of the tonari system and each of these lives in binaries
, a combination library/binary crate. Its library modules contains a set of reusable actors that combine our private API with our actor system, and then a collection of individual binaries that consume these actors and define the plumbing between them.
Flags as far as the eye can see
We make extensive use of feature flags to allow development of our project on different OSes (like Brian's 1970s-era MacBook Pro) or different hardware configurations. This allows us to easily swap out camera hardware without extra runtime checks or using awful sed
hacks. For example, Linux uses v4l2
(Video For Linux...2) to access most webcams, but other webcams might have their own SDK. To compile for platforms that don't use v4l2
or when an SDK isn't available for a particular OS, we can put those SDKs behind feature flags and export a common interface. As a (simplified) concrete example, let's say we have a common camera interface defined as a trait:
pub trait Capture { /// Capture a frame from a camera, returning a Vec of RGB image bytes. fn capture(&mut self) -> Vec<u8>; }
Let's also say we have three different camera interfaces - v4l2
, corevideo
, and polaroid.
We can make our binaries work exclusively with this trait to be flexible, and we can swap in different implementations of Capture
with feature flags.
#[cfg(feature = "v4l2")] mod v4l2 { pub struct V4l2Capture { ... } impl Capture for V4l2Capture { fn capture(&mut self) -> Vec<u8> { ... } } } #[cfg(feature = "corevideo")] mod corevideo { pub struct CoreVideoCapture { ... } impl Capture for CoreVideoCapture { fn capture(&mut self) -> Vec<u8> { ... } } } #[cfg(feature = "polaroid")] mod polaroid { pub struct PolaroidCapture { ... } impl Capture for PolaroidCapture { fn capture(&mut self) -> Vec<u8> { ... } } } #[cfg(feature = "v4l2")] pub type VideoCapture = v4l2::V4l2Capture; #[cfg(feature = "corevideo")] pub type VideoCapture = corevideo::CoreVideoCapture; #[cfg(feature = "polaroid")] pub type VideoCapture = polaroid::PolaroidCapture;
If we make our code work with things which implement the Capture
trait instead of concrete types, we can now compile on and target various platforms by simply toggling feature flags. For example, we can have a struct which has a field - video_capture: Box<dyn Capture>
which will let us store any type which can Capture
from a camera. An example Cargo.toml
file to support the capture implementations we wrote above might look something like this:
[package] name = "tonari" version = "1.0.0" edition = "2018" [features] default = ["v4l2"] macos = ["corevideo"] classic = ["polaroid"] v4l2 = ["rscam"] [dependencies] rscam = { version = "0.5", optional = true } # v4l2 linux camera library corevideo = { version = "0.1", optional = true } # MacOS camera library polaroid = { version = "0.1", optional = true } # Polaroid camera library (very slow FPS)
This way we can avoid building and linking to platform-specific libraries like v4l2
which aren't available everywhere.
Learning Rust on the job
A year after switching over to Rust, we onboarded our fourth engineer to the team, who didn't have much prior experience in either Rust or systems engineering. While the learning curve is undeniable (
As mentioned, memory and concurrency safety built into the language means that an
There is usually one obvious "right way" to do a thing in Rust, unlike many other languages. Code that isn't written the "right way" tends to stand out, and is easy to pick out in reviews, often times automatically by cargo clippy
.
In practice, this has meant that new engineers can quickly start contributing production-ready code. Code reviews can remain focused on the implementation, as opposed to expending energy doing manual correctness checks.
In the IDE department, Rust still shows its relative immaturity compared to some of its predecessors. This year especially, though, there have been huge strides, and each of us has found a pretty comfortable development environment at this point. • 1 of us use macOS, and 3 of us use Linux (Arch, Ubuntu, and Pop!_OS, revealing our respective levels of masochism) • 2 of us use VS Code with the rust-analyzer
plugin, and 2 of us use Sublime Text with RustEnhanced
. We’re often sharing setups and trying each other’s out (except Brian, who is stuck in his ways at the ripe old age of 29), and we’re constantly keeping an eye on new development tools that can help us work better together.
Code style guidelines are dead, long live rustfmt
You know what's wild ? We don't have a code style guideline document that you have to read before submitting code. We don't need one. We just enforce rustfmt
. Let me tell you: it really takes the edge off of code reviews.
Our code reviews are straightforward since there's only four of us so far, and we are lucky to have a lot of trust amongst us. Our main goal is to have at least two pairs of eyes on every line of code, and to not block each other so we can maintain momentum.
We use Google’s Cloud Builder for running our CI build, as our infrastructure stack is mostly built on GCP and it allows for easy-ish tweaking of build machine specs and custom build images. It's triggered for every commit and runs cargo clippy
and cargo build
. We pass -D warnings
to the compiler to promote warnings into errors to ensure our changes don’t make our poor fellow coworker’s rustc
target
and .cargo
directories in Cloud Storage, so it can be download next time for incremental build.
for crate in $(ls */Cargo.toml | xargs dirname); do pushd $crate # Lint. cargo +$NIGHTLY_TOOLCHAIN clippy --no-default-features -- -D warnings # Build. time RUSTFLAGS="-D warnings" cargo build --no-default-features # Test. time cargo test --no-default-features popd done
We've also heard good things about sccache
and will evaluate it soon!
Integrating with existing C/C++ libraries
The Rust ecosystem is great, but there are huge existing projects out there which just aren't feasible to port to Rust yet without a huge time investment. webrtc-audio-processing
is a good example. The benefits it provides (clear audio with no vocal echoes or feedback) are huge and porting it to Rust in the near-term is not likely (it's around 80k lines of C and C++ code).
Thankfully, Rust makes it quite easy to use existing C and C++ libraries.
A lot of this process is fairly automatic for libraries with straightforward or commonly-used build processes. Creating the higher level safe API is important though - the Rust API that bindgen provides is not very fun to use directly as it's unsafe and typically not very idiomatic. Fortunately, once you have a higher level API you can eventually swap the C library out with your own Rust version and consumers of the crate are none the wiser.
These features let us work with APIs and hardware which would either never have a native Rust API or take months or years to re-implement. Low-level OS libraries, large codebases such as webrtc-audio-processing
, and manufacturer-supplied camera SDKs all become available for use in our Rust codebase without having to move our entire application language over to C++, while still performing as if we had.
Some C++ libraries are difficult to interface with directly from Rust. You have to whitelist types because bindgen
can't handle all the std::*
types that get pulled in, it doesn't play well with templated functions and copy/move constructors, and a whole host of other issues documented here .
To get around these issues, we'll typically create a simplified C++ header and source wrapper which exports bindgen
-friendly functions. It's a bit more work, but far less work than porting the entire library to Rust. You can see an example of this wrapper creation here .
With all of Rust's ecosystem, and C/C++ projects being only a bindgen
invocation away, we have easy access to some of the highest quality software packages in existence, all without having to sacrifice execution speed.
Rust is not problem-free. It's a relatively new language that is constantly evolving, and there are shortcomings that you should consider when evaluating a move to Rust.
- Long compile times ; the popular xkcd comic , the coffee break while waiting for Rust code to compile is very real. Our codebase for example takes about 8 mins to compile non-incrementally on a moderately beefy laptop, but it can be a lot worse . The Rust compiler has a lot of work to do enforcing strong language guarantees, and it must compile your entire dependency tree from source. Incremental builds are better, but some crates come with build scripts that pull and compile non-Rust dependency code, and the build cache may need to be cleared when upgrading their versions and switching branches.
- Library coverage ; The library ecosystem is quite mature but the coverage is limited compared to C / C++. We ended up implementing our own jitter buffer, and we also wrap several C / C++ libraries with Rust's bindgen , which means we have
unsafe
regions in our Rust code. Non-trivial projects tend to have some minimal amount ofunsafe
coding, which adds to the learning curve and chance of memory bugs. - Rust demands you to write correct and explicit code up-front . If you get it wrong, the compiler won't let it slide. If you care less about concurrency and memory guarantees, development can feel needlessly slow. Rust developers are constantly working on improving the error messages, though. They are friendly and actionable, often with an included fix suggestion. A good foundational model on memory & concurrency also helps getting over the initial hump quicker, so we suggest taking time to truly understand the language and its guarantees.
- Rust's type inferencer is so strong it makes you feel sometimes like you're using a dynamically-typed language. That said, there comes the moment where it does not quite work the way you want, especially when generics and deref coercion are involved, and you end up having to fumble around to make the inferencer happy. It can come with frustration, and it's really helpful to have someone in the team who already has gone through that stage of learning. With enough patience, that frustration often turns into a wow moment, with a deeper understanding of the language design and why it's done that way, as well as a possible bug that you would have otherwise introduced.
- Language evolution ; The Rust language is constantly evolving. Some of the language constructs like
async
are still volatile, and you may find it's best to stick with threads and the standard library when you can.
Consequences of selecting Rust so far
Experiencing no software-related downtime so far is both a pleasant surprise, and a testament to the safety provided by Rust's guarantees. Rust has also made it easy to write performant code with efficient resource usage - both our CPU and memory usage has been predictable and consistent. Without a garbage collector, we can guarantee consistent latency and frame rates.
Our experience maintaining a Rust codebase has also been great. We've been able to introduce significant improvements to our latency through sizable changes to our codebase with confidence. A clean compile doesn't always imply everything will work, but honestly, that's been the case more often than not.
The end result is a reliable product which hasn't been a nightmare to maintain (strong words, we know), and performs quickly at the high specs we demand for frame rate, latency, and resource efficiency. Again, it's hard to imagine where we might be without Rust!
webrtc-audio-processing
. This is one of the crates that used to live in the top level of our repo, and there are many more like it on their way to open-source.
There will be more on this subject later as we release more code, but one thing feels true: even before open-sourcing our crates, it’s felt very healthy to our code's clarity to assume each crate we crate privately will be open-sourced. This philosophy keeps our boundaries between crates more clean, and encourages us to make quicker decisions about opening up parts of our codebase with minimal fuss.
Thanks for making it this far, we hope this brain-dump might have offered a useful thought or two for those getting into Rust, or those with advanced knowledge of Rust but using it in different environments. Please feel free to say hi athey@tonari.no or find us on Twitter with any feedback.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人人都在晒,凭什么你出彩
【美】奥斯丁•克莱恩 / 张舜芬、徐立妍 / 北京联合出版公司 / 2015-4 / 38.00
1. 《纽约时报》、亚马逊畅销书排名第1位、好评如潮的创意营销书。《出版人周刊》称其在社交网络时代“在安全范围内提供了实用的自我营销策略”。 2. TED演讲者创意分享:晒对了,全世界都为你点赞:别人在朋友圈、微博晒自拍、晒孩子、晒吃喝,你来晒创意、晒灵感、晒工作、晒收获,发出自己的声音,找到伙伴,机会也会主动找上门! 3. 10堂创意课+手绘涂鸦,所有人都能轻松读完、迅速学会的创意小......一起来看看 《人人都在晒,凭什么你出彩》 这本书的介绍吧!