+214
-1
Cargo.lock
+214
-1
Cargo.lock
···
567
567
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
568
568
569
569
[[package]]
570
+
name = "block"
571
+
version = "0.1.6"
572
+
source = "registry+https://github.com/rust-lang/crates.io-index"
573
+
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
574
+
575
+
[[package]]
570
576
name = "block-buffer"
571
577
version = "0.9.0"
572
578
source = "registry+https://github.com/rust-lang/crates.io-index"
···
725
731
checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
726
732
727
733
[[package]]
734
+
name = "cocoa"
735
+
version = "0.24.1"
736
+
source = "registry+https://github.com/rust-lang/crates.io-index"
737
+
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
738
+
dependencies = [
739
+
"bitflags 1.3.2",
740
+
"block",
741
+
"cocoa-foundation",
742
+
"core-foundation",
743
+
"core-graphics",
744
+
"foreign-types",
745
+
"libc",
746
+
"objc",
747
+
]
748
+
749
+
[[package]]
750
+
name = "cocoa-foundation"
751
+
version = "0.1.2"
752
+
source = "registry+https://github.com/rust-lang/crates.io-index"
753
+
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
754
+
dependencies = [
755
+
"bitflags 1.3.2",
756
+
"block",
757
+
"core-foundation",
758
+
"core-graphics-types",
759
+
"libc",
760
+
"objc",
761
+
]
762
+
763
+
[[package]]
728
764
name = "combine"
729
765
version = "4.6.7"
730
766
source = "registry+https://github.com/rust-lang/crates.io-index"
···
813
849
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
814
850
815
851
[[package]]
852
+
name = "core-graphics"
853
+
version = "0.22.3"
854
+
source = "registry+https://github.com/rust-lang/crates.io-index"
855
+
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
856
+
dependencies = [
857
+
"bitflags 1.3.2",
858
+
"core-foundation",
859
+
"core-graphics-types",
860
+
"foreign-types",
861
+
"libc",
862
+
]
863
+
864
+
[[package]]
865
+
name = "core-graphics-types"
866
+
version = "0.1.3"
867
+
source = "registry+https://github.com/rust-lang/crates.io-index"
868
+
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
869
+
dependencies = [
870
+
"bitflags 1.3.2",
871
+
"core-foundation",
872
+
"libc",
873
+
]
874
+
875
+
[[package]]
816
876
name = "coreaudio-rs"
817
877
version = "0.10.0"
818
878
source = "registry+https://github.com/rust-lang/crates.io-index"
···
852
912
"stdweb",
853
913
"thiserror 1.0.69",
854
914
"web-sys",
855
-
"windows",
915
+
"windows 0.37.0",
856
916
]
857
917
858
918
[[package]]
···
949
1009
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
950
1010
951
1011
[[package]]
1012
+
name = "dbus"
1013
+
version = "0.9.7"
1014
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1015
+
checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
1016
+
dependencies = [
1017
+
"libc",
1018
+
"libdbus-sys",
1019
+
"winapi",
1020
+
]
1021
+
1022
+
[[package]]
1023
+
name = "dbus-crossroads"
1024
+
version = "0.5.2"
1025
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1026
+
checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0"
1027
+
dependencies = [
1028
+
"dbus",
1029
+
]
1030
+
1031
+
[[package]]
952
1032
name = "deadpool"
953
1033
version = "0.7.0"
954
1034
source = "registry+https://github.com/rust-lang/crates.io-index"
···
989
1069
version = "1.0.4"
990
1070
source = "registry+https://github.com/rust-lang/crates.io-index"
991
1071
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
1072
+
1073
+
[[package]]
1074
+
name = "dispatch"
1075
+
version = "0.2.0"
1076
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1077
+
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
992
1078
993
1079
[[package]]
994
1080
name = "displaydoc"
···
1109
1195
version = "0.1.4"
1110
1196
source = "registry+https://github.com/rust-lang/crates.io-index"
1111
1197
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
1198
+
1199
+
[[package]]
1200
+
name = "foreign-types"
1201
+
version = "0.3.2"
1202
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1203
+
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
1204
+
dependencies = [
1205
+
"foreign-types-shared",
1206
+
]
1207
+
1208
+
[[package]]
1209
+
name = "foreign-types-shared"
1210
+
version = "0.1.1"
1211
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1212
+
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
1112
1213
1113
1214
[[package]]
1114
1215
name = "form_urlencoded"
···
2016
2117
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
2017
2118
2018
2119
[[package]]
2120
+
name = "libdbus-sys"
2121
+
version = "0.2.5"
2122
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2123
+
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
2124
+
dependencies = [
2125
+
"pkg-config",
2126
+
]
2127
+
2128
+
[[package]]
2019
2129
name = "libloading"
2020
2130
version = "0.8.6"
2021
2131
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2120
2230
version = "0.4.3"
2121
2231
source = "registry+https://github.com/rust-lang/crates.io-index"
2122
2232
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
2233
+
dependencies = [
2234
+
"libc",
2235
+
]
2236
+
2237
+
[[package]]
2238
+
name = "malloc_buf"
2239
+
version = "0.0.6"
2240
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2241
+
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
2123
2242
dependencies = [
2124
2243
"libc",
2125
2244
]
···
2416
2535
version = "0.1.0"
2417
2536
source = "registry+https://github.com/rust-lang/crates.io-index"
2418
2537
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
2538
+
2539
+
[[package]]
2540
+
name = "objc"
2541
+
version = "0.2.7"
2542
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2543
+
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
2544
+
dependencies = [
2545
+
"malloc_buf",
2546
+
]
2419
2547
2420
2548
[[package]]
2421
2549
name = "object"
···
3385
3513
]
3386
3514
3387
3515
[[package]]
3516
+
name = "souvlaki"
3517
+
version = "0.8.3"
3518
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3519
+
checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8"
3520
+
dependencies = [
3521
+
"base64 0.22.1",
3522
+
"block",
3523
+
"cocoa",
3524
+
"core-graphics",
3525
+
"dbus",
3526
+
"dbus-crossroads",
3527
+
"dispatch",
3528
+
"objc",
3529
+
"thiserror 1.0.69",
3530
+
"windows 0.44.0",
3531
+
]
3532
+
3533
+
[[package]]
3388
3534
name = "spin"
3389
3535
version = "0.5.2"
3390
3536
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4235
4381
"rodio",
4236
4382
"rustfft",
4237
4383
"serde",
4384
+
"souvlaki",
4238
4385
"surf",
4239
4386
"symphonia",
4240
4387
"termion",
···
4567
4714
]
4568
4715
4569
4716
[[package]]
4717
+
name = "windows"
4718
+
version = "0.44.0"
4719
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4720
+
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
4721
+
dependencies = [
4722
+
"windows-targets 0.42.2",
4723
+
]
4724
+
4725
+
[[package]]
4570
4726
name = "windows-core"
4571
4727
version = "0.52.0"
4572
4728
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4604
4760
4605
4761
[[package]]
4606
4762
name = "windows-targets"
4763
+
version = "0.42.2"
4764
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4765
+
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
4766
+
dependencies = [
4767
+
"windows_aarch64_gnullvm 0.42.2",
4768
+
"windows_aarch64_msvc 0.42.2",
4769
+
"windows_i686_gnu 0.42.2",
4770
+
"windows_i686_msvc 0.42.2",
4771
+
"windows_x86_64_gnu 0.42.2",
4772
+
"windows_x86_64_gnullvm 0.42.2",
4773
+
"windows_x86_64_msvc 0.42.2",
4774
+
]
4775
+
4776
+
[[package]]
4777
+
name = "windows-targets"
4607
4778
version = "0.48.5"
4608
4779
source = "registry+https://github.com/rust-lang/crates.io-index"
4609
4780
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
···
4635
4806
4636
4807
[[package]]
4637
4808
name = "windows_aarch64_gnullvm"
4809
+
version = "0.42.2"
4810
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4811
+
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
4812
+
4813
+
[[package]]
4814
+
name = "windows_aarch64_gnullvm"
4638
4815
version = "0.48.5"
4639
4816
source = "registry+https://github.com/rust-lang/crates.io-index"
4640
4817
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
···
4653
4830
4654
4831
[[package]]
4655
4832
name = "windows_aarch64_msvc"
4833
+
version = "0.42.2"
4834
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4835
+
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
4836
+
4837
+
[[package]]
4838
+
name = "windows_aarch64_msvc"
4656
4839
version = "0.48.5"
4657
4840
source = "registry+https://github.com/rust-lang/crates.io-index"
4658
4841
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
···
4671
4854
4672
4855
[[package]]
4673
4856
name = "windows_i686_gnu"
4857
+
version = "0.42.2"
4858
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4859
+
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
4860
+
4861
+
[[package]]
4862
+
name = "windows_i686_gnu"
4674
4863
version = "0.48.5"
4675
4864
source = "registry+https://github.com/rust-lang/crates.io-index"
4676
4865
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
···
4695
4884
4696
4885
[[package]]
4697
4886
name = "windows_i686_msvc"
4887
+
version = "0.42.2"
4888
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4889
+
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
4890
+
4891
+
[[package]]
4892
+
name = "windows_i686_msvc"
4698
4893
version = "0.48.5"
4699
4894
source = "registry+https://github.com/rust-lang/crates.io-index"
4700
4895
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
···
4713
4908
4714
4909
[[package]]
4715
4910
name = "windows_x86_64_gnu"
4911
+
version = "0.42.2"
4912
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4913
+
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
4914
+
4915
+
[[package]]
4916
+
name = "windows_x86_64_gnu"
4716
4917
version = "0.48.5"
4717
4918
source = "registry+https://github.com/rust-lang/crates.io-index"
4718
4919
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
···
4722
4923
version = "0.52.6"
4723
4924
source = "registry+https://github.com/rust-lang/crates.io-index"
4724
4925
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
4926
+
4927
+
[[package]]
4928
+
name = "windows_x86_64_gnullvm"
4929
+
version = "0.42.2"
4930
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4931
+
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
4725
4932
4726
4933
[[package]]
4727
4934
name = "windows_x86_64_gnullvm"
···
4740
4947
version = "0.37.0"
4741
4948
source = "registry+https://github.com/rust-lang/crates.io-index"
4742
4949
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
4950
+
4951
+
[[package]]
4952
+
name = "windows_x86_64_msvc"
4953
+
version = "0.42.2"
4954
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4955
+
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
4743
4956
4744
4957
[[package]]
4745
4958
name = "windows_x86_64_msvc"
+1
Cargo.toml
+1
Cargo.toml
+300
-64
src/app.rs
+300
-64
src/app.rs
···
3
3
prelude::*,
4
4
widgets::{block::*, *},
5
5
};
6
+
use souvlaki::MediaControlEvent;
6
7
use std::{
7
8
io,
8
9
ops::Range,
···
11
12
time::{Duration, Instant},
12
13
};
13
14
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
15
+
use tunein_cli::os_media_controls::{self, OsMediaControls};
14
16
15
17
use crate::{
16
18
extract::get_currently_playing,
···
76
78
self.is_muted = !self.is_muted;
77
79
}
78
80
81
+
/// Set the volume to the given volume ratio.
82
+
///
83
+
/// `1.0` is 100% volume.
84
+
pub const fn set_volume_ratio(&mut self, volume: f32) {
85
+
self.raw_volume_percent = volume * 100.0;
86
+
self.raw_volume_percent = self.raw_volume_percent.max(0.0);
87
+
}
88
+
79
89
/// Change the volume by the given step percent.
80
90
///
81
91
/// To increase the volume, use a positive step. To decrease the
···
135
145
spectroscope: Spectroscope,
136
146
mode: CurrentDisplayMode,
137
147
frame_rx: Receiver<minimp3::Frame>,
148
+
/// [`OsMediaControls`].
149
+
os_media_controls: Option<OsMediaControls>,
150
+
/// Poll for events every specified [`Duration`].
151
+
///
152
+
/// Allows user to decide the trade off between computational
153
+
/// resource comsumption, animation smoothness and how responsive
154
+
/// the application. Smaller durations lead to more resource
155
+
/// consumption but smoother animations and better responsiveness.
156
+
poll_events_every: Duration,
157
+
/// [`Self::poll_events_every`] but when player is paused.
158
+
///
159
+
/// This should generally be larger than
160
+
/// [`Self::poll_events_every`].
161
+
poll_events_every_while_paused: Duration,
138
162
}
139
163
140
164
impl App {
···
143
167
source: &crate::cfg::SourceOptions,
144
168
frame_rx: Receiver<minimp3::Frame>,
145
169
mode: CurrentDisplayMode,
170
+
os_media_controls: Option<OsMediaControls>,
171
+
poll_events_every: Duration,
172
+
poll_events_every_while_paused: Duration,
146
173
) -> Self {
147
174
let graph = GraphConfig {
148
175
axis_color: Color::DarkGray,
···
175
202
mode,
176
203
channels: source.channels as u8,
177
204
frame_rx,
205
+
os_media_controls,
206
+
poll_events_every,
207
+
poll_events_every_while_paused,
178
208
}
179
209
}
180
210
}
···
307
337
id: &str,
308
338
) {
309
339
let new_state = cmd_rx.recv().await.unwrap();
340
+
341
+
// report metadata to OS
342
+
send_os_media_controls_command(
343
+
self.os_media_controls.as_mut(),
344
+
os_media_controls::Command::SetMetadata(souvlaki::MediaMetadata {
345
+
title: (!new_state.now_playing.is_empty()).then_some(&new_state.now_playing),
346
+
album: (!new_state.name.is_empty()).then_some(&new_state.name),
347
+
artist: None,
348
+
cover_url: None,
349
+
duration: None,
350
+
}),
351
+
);
352
+
// report started playing to OS
353
+
send_os_media_controls_command(
354
+
self.os_media_controls.as_mut(),
355
+
os_media_controls::Command::Play,
356
+
);
357
+
// report volume to OS
358
+
send_os_media_controls_command(
359
+
self.os_media_controls.as_mut(),
360
+
os_media_controls::Command::SetVolume(new_state.volume.volume_ratio() as f64),
361
+
);
362
+
310
363
let new_state = Arc::new(Mutex::new(new_state));
311
364
312
365
let id = id.to_string();
···
328
381
let mut last_poll = Instant::now();
329
382
330
383
loop {
331
-
let channels = (!self.graph.pause)
332
-
.then(|| self.frame_rx.recv().unwrap())
333
-
.map(|audio_frame| {
334
-
stream_to_matrix(audio_frame.data.iter().cloned(), audio_frame.channels, 1.)
335
-
});
384
+
let channels = if self.graph.pause {
385
+
None
386
+
} else {
387
+
let Ok(audio_frame) = self.frame_rx.recv() else {
388
+
// other thread has closed so application has
389
+
// closed
390
+
return;
391
+
};
392
+
Some(stream_to_matrix(
393
+
audio_frame.data.iter().cloned(),
394
+
audio_frame.channels,
395
+
1.,
396
+
))
397
+
};
336
398
337
399
fps += 1;
338
400
···
387
449
.unwrap();
388
450
}
389
451
390
-
while event::poll(Duration::from_millis(0)).unwrap() {
452
+
while let Some(event) = self
453
+
.os_media_controls
454
+
.as_mut()
455
+
.and_then(|os_media_controls| os_media_controls.try_recv_os_event())
456
+
{
457
+
if self.process_os_media_control_event(event, &new_state, &mut sink_cmd_tx) {
458
+
return;
459
+
}
460
+
}
461
+
462
+
let timeout_duration = if self.graph.pause {
463
+
self.poll_events_every_while_paused
464
+
} else {
465
+
self.poll_events_every
466
+
};
467
+
468
+
while event::poll(timeout_duration).unwrap() {
391
469
// process all enqueued events
392
470
let event = event::read().unwrap();
393
471
···
434
512
) -> Result<bool, io::Error> {
435
513
let mut quit = false;
436
514
437
-
let play = |graph: &mut GraphConfig| {
438
-
graph.pause = false;
439
-
sink_cmd_tx
440
-
.send(SinkCommand::Play)
441
-
.expect("receiver never dropped");
442
-
};
443
-
444
-
let pause = |graph: &mut GraphConfig| {
445
-
graph.pause = true;
446
-
sink_cmd_tx
447
-
.send(SinkCommand::Pause)
448
-
.expect("receiver never dropped");
449
-
};
450
-
451
-
let toggle_play_pause = |graph: &mut GraphConfig| {
452
-
graph.pause = !graph.pause;
453
-
let sink_cmd = if graph.pause {
454
-
SinkCommand::Pause
455
-
} else {
456
-
SinkCommand::Play
457
-
};
458
-
sink_cmd_tx.send(sink_cmd).expect("receiver never dropped");
459
-
};
460
-
461
-
let lower_volume = || {
462
-
let mut state = state.lock().unwrap();
463
-
state.volume.change_volume(-1.0);
464
-
sink_cmd_tx
465
-
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
466
-
.expect("receiver never dropped");
467
-
};
468
-
469
-
let raise_volume = || {
470
-
let mut state = state.lock().unwrap();
471
-
state.volume.change_volume(1.0);
472
-
sink_cmd_tx
473
-
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
474
-
.expect("receiver never dropped");
475
-
};
476
-
477
-
let mute_volume = || {
478
-
let mut state = state.lock().unwrap();
479
-
state.volume.toggle_mute();
480
-
sink_cmd_tx
481
-
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
482
-
.expect("receiver never dropped");
483
-
};
484
-
485
515
if let Event::Key(key) = event {
486
516
if let KeyModifiers::CONTROL = key.modifiers {
487
517
match key.code {
···
500
530
KeyCode::Up => {
501
531
// inverted to act as zoom
502
532
update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0);
503
-
raise_volume();
533
+
raise_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx);
504
534
}
505
535
KeyCode::Down => {
506
536
// inverted to act as zoom
507
537
update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0);
508
-
lower_volume();
538
+
lower_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx);
509
539
}
510
540
KeyCode::Right => update_value_i(
511
541
&mut self.graph.samples,
···
522
552
0..self.graph.width * 2,
523
553
),
524
554
KeyCode::Char('q') => quit = true,
525
-
KeyCode::Char(' ') => toggle_play_pause(&mut self.graph),
555
+
KeyCode::Char(' ') => toggle_play_pause(
556
+
&mut self.graph,
557
+
self.os_media_controls.as_mut(),
558
+
sink_cmd_tx,
559
+
),
526
560
KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter,
527
561
KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui,
528
562
KeyCode::Char('r') => self.graph.references = !self.graph.references,
529
-
KeyCode::Char('m') => mute_volume(),
563
+
KeyCode::Char('m') => {
564
+
mute_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx)
565
+
}
530
566
KeyCode::Esc => {
531
567
self.graph.samples = self.graph.width;
532
568
self.graph.scale = 1.;
···
552
588
}
553
589
}
554
590
KeyCode::Media(media_key_code) => match media_key_code {
555
-
MediaKeyCode::Play => play(&mut self.graph),
556
-
MediaKeyCode::Pause => pause(&mut self.graph),
557
-
MediaKeyCode::PlayPause => toggle_play_pause(&mut self.graph),
591
+
MediaKeyCode::Play => play(
592
+
&mut self.graph,
593
+
self.os_media_controls.as_mut(),
594
+
sink_cmd_tx,
595
+
),
596
+
MediaKeyCode::Pause => pause(
597
+
&mut self.graph,
598
+
self.os_media_controls.as_mut(),
599
+
sink_cmd_tx,
600
+
),
601
+
MediaKeyCode::PlayPause => toggle_play_pause(
602
+
&mut self.graph,
603
+
self.os_media_controls.as_mut(),
604
+
sink_cmd_tx,
605
+
),
558
606
MediaKeyCode::Stop => {
559
607
quit = true;
560
608
}
561
-
MediaKeyCode::LowerVolume => lower_volume(),
562
-
MediaKeyCode::RaiseVolume => raise_volume(),
563
-
MediaKeyCode::MuteVolume => mute_volume(),
609
+
MediaKeyCode::LowerVolume => {
610
+
lower_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx)
611
+
}
612
+
MediaKeyCode::RaiseVolume => {
613
+
raise_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx)
614
+
}
615
+
MediaKeyCode::MuteVolume => {
616
+
mute_volume(&state, self.os_media_controls.as_mut(), sink_cmd_tx)
617
+
}
564
618
MediaKeyCode::TrackNext
565
619
| MediaKeyCode::TrackPrevious
566
620
| MediaKeyCode::Reverse
···
574
628
575
629
Ok(quit)
576
630
}
631
+
632
+
/// Process OS media control event.
633
+
///
634
+
/// Returns [`true`] if application should quit.
635
+
fn process_os_media_control_event(
636
+
&mut self,
637
+
event: MediaControlEvent,
638
+
state: &Mutex<State>,
639
+
sink_cmd_tx: &mut UnboundedSender<SinkCommand>,
640
+
) -> bool {
641
+
let mut quit = false;
642
+
643
+
match event {
644
+
MediaControlEvent::Play => {
645
+
play(
646
+
&mut self.graph,
647
+
self.os_media_controls.as_mut(),
648
+
sink_cmd_tx,
649
+
);
650
+
}
651
+
MediaControlEvent::Pause => {
652
+
pause(
653
+
&mut self.graph,
654
+
self.os_media_controls.as_mut(),
655
+
sink_cmd_tx,
656
+
);
657
+
}
658
+
MediaControlEvent::Toggle => {
659
+
toggle_play_pause(
660
+
&mut self.graph,
661
+
self.os_media_controls.as_mut(),
662
+
sink_cmd_tx,
663
+
);
664
+
}
665
+
MediaControlEvent::Stop | MediaControlEvent::Quit => {
666
+
quit = true;
667
+
}
668
+
MediaControlEvent::SetVolume(volume) => {
669
+
set_volume_ratio(
670
+
volume as f32,
671
+
state,
672
+
self.os_media_controls.as_mut(),
673
+
sink_cmd_tx,
674
+
);
675
+
}
676
+
MediaControlEvent::Next
677
+
| MediaControlEvent::Previous
678
+
| MediaControlEvent::Seek(_)
679
+
| MediaControlEvent::SeekBy(_, _)
680
+
| MediaControlEvent::SetPosition(_)
681
+
| MediaControlEvent::OpenUri(_)
682
+
| MediaControlEvent::Raise => {}
683
+
}
684
+
685
+
quit
686
+
}
577
687
}
578
688
579
689
pub fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: Range<f64>) {
···
635
745
)
636
746
.style(Style::default().fg(cfg.labels_color))
637
747
}
748
+
749
+
/// Play music.
750
+
fn play(
751
+
graph: &mut GraphConfig,
752
+
os_media_controls: Option<&mut OsMediaControls>,
753
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
754
+
) {
755
+
graph.pause = false;
756
+
send_os_media_controls_command(os_media_controls, os_media_controls::Command::Play);
757
+
sink_cmd_tx
758
+
.send(SinkCommand::Play)
759
+
.expect("receiver never dropped");
760
+
}
761
+
762
+
/// Pause music.
763
+
fn pause(
764
+
graph: &mut GraphConfig,
765
+
os_media_controls: Option<&mut OsMediaControls>,
766
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
767
+
) {
768
+
graph.pause = true;
769
+
send_os_media_controls_command(os_media_controls, os_media_controls::Command::Pause);
770
+
sink_cmd_tx
771
+
.send(SinkCommand::Pause)
772
+
.expect("receiver never dropped");
773
+
}
774
+
775
+
/// Toggle between play and pause.
776
+
fn toggle_play_pause(
777
+
graph: &mut GraphConfig,
778
+
os_media_controls: Option<&mut OsMediaControls>,
779
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
780
+
) {
781
+
graph.pause = !graph.pause;
782
+
let (sink_cmd, os_media_controls_command) = if graph.pause {
783
+
(SinkCommand::Pause, os_media_controls::Command::Pause)
784
+
} else {
785
+
(SinkCommand::Play, os_media_controls::Command::Play)
786
+
};
787
+
send_os_media_controls_command(os_media_controls, os_media_controls_command);
788
+
sink_cmd_tx.send(sink_cmd).expect("receiver never dropped");
789
+
}
790
+
791
+
/// Lower the volume.
792
+
fn lower_volume(
793
+
state: &Mutex<State>,
794
+
os_media_controls: Option<&mut OsMediaControls>,
795
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
796
+
) {
797
+
let mut state = state.lock().unwrap();
798
+
state.volume.change_volume(-1.0);
799
+
send_os_media_controls_command(
800
+
os_media_controls,
801
+
os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64),
802
+
);
803
+
sink_cmd_tx
804
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
805
+
.expect("receiver never dropped");
806
+
}
807
+
808
+
/// Raise the volume.
809
+
fn raise_volume(
810
+
state: &Mutex<State>,
811
+
os_media_controls: Option<&mut OsMediaControls>,
812
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
813
+
) {
814
+
let mut state = state.lock().unwrap();
815
+
state.volume.change_volume(1.0);
816
+
send_os_media_controls_command(
817
+
os_media_controls,
818
+
os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64),
819
+
);
820
+
sink_cmd_tx
821
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
822
+
.expect("receiver never dropped");
823
+
}
824
+
825
+
/// Mute the volume.
826
+
fn mute_volume(
827
+
state: &Mutex<State>,
828
+
os_media_controls: Option<&mut OsMediaControls>,
829
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
830
+
) {
831
+
let mut state = state.lock().unwrap();
832
+
state.volume.toggle_mute();
833
+
send_os_media_controls_command(
834
+
os_media_controls,
835
+
os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64),
836
+
);
837
+
sink_cmd_tx
838
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
839
+
.expect("receiver never dropped");
840
+
}
841
+
842
+
/// Set the volume to the given volume ratio.
843
+
fn set_volume_ratio(
844
+
volume_ratio: f32,
845
+
state: &Mutex<State>,
846
+
os_media_controls: Option<&mut OsMediaControls>,
847
+
sink_cmd_tx: &UnboundedSender<SinkCommand>,
848
+
) {
849
+
let mut state = state.lock().unwrap();
850
+
state.volume.set_volume_ratio(volume_ratio);
851
+
send_os_media_controls_command(
852
+
os_media_controls,
853
+
os_media_controls::Command::SetVolume(state.volume.volume_ratio() as f64),
854
+
);
855
+
sink_cmd_tx
856
+
.send(SinkCommand::SetVolume(state.volume.volume_ratio()))
857
+
.expect("receiver never dropped");
858
+
}
859
+
860
+
/// Send [`os_media_controls::Command`].
861
+
fn send_os_media_controls_command(
862
+
os_media_controls: Option<&mut OsMediaControls>,
863
+
command: os_media_controls::Command<'_>,
864
+
) {
865
+
if let Some(os_media_controls) = os_media_controls {
866
+
let _ = os_media_controls.send_to_os(command).inspect_err(|err| {
867
+
eprintln!(
868
+
"error: failed to send command to OS media controls due to `{}`",
869
+
err
870
+
);
871
+
});
872
+
}
873
+
}
+1
src/lib.rs
+1
src/lib.rs
+26
-3
src/main.rs
+26
-3
src/main.rs
···
1
+
use std::time::Duration;
2
+
1
3
use anyhow::Error;
2
4
use app::CurrentDisplayMode;
3
-
use clap::{arg, Command};
5
+
use clap::{arg, builder::ValueParser, Command};
4
6
5
7
mod app;
6
8
mod browse;
···
48
50
.about("Play a radio station")
49
51
.arg(arg!(<station> "The station to play"))
50
52
.arg(arg!(--volume "Set the initial volume (as a percent)").default_value("100"))
51
-
.arg(clap::Arg::new("display-mode").long("display-mode").help("Set the display mode to start with").default_value("Spectroscope")),
53
+
.arg(clap::Arg::new("display-mode").long("display-mode").help("Set the display mode to start with").default_value("Spectroscope"))
54
+
.arg(clap::Arg::new("enable-os-media-controls").long("enable-os-media-controls").help("Should enable OS media controls?").default_value("true").value_parser(ValueParser::bool()))
55
+
.arg(clap::Arg::new("poll-events-every").long("poll-events-every").help("Poll for events every specified milliseconds.").default_value("16"))
56
+
.arg(clap::Arg::new("poll-events-every-while-paused").long("poll-events-every-while-paused").help("Poll for events every specified milliseconds while player is paused.").default_value("100")),
52
57
)
53
58
.subcommand(
54
59
Command::new("browse")
···
99
104
.unwrap()
100
105
.parse::<CurrentDisplayMode>()
101
106
.unwrap();
102
-
play::exec(station, provider, volume, display_mode).await?;
107
+
let enable_os_media_controls = args.get_one("enable-os-media-controls").unwrap();
108
+
let poll_events_every =
109
+
Duration::from_millis(args.value_of("poll-events-every").unwrap().parse().unwrap());
110
+
let poll_events_every_while_paused = Duration::from_millis(
111
+
args.value_of("poll-events-every-while-paused")
112
+
.unwrap()
113
+
.parse()
114
+
.unwrap(),
115
+
);
116
+
play::exec(
117
+
station,
118
+
provider,
119
+
volume,
120
+
display_mode,
121
+
*enable_os_media_controls,
122
+
poll_events_every,
123
+
poll_events_every_while_paused,
124
+
)
125
+
.await?;
103
126
}
104
127
Some(("browse", args)) => {
105
128
let category = args.value_of("category");
+87
src/os_media_controls.rs
+87
src/os_media_controls.rs
···
1
+
//! Operating system level media controls.
2
+
3
+
use tokio::sync::mpsc::UnboundedReceiver;
4
+
5
+
/// Operating system level media controls.
6
+
#[derive(Debug)]
7
+
pub struct OsMediaControls {
8
+
/// Controls that interface with the OS.
9
+
controls: souvlaki::MediaControls,
10
+
/// Receiver for events produced by OS level interaction.
11
+
event_receiver: UnboundedReceiver<souvlaki::MediaControlEvent>,
12
+
}
13
+
14
+
impl OsMediaControls {
15
+
/// Create new [`OsMediaControls`].
16
+
pub fn new() -> Result<Self, souvlaki::Error> {
17
+
let mut controls = souvlaki::MediaControls::new(souvlaki::PlatformConfig {
18
+
display_name: "tunein-cli",
19
+
dbus_name: "tsirysndr.tunein-cli",
20
+
// TODO: support windows platform
21
+
hwnd: None,
22
+
})?;
23
+
24
+
let (event_sender, event_receiver) =
25
+
tokio::sync::mpsc::unbounded_channel::<souvlaki::MediaControlEvent>();
26
+
27
+
controls.attach(move |event| {
28
+
event_sender.send(event).expect("receiver always alive");
29
+
})?;
30
+
31
+
Ok(Self {
32
+
controls,
33
+
event_receiver,
34
+
})
35
+
}
36
+
37
+
/// Try to receive event produced by the operating system.
38
+
///
39
+
/// Is [`None`] if no event is produced.
40
+
pub fn try_recv_os_event(&mut self) -> Option<souvlaki::MediaControlEvent> {
41
+
self.event_receiver.try_recv().ok()
42
+
}
43
+
44
+
/// Send the given [`Command`] to the operating system.
45
+
pub fn send_to_os(&mut self, command: Command) -> Result<(), souvlaki::Error> {
46
+
match command {
47
+
Command::Play => self
48
+
.controls
49
+
.set_playback(souvlaki::MediaPlayback::Playing { progress: None }),
50
+
Command::Pause => self
51
+
.controls
52
+
.set_playback(souvlaki::MediaPlayback::Paused { progress: None }),
53
+
Command::SetVolume(volume) => {
54
+
// NOTE: is supported only for MPRIS backend,
55
+
// `souvlaki` doesn't provide a way to know this, so
56
+
// need to use `cfg` attribute like the way it exposes
57
+
// the platform
58
+
#[cfg(all(
59
+
unix,
60
+
not(any(target_os = "macos", target_os = "ios", target_os = "android"))
61
+
))]
62
+
{
63
+
self.controls.set_volume(volume)
64
+
}
65
+
#[cfg(not(all(
66
+
unix,
67
+
not(any(target_os = "macos", target_os = "ios", target_os = "android"))
68
+
)))]
69
+
{
70
+
Ok(())
71
+
}
72
+
}
73
+
Command::SetMetadata(metadata) => self.controls.set_metadata(metadata),
74
+
}
75
+
}
76
+
}
77
+
78
+
/// Commands understood by OS media controls.
79
+
#[derive(Debug, Clone)]
80
+
pub enum Command<'a> {
81
+
Play,
82
+
Pause,
83
+
/// Volume must be between `0.0..=1.0`.
84
+
SetVolume(f64),
85
+
/// Set the [`souvlaki::MediaMetadata`].
86
+
SetMetadata(souvlaki::MediaMetadata<'a>),
87
+
}
+26
-1
src/play.rs
+26
-1
src/play.rs
···
2
2
3
3
use anyhow::Error;
4
4
use hyper::header::HeaderValue;
5
+
use tunein_cli::os_media_controls::OsMediaControls;
5
6
6
7
use crate::{
7
8
app::{App, CurrentDisplayMode, State, Volume},
···
16
17
provider: &str,
17
18
volume: f32,
18
19
display_mode: CurrentDisplayMode,
20
+
enable_os_media_controls: bool,
21
+
poll_events_every: Duration,
22
+
poll_events_every_while_paused: Duration,
19
23
) -> Result<(), Error> {
20
24
let _provider = provider;
21
25
let provider: Box<dyn Provider> = match provider {
···
57
61
tune: None,
58
62
};
59
63
60
-
let mut app = App::new(&ui, &opts, frame_rx, display_mode);
64
+
let os_media_controls = if enable_os_media_controls {
65
+
OsMediaControls::new()
66
+
.inspect_err(|err| {
67
+
eprintln!(
68
+
"error: failed to initialize os media controls due to `{}`",
69
+
err
70
+
);
71
+
})
72
+
.ok()
73
+
} else {
74
+
None
75
+
};
76
+
77
+
let mut app = App::new(
78
+
&ui,
79
+
&opts,
80
+
frame_rx,
81
+
display_mode,
82
+
os_media_controls,
83
+
poll_events_every,
84
+
poll_events_every_while_paused,
85
+
);
61
86
let station_name = station.name.clone();
62
87
63
88
thread::spawn(move || {