Skip to content

Commit 35bf42f

Browse files
committed
feat: add media_control plugin
1 parent 08abd84 commit 35bf42f

File tree

7 files changed

+183
-2
lines changed

7 files changed

+183
-2
lines changed

Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ file-rotate = "0.7.4"
1919
hyper = { version = "0.14.27", features = ["http1", "server"] }
2020
log = "0.4.17"
2121
reqwest = { version = "0.11.17", features = ["default", "json"], optional = true }
22+
enigo = { version = "0.2.0-rc2", optional = true }
2223
searchlight = "0.3.1"
2324
serde = { version = "1.0.163", features = ["derive"] }
2425
serde_json = "1.0.105"
@@ -42,6 +43,7 @@ tray-item = { version = "0.9.0", features = ["ksni"] }
4243
tray-item = { version = "0.9.0" }
4344

4445
[features]
45-
default = ["pishock", "watch"]
46+
default = ["media-control", "pishock", "watch"]
47+
media-control = ["dep:enigo"]
4648
pishock = ["dep:reqwest"]
4749
watch = []

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Please note that on Windows oyu will not see any debug output on the console wit
5151

5252
Log files can be found on Linux under `~/.local/share/vrc-osc-manager\logs`. On Windows they should be located under
5353
`C:\Users\username\Application Data\vrc-osc-manager\logs`. The latest log file is always called `log`, while older
54-
ones are suffixed with a timestamp. Log files are rotated every hour and a maximum of 12 log files is every kept.
54+
ones are suffixed with a timestamp.
5555

5656
## Dark mode
5757

@@ -64,6 +64,16 @@ Both Linux and Windows are supported, though Linux is the primarily tested platf
6464

6565
## Plugins
6666

67+
### Media Control
68+
69+
This plugin allows you to control your local media player from VRChat without relying on overlays. All you need is to
70+
set up is a menu within your avatar with buttons controlling the following booleans:
71+
72+
- `MC_PrevTrack`
73+
- `MC_NextTrack`
74+
- `MC_PlayPause`
75+
- `MC_Stop`
76+
6777
### Watch
6878

6979
This plugin drives the [OSC Watch VRChat accessory](https://booth.pm/en/items/3687002) component. It implements the

src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ async fn run_plugins(
9090
receiver_tx: broadcast::Sender<OscMessage>,
9191
sender_tx: mpsc::Sender<OscMessage>,
9292
) -> Result<()> {
93+
#[cfg(feature = "media-control")]
94+
{
95+
let receiver_rx = receiver_tx.subscribe();
96+
subsys.start("PluginMediaControl", |subsys| {
97+
plugins::media_control::MediaControl::new(receiver_rx).run(subsys)
98+
});
99+
}
100+
93101
#[cfg(feature = "watch")]
94102
{
95103
let sender_tx = sender_tx.clone();

src/osc.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ impl Query {
122122
"".to_string(),
123123
);
124124

125+
#[cfg(feature = "media-control")]
126+
plugins::media_control::register_osc_query_parameters(&mut osc_query_service);
127+
125128
#[cfg(feature = "pishock")]
126129
plugins::pishock::register_osc_query_parameters(&mut osc_query_service);
127130

src/plugins/media_control.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use anyhow::{bail, Result};
2+
use async_osc::{prelude::OscMessageExt, OscMessage, OscType};
3+
use enigo::Direction::Click;
4+
use enigo::{Enigo, Key, Keyboard, Settings};
5+
use log::{debug, warn};
6+
use tokio::sync::broadcast;
7+
use tokio::sync::broadcast::error::RecvError;
8+
use tokio_graceful_shutdown::{errors::CancelledByShutdown, FutureExt, SubsystemHandle};
9+
10+
use crate::osc_query::{OscAccess, OscQueryService};
11+
12+
pub struct MediaControl {
13+
rx: broadcast::Receiver<OscMessage>,
14+
}
15+
16+
impl MediaControl {
17+
pub fn new(rx: broadcast::Receiver<OscMessage>) -> Self {
18+
Self { rx }
19+
}
20+
21+
async fn handle_buttons(&mut self) -> Result<()> {
22+
let mut enigo = Enigo::new(&Settings::default())?;
23+
24+
loop {
25+
match self.rx.recv().await {
26+
Ok(message) => match message.as_tuple() {
27+
("/avatar/parameters/MC_PrevTrack", &[OscType::Bool(value)]) => {
28+
if value {
29+
enigo.key(Key::MediaPrevTrack, Click)?;
30+
}
31+
}
32+
("/avatar/parameters/MC_NextTrack", &[OscType::Bool(value)]) => {
33+
if value {
34+
enigo.key(Key::MediaNextTrack, Click)?;
35+
}
36+
}
37+
("/avatar/parameters/MC_PlayPause", &[OscType::Bool(value)]) => {
38+
if value {
39+
enigo.key(Key::MediaPlayPause, Click)?;
40+
}
41+
}
42+
("/avatar/parameters/MC_Stop", &[OscType::Bool(value)]) => {
43+
if value {
44+
enigo.key(Key::MediaStop, Click)?;
45+
}
46+
}
47+
_ => {}
48+
},
49+
Err(error) => match error {
50+
RecvError::Closed => {
51+
debug!("Channel closed");
52+
break;
53+
}
54+
RecvError::Lagged(skipped) => {
55+
warn!(
56+
"MediaControl lagging behind, {} messages have been dropped",
57+
skipped
58+
);
59+
}
60+
},
61+
}
62+
}
63+
64+
bail!("Message receiver died unexpectedly");
65+
}
66+
67+
pub async fn run(mut self, subsys: SubsystemHandle) -> Result<()> {
68+
match (self.handle_buttons().cancel_on_shutdown(&subsys)).await {
69+
Ok(Ok(())) => subsys.request_shutdown(),
70+
Ok(Err(error)) => return Err(error),
71+
Err(CancelledByShutdown) => {}
72+
}
73+
74+
Ok(())
75+
}
76+
}
77+
78+
pub fn register_osc_query_parameters(service: &mut OscQueryService) {
79+
service.add_endpoint(
80+
"/avatar/parameters/MC_PrevTrack".to_string(),
81+
"b".to_string(),
82+
OscAccess::Read,
83+
"Media Control: Previous Track".to_string(),
84+
);
85+
service.add_endpoint(
86+
"/avatar/parameters/MC_NextTrack".to_string(),
87+
"b".to_string(),
88+
OscAccess::Read,
89+
"Media Control: Next Track".to_string(),
90+
);
91+
service.add_endpoint(
92+
"/avatar/parameters/MC_PlayPause".to_string(),
93+
"b".to_string(),
94+
OscAccess::Read,
95+
"Media Control: Play/Pause".to_string(),
96+
);
97+
service.add_endpoint(
98+
"/avatar/parameters/MC_Stop".to_string(),
99+
"b".to_string(),
100+
OscAccess::Read,
101+
"Media Control: Stop".to_string(),
102+
);
103+
}

src/plugins/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "media-control")]
2+
pub mod media_control;
13
#[cfg(feature = "pishock")]
24
pub mod pishock;
35
#[cfg(feature = "watch")]

0 commit comments

Comments
 (0)