Closed Hoeze closed 6 years ago
Most of librespot's API returns futures. Whenever you get one of these you should run it through core.run
rather than calling wait
, just like you do for Session::connect
.
For example :
let track = core.run(Track::get(&session, track)).unwrap();
println!("{}", track.name);
Note that this will block the current thread until the track information has been retrieved.
If you want to perform other work you can combine the returned future using the tokio
and future-rs
libraries.
See https://tokio.rs/docs/getting-started/futures/#adding-a-timeout for an example of a timeout associated with a future.
Thank you very much, I thought that wait() would run that future :)
How does "player.rs" run this future?
Where is the thread spawned that completes Track::get(&session, track).wait()
?
Is this happening inside the internal.run()
loop?
I started explaining the architecture, but this ended up quite lengthy, sorry.
Here's the TLDR of futures/threads in librespot.
core.run
somewhere.core.run
to run a future you got from librespot or anything else, including an empty future.Session::connect
) should be safe to .wait()
, as long as the core is running on another thread.AudioFile
API is synchronous (it uses .wait()
internally). It must therefore only be used if another thread is running the core.Session::connect
returns a future which will asynchronously set up the connection (AP resolve, authentication). This future must be run through Core::run
.
While completing that future, Session::connect
uses Session::create
to create the Session
object (which is the result of the future) and create the "packet handling" future. This future is registered with tokio to run in the background (using handle.spawn
). Note that it does not return any useful value (It's type is BoxFuture<(), io::Error>
, ie return ()
)). It actually doesn't return as long as the connection is open.
However note that despite its name, spawn
only registered the packet handling future with tokio's event loop (I use core and event loop interchangeably). This means that it will not run if the core is not running, and packets will not be sent nor received. That means that after core.run(Session::connect(...))
returns the Session
object you should run the core with something else in order to keep processing packets. The packet handling future will be scheduled by the event loop when it has work to do.
At this point, when you make a request on a "low level" API (like AudioKeyManager
, MercuryManager
or ChannelManager
), it sends the corresponding packets to an in-process channel (these have nothing to do with the ChannelManager
, which deals with Spotify channels. The naming is confusing sorry), and the packet handling future will forward them to the socket (that's the Session::send_packet
function). They also create a channel specific to that request, store the tx side somewhere and return the receiver end. If you want to look into it AudioKeyManager
is definitely the simplest one. The others have a lot more parsing and state involved.
When a response comes back from Spotify, the packet handling future will pick it up and call the appropriate function (that's the Session::dispatch
function). That function will parse the response, find the channel associated with that request and send the response on there.
"Higher level" APIs like metadata and keymaster are just wrappers around these which prepare the request and parse the response.
Asynchronous requests can be made at any time, from any thread, even if the core isn't running. It will just enqueue packets onto the shared packet sending channel and return a future back. At this point there are three ways to receive data from the channel (future) that the request returned.
core.run
to execute the future directly (eg core.run(Track::get(...))
). This will block the thread until you get a response.handle.spawn
/ Session::spawn
. The result will be lost, but you can use .map
or .and_then
(or some other combinators) to add some processing to the future. Just like packet processing, futures which are "spawned" only make progress if you run core.run
..wait
on it. This only works if another thread is running the core.Because the OGG decoder expects reads from files to be synchronous, the AudioFile::read
function uses .wait()
to block on data. The entire player is therefore run on a separate ~process~ thread, and uses .wait()
in a couple of other places. This is fine because the main thread is running the core.
When doing the second or third option you may not have anything useful to give to core.run
. In this case you can make an empty future which never completes and pass it. That way the core will run forever.
See for example python-librespot
. It creates a thread just for the event loop but runs an empty future on it. When some python code makes a request which returns a future, that future is combined using then
such that the result is communicated back to Python when the result is available, and is spawned to run in the background
EDIT: typos
Hi,
I'm playing a little bit with rust at the moment. Here I'm trying to load the name of a track, given some track id:
However, when I run this code,
Track::get()
blocks infinitely. Is there some kind of event loop I have to run before I can use Track::get?