mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-29 06:10:52 +00:00
linux-rust: add tipi
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||
use crate::bluetooth::att::ATTManager;
|
||||
use crate::media_controller::MediaController;
|
||||
use bluer::Address;
|
||||
use log::{debug, info};
|
||||
use log::{debug, info, error};
|
||||
use std::sync::Arc;
|
||||
use ksni::Handle;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -47,19 +47,29 @@ impl AirPodsDevice {
|
||||
"Failed to request notifications",
|
||||
);
|
||||
|
||||
info!("sending some packet");
|
||||
aacp_manager.send_some_packet().await.expect(
|
||||
"Failed to send some packet",
|
||||
);
|
||||
|
||||
info!("Requesting Proximity Keys: IRK and ENC_KEY");
|
||||
aacp_manager.send_proximity_keys_request(
|
||||
vec![ProximityKeyType::Irk, ProximityKeyType::EncKey],
|
||||
).await.expect(
|
||||
"Failed to request proximity keys",
|
||||
);
|
||||
let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string())));
|
||||
|
||||
let session = bluer::Session::new().await.expect("Failed to get bluer session");
|
||||
let adapter = session.default_adapter().await.expect("Failed to get default adapter");
|
||||
let local_mac = adapter.address().await.expect("Failed to get adapter address").to_string();
|
||||
|
||||
let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string(), local_mac.clone())));
|
||||
let mc_clone = media_controller.clone();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
aacp_manager.set_event_channel(tx).await;
|
||||
tray_handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx)).await;
|
||||
tray_handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())).await;
|
||||
|
||||
let aacp_manager_clone = aacp_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -70,6 +80,11 @@ impl AirPodsDevice {
|
||||
}
|
||||
});
|
||||
|
||||
let mc_listener = media_controller.lock().await;
|
||||
let aacp_manager_clone_listener = aacp_manager.clone();
|
||||
mc_listener.start_playback_listener(aacp_manager_clone_listener, command_tx.clone()).await;
|
||||
drop(mc_listener);
|
||||
|
||||
let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ListeningMode, listening_mode_tx).await;
|
||||
let tray_handle_clone = tray_handle.clone();
|
||||
@@ -103,6 +118,23 @@ impl AirPodsDevice {
|
||||
}
|
||||
});
|
||||
|
||||
let (owns_connection_tx, mut owns_connection_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::OwnsConnection, owns_connection_tx).await;
|
||||
let mc_clone_owns = media_controller.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(value) = owns_connection_rx.recv().await {
|
||||
let owns = value.get(0).copied().unwrap_or(0) != 0;
|
||||
if !owns {
|
||||
info!("Lost ownership, pausing media and disconnecting audio");
|
||||
let controller = mc_clone_owns.lock().await;
|
||||
controller.pause_all_media().await;
|
||||
controller.deactivate_a2dp_profile().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let aacp_manager_clone_events = aacp_manager.clone();
|
||||
let local_mac_events = local_mac.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
@@ -143,6 +175,37 @@ impl AirPodsDevice {
|
||||
let controller = mc_clone.lock().await;
|
||||
controller.handle_conversational_awareness(status).await;
|
||||
}
|
||||
AACPEvent::ConnectedDevices(old_devices, new_devices) => {
|
||||
let local_mac = local_mac_events.clone();
|
||||
let new_devices_filtered = new_devices.iter().filter(|new_device| {
|
||||
let not_in_old = old_devices.iter().all(|old_device| old_device.mac != new_device.mac);
|
||||
let not_local = new_device.mac != local_mac;
|
||||
not_in_old && not_local
|
||||
});
|
||||
|
||||
for device in new_devices_filtered {
|
||||
info!("New connected device: {}, info1: {}, info2: {}", device.mac, device.info1, device.info2);
|
||||
info!("Sending new Tipi packet for device {}, and sending media info to the device", device.mac);
|
||||
let aacp_manager_clone = aacp_manager_clone_events.clone();
|
||||
let local_mac_clone = local_mac.clone();
|
||||
let device_mac_clone = device.mac.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = aacp_manager_clone.send_media_information_new_device(&local_mac_clone, &device_mac_clone).await {
|
||||
error!("Failed to send media info new device: {}", e);
|
||||
}
|
||||
if let Err(e) = aacp_manager_clone.send_add_tipi_device(&local_mac_clone, &device_mac_clone).await {
|
||||
error!("Failed to send add tipi device: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
AACPEvent::OwnershipToFalseRequest => {
|
||||
info!("Received ownership to false request. Setting ownership to false and pausing media.");
|
||||
let _ = command_tx.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
|
||||
let controller = mc_clone.lock().await;
|
||||
controller.pause_all_media().await;
|
||||
controller.deactivate_a2dp_profile().await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ pub mod opcodes {
|
||||
pub const SMART_ROUTING: u8 = 0x10;
|
||||
pub const SMART_ROUTING_RESP: u8 = 0x11;
|
||||
pub const SEND_CONNECTED_MAC: u8 = 0x14;
|
||||
pub const HEADTRACKING: u8 = 0x17;
|
||||
pub const TIPI_3: u8 = 0x0C;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -217,7 +219,8 @@ pub enum AACPEvent {
|
||||
ConversationalAwareness(u8),
|
||||
ProximityKeys(Vec<(u8, Vec<u8>)>),
|
||||
AudioSource(AudioSource),
|
||||
ConnectedDevices(Vec<ConnectedDevice>),
|
||||
ConnectedDevices(Vec<ConnectedDevice>, Vec<ConnectedDevice>),
|
||||
OwnershipToFalseRequest,
|
||||
}
|
||||
|
||||
struct AACPManagerState {
|
||||
@@ -225,6 +228,7 @@ struct AACPManagerState {
|
||||
control_command_status_list: Vec<ControlCommandStatus>,
|
||||
control_command_subscribers: HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
|
||||
owns: bool,
|
||||
old_connected_devices: Vec<ConnectedDevice>,
|
||||
connected_devices: Vec<ConnectedDevice>,
|
||||
audio_source: Option<AudioSource>,
|
||||
battery_info: Vec<BatteryInfo>,
|
||||
@@ -241,6 +245,7 @@ impl AACPManagerState {
|
||||
control_command_status_list: Vec::new(),
|
||||
control_command_subscribers: HashMap::new(),
|
||||
owns: false,
|
||||
old_connected_devices: Vec::new(),
|
||||
connected_devices: Vec::new(),
|
||||
audio_source: None,
|
||||
battery_info: Vec::new(),
|
||||
@@ -355,6 +360,10 @@ impl AACPManager {
|
||||
state.event_tx = Some(tx);
|
||||
}
|
||||
|
||||
pub async fn get_connected_devices(&self) -> Vec<ConnectedDevice> {
|
||||
self.state.lock().await.connected_devices.clone()
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_control_command(&self, identifier: ControlCommandIdentifiers, tx: mpsc::UnboundedSender<Vec<u8>>) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.control_command_subscribers.entry(identifier).or_default().push(tx);
|
||||
@@ -578,14 +587,22 @@ impl AACPManager {
|
||||
devices.push(ConnectedDevice { mac, info1, info2, r#type: None });
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
state.old_connected_devices = state.connected_devices.clone();
|
||||
state.connected_devices = devices.clone();
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::ConnectedDevices(devices));
|
||||
let _ = tx.send(AACPEvent::ConnectedDevices(state.old_connected_devices.clone(), devices));
|
||||
}
|
||||
info!("Received Connected Devices: {:?}", state.connected_devices);
|
||||
}
|
||||
opcodes::SMART_ROUTING_RESP => {
|
||||
info!("Received Smart Routing Response: {:?}", &payload[1..]);
|
||||
let packet_string = String::from_utf8_lossy(&payload[2..]);
|
||||
info!("Received Smart Routing Response: {}", packet_string);
|
||||
if packet_string.contains("SetOwnershipToFalse") {
|
||||
info!("Received OwnershipToFalse request");
|
||||
if let Some(ref tx) = self.state.lock().await.event_tx {
|
||||
let _ = tx.send(AACPEvent::OwnershipToFalseRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
opcodes::EQ_DATA => {
|
||||
debug!("Received EQ Data");
|
||||
@@ -648,6 +665,195 @@ impl AACPManager {
|
||||
let packet = [opcode.as_slice(), data.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_media_information_new_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> {
|
||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||
let mut buffer = Vec::with_capacity(112);
|
||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||
|
||||
buffer.extend_from_slice(&[0x68, 0x00]);
|
||||
buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]);
|
||||
buffer.extend_from_slice(b"playingApp");
|
||||
buffer.push(0x42);
|
||||
buffer.extend_from_slice(b"NA");;
|
||||
buffer.push(0x52);
|
||||
buffer.extend_from_slice(b"hostStreamingState");
|
||||
buffer.push(0x42);
|
||||
buffer.extend_from_slice(b"NO");;
|
||||
buffer.push(0x49);
|
||||
buffer.extend_from_slice(b"btAddress");
|
||||
buffer.push(0x51);
|
||||
buffer.extend_from_slice(self_mac_address.as_bytes());
|
||||
buffer.push(0x46);
|
||||
buffer.extend_from_slice(b"btName");
|
||||
buffer.push(0x43);
|
||||
buffer.extend_from_slice(b"Mac");;
|
||||
buffer.push(0x58);
|
||||
buffer.extend_from_slice(b"otherDevice");
|
||||
buffer.extend_from_slice(b"AudioCategory");
|
||||
buffer.extend_from_slice(&[0x30, 0x64]);
|
||||
|
||||
let packet = [opcode.as_slice(), buffer.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> {
|
||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||
let mut buffer = Vec::with_capacity(106);
|
||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||
buffer.extend_from_slice(&[0x62, 0x00]);
|
||||
buffer.extend_from_slice(&[0x01, 0xE5]);
|
||||
buffer.push(0x4A);
|
||||
buffer.extend_from_slice(b"localscore");
|
||||
buffer.extend_from_slice(&[0x30, 0x64]);
|
||||
buffer.push(0x46);
|
||||
buffer.extend_from_slice(b"reason");
|
||||
buffer.push(0x48);
|
||||
buffer.extend_from_slice(b"Hijackv2");
|
||||
buffer.push(0x51);
|
||||
buffer.extend_from_slice(b"audioRoutingScore");
|
||||
buffer.extend_from_slice(&[0x31, 0x2D, 0x01, 0x5F]);
|
||||
buffer.extend_from_slice(b"audioRoutingSetOwnershipToFalse");
|
||||
buffer.push(0x01);
|
||||
buffer.push(0x4B);
|
||||
buffer.extend_from_slice(b"remotescore");
|
||||
buffer.push(0xA5);
|
||||
|
||||
while buffer.len() < 106 {
|
||||
buffer.push(0x00);
|
||||
}
|
||||
|
||||
let packet = [opcode.as_slice(), buffer.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_media_information(&self, self_mac_address: &str, target_mac_address: &str, streaming_state: bool) -> Result<()> {
|
||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||
let mut buffer = Vec::with_capacity(138);
|
||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||
buffer.extend_from_slice(&[0x82, 0x00]);
|
||||
buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]);
|
||||
buffer.extend_from_slice(b"PlayingApp");
|
||||
buffer.push(0x56);
|
||||
buffer.extend_from_slice(b"com.google.ios.youtube");
|
||||
buffer.push(0x52);
|
||||
buffer.extend_from_slice(b"HostStreamingState");
|
||||
buffer.push(0x42);
|
||||
buffer.extend_from_slice(if streaming_state { b"YES" } else { b"NO" });
|
||||
buffer.push(0x49);
|
||||
buffer.extend_from_slice(b"btAddress");
|
||||
buffer.push(0x51);
|
||||
buffer.extend_from_slice(self_mac_address.as_bytes());
|
||||
buffer.extend_from_slice(b"btName");
|
||||
buffer.push(0x43);
|
||||
buffer.extend_from_slice(b"Mac");
|
||||
buffer.push(0x58);
|
||||
buffer.extend_from_slice(b"otherDevice");
|
||||
buffer.extend_from_slice(b"AudioCategory");
|
||||
buffer.extend_from_slice(&[0x31, 0x2D, 0x01]);
|
||||
|
||||
while buffer.len() < 138 {
|
||||
buffer.push(0x00);
|
||||
}
|
||||
let packet = [opcode.as_slice(), buffer.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_smart_routing_show_ui(&self, target_mac_address: &str) -> Result<()> {
|
||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||
let mut buffer = Vec::with_capacity(134);
|
||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||
buffer.extend_from_slice(&[0x7E, 0x00]);
|
||||
buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]);
|
||||
buffer.extend_from_slice(b"SmartRoutingKeyShowNearbyUI");
|
||||
buffer.push(0x01);
|
||||
buffer.push(0x4A);
|
||||
buffer.extend_from_slice(b"localscore");
|
||||
buffer.extend_from_slice(&[0x31, 0x2D]);
|
||||
buffer.push(0x01);
|
||||
buffer.push(0x46);
|
||||
buffer.extend_from_slice(b"reasonHhijackv2");
|
||||
buffer.push(0x51);
|
||||
buffer.extend_from_slice(b"audioRoutingScore");
|
||||
buffer.push(0xA2);
|
||||
buffer.push(0x5F);
|
||||
buffer.extend_from_slice(b"audioRoutingSetOwnershipToFalse");
|
||||
buffer.push(0x01);
|
||||
buffer.push(0x4B);
|
||||
buffer.extend_from_slice(b"remotescore");
|
||||
buffer.push(0xA2);
|
||||
|
||||
while buffer.len() < 134 {
|
||||
buffer.push(0x00);
|
||||
}
|
||||
|
||||
let packet = [opcode.as_slice(), buffer.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_hijack_reversed(&self, target_mac_address: &str) -> Result<()> {
|
||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||
let mut buffer = Vec::with_capacity(97);
|
||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||
buffer.extend_from_slice(&[0x59, 0x00]);
|
||||
buffer.extend_from_slice(&[0x01, 0xE3]);
|
||||
buffer.push(0x5F);
|
||||
buffer.extend_from_slice(b"audioRoutingSetOwnershipToFalse");
|
||||
buffer.push(0x01);
|
||||
buffer.push(0x59);
|
||||
buffer.extend_from_slice(b"audioRoutingShowReverseUI");
|
||||
buffer.push(0x01);
|
||||
buffer.push(0x46);
|
||||
buffer.extend_from_slice(b"reason");
|
||||
buffer.push(0x53);
|
||||
buffer.extend_from_slice(b"ReverseBannerTapped");
|
||||
|
||||
while buffer.len() < 97 {
|
||||
buffer.push(0x00);
|
||||
}
|
||||
|
||||
let packet = [opcode.as_slice(), buffer.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_add_tipi_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> {
|
||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||
let mut buffer = Vec::with_capacity(86);
|
||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||
buffer.extend_from_slice(&[0x4E, 0x00]);
|
||||
buffer.extend_from_slice(&[0x01, 0xE5]);
|
||||
buffer.extend_from_slice(&[0x48, 0x69]);
|
||||
buffer.extend_from_slice(b"idleTime");
|
||||
buffer.extend_from_slice(&[0x08, 0x47]);
|
||||
buffer.extend_from_slice(b"newTipi");
|
||||
buffer.extend_from_slice(&[0x01, 0x49]);
|
||||
buffer.extend_from_slice(b"btAddress");
|
||||
buffer.push(0x51);
|
||||
buffer.extend_from_slice(self_mac_address.as_bytes());
|
||||
buffer.push(0x46);
|
||||
buffer.extend_from_slice(b"btName");
|
||||
buffer.push(0x43);
|
||||
buffer.extend_from_slice(b"Mac");
|
||||
buffer.push(0x50);
|
||||
buffer.extend_from_slice(b"nearbyAudioScore");
|
||||
buffer.push(0x0E);
|
||||
|
||||
let packet = [opcode.as_slice(), buffer.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_some_packet(&self) -> Result<()> {
|
||||
self.send_data_packet(&[
|
||||
0x29, 0x00,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
]).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
|
||||
@@ -665,6 +871,11 @@ async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Read error: {}", e);
|
||||
debug!("We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty).");
|
||||
let mut state = manager.state.lock().await;
|
||||
state.owns = false;
|
||||
state.connected_devices.clear();
|
||||
state.control_command_status_list.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::ui::tray::MyTray;
|
||||
#[tokio::main]
|
||||
async fn main() -> bluer::Result<()> {
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
unsafe { env::set_var("RUST_LOG", "info"); }
|
||||
unsafe { env::set_var("RUST_LOG", "debug"); }
|
||||
}
|
||||
|
||||
env_logger::init();
|
||||
|
||||
@@ -19,6 +19,7 @@ use libpulse_binding::proplist::Proplist;
|
||||
use libpulse_binding::{
|
||||
volume::{ChannelVolumes, Volume},
|
||||
};
|
||||
use crate::bluetooth::aacp::AACPManager;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OwnedCardProfileInfo {
|
||||
@@ -41,6 +42,7 @@ struct OwnedSinkInfo {
|
||||
|
||||
struct MediaControllerState {
|
||||
connected_device_mac: String,
|
||||
local_mac: String,
|
||||
is_playing: bool,
|
||||
paused_by_app_services: Vec<String>,
|
||||
device_index: Option<u32>,
|
||||
@@ -52,12 +54,14 @@ struct MediaControllerState {
|
||||
disconnect_when_not_wearing: bool,
|
||||
conv_original_volume: Option<u32>,
|
||||
conv_conversation_started: bool,
|
||||
playback_listener_running: bool,
|
||||
}
|
||||
|
||||
impl MediaControllerState {
|
||||
fn new() -> Self {
|
||||
MediaControllerState {
|
||||
connected_device_mac: String::new(),
|
||||
local_mac: String::new(),
|
||||
is_playing: false,
|
||||
paused_by_app_services: Vec::new(),
|
||||
device_index: None,
|
||||
@@ -69,6 +73,7 @@ impl MediaControllerState {
|
||||
disconnect_when_not_wearing: true,
|
||||
conv_original_volume: None,
|
||||
conv_conversation_started: false,
|
||||
playback_listener_running: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,14 +84,104 @@ pub struct MediaController {
|
||||
}
|
||||
|
||||
impl MediaController {
|
||||
pub fn new(connected_mac: String) -> Self {
|
||||
pub fn new(connected_mac: String, local_mac: String) -> Self {
|
||||
let mut state = MediaControllerState::new();
|
||||
state.connected_device_mac = connected_mac;
|
||||
state.local_mac = local_mac;
|
||||
MediaController {
|
||||
state: Arc::new(Mutex::new(state)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_playback_listener(&self, aacp_manager: AACPManager, control_tx: tokio::sync::mpsc::UnboundedSender<(crate::bluetooth::aacp::ControlCommandIdentifiers, Vec<u8>)>) {
|
||||
let mut state = self.state.lock().await;
|
||||
if state.playback_listener_running {
|
||||
debug!("Playback listener already running");
|
||||
return;
|
||||
}
|
||||
state.playback_listener_running = true;
|
||||
drop(state);
|
||||
|
||||
let controller_clone = self.clone();
|
||||
tokio::spawn(async move {
|
||||
controller_clone.playback_listener_loop(aacp_manager, control_tx).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn playback_listener_loop(&self, aacp_manager: AACPManager, control_tx: tokio::sync::mpsc::UnboundedSender<(crate::bluetooth::aacp::ControlCommandIdentifiers, Vec<u8>)>) {
|
||||
info!("Starting playback listener loop");
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
let is_playing = tokio::task::spawn_blocking(|| {
|
||||
Self::check_if_playing()
|
||||
}).await.unwrap_or(false);
|
||||
|
||||
let mut state = self.state.lock().await;
|
||||
let was_playing = state.is_playing;
|
||||
state.is_playing = is_playing;
|
||||
let local_mac = state.local_mac.clone();
|
||||
drop(state);
|
||||
|
||||
if !was_playing && is_playing {
|
||||
info!("Media playback started, taking ownership and activating a2dp");
|
||||
let _ = control_tx.send((crate::bluetooth::aacp::ControlCommandIdentifiers::OwnsConnection, vec![0x01]));
|
||||
self.activate_a2dp_profile().await;
|
||||
|
||||
info!("already connected locally, hijacking connection by asking AirPods");
|
||||
|
||||
let connected_devices = aacp_manager.get_connected_devices().await;
|
||||
for device in connected_devices {
|
||||
if device.mac != local_mac {
|
||||
if let Err(e) = aacp_manager.send_media_information(&local_mac, &device.mac, true).await {
|
||||
error!("Failed to send media information to {}: {}", device.mac, e);
|
||||
}
|
||||
if let Err(e) = aacp_manager.send_smart_routing_show_ui(&device.mac).await {
|
||||
error!("Failed to send smart routing show ui to {}: {}", device.mac, e);
|
||||
}
|
||||
if let Err(e) = aacp_manager.send_hijack_request(&device.mac).await {
|
||||
error!("Failed to send hijack request to {}: {}", device.mac, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_if_playing() -> bool {
|
||||
let conn = match Connection::new_session() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(5));
|
||||
let (names,): (Vec<String>,) = match proxy.method_call("org.freedesktop.DBus", "ListNames", ()) {
|
||||
Ok(n) => n,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
for service in names {
|
||||
if !service.starts_with("org.mpris.MediaPlayer2.") {
|
||||
continue;
|
||||
}
|
||||
if Self::is_kdeconnect_service(&service) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if let Ok(playback_status) = proxy.get::<String>("org.mpris.MediaPlayer2.Player", "PlaybackStatus") {
|
||||
if playback_status == "Playing" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_kdeconnect_service(service: &str) -> bool {
|
||||
service.starts_with("org.mpris.MediaPlayer2.kdeconnect.mpris_")
|
||||
}
|
||||
|
||||
pub async fn handle_ear_detection(&self, old_statuses: Vec<EarDetectionStatus>, new_statuses: Vec<EarDetectionStatus>) {
|
||||
debug!("Entering handle_ear_detection with old_statuses: {:?}, new_statuses: {:?}", old_statuses, new_statuses);
|
||||
|
||||
@@ -262,19 +357,25 @@ impl MediaController {
|
||||
let mut paused_services = Vec::new();
|
||||
|
||||
for service in names {
|
||||
if service.starts_with("org.mpris.MediaPlayer2.") {
|
||||
debug!("Checking playback status for service: {}", service);
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if let Ok(playback_status) = proxy.get::<String>("org.mpris.MediaPlayer2.Player", "PlaybackStatus") {
|
||||
if playback_status == "Playing" {
|
||||
debug!("Service {} is playing, attempting to pause", service);
|
||||
if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Pause", ()).is_ok() {
|
||||
info!("Paused playback for: {}", service);
|
||||
paused_services.push(service);
|
||||
} else {
|
||||
debug!("Failed to pause service: {}", service);
|
||||
error!("Failed to pause {}", service);
|
||||
}
|
||||
if !service.starts_with("org.mpris.MediaPlayer2.") {
|
||||
continue;
|
||||
}
|
||||
if Self::is_kdeconnect_service(&service) {
|
||||
debug!("Skipping kdeconnect service: {}", service);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Checking playback status for service: {}", service);
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if let Ok(playback_status) = proxy.get::<String>("org.mpris.MediaPlayer2.Player", "PlaybackStatus") {
|
||||
if playback_status == "Playing" {
|
||||
debug!("Service {} is playing, attempting to pause", service);
|
||||
if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Pause", ()).is_ok() {
|
||||
info!("Paused playback for: {}", service);
|
||||
paused_services.push(service);
|
||||
} else {
|
||||
debug!("Failed to pause service: {}", service);
|
||||
error!("Failed to pause {}", service);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,12 +388,59 @@ impl MediaController {
|
||||
info!("Paused {} media player(s) via DBus", paused_services.len());
|
||||
let mut state = self.state.lock().await;
|
||||
state.paused_by_app_services = paused_services;
|
||||
state.is_playing = false;
|
||||
} else {
|
||||
debug!("No playing media players found");
|
||||
info!("No playing media players found to pause");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn pause_all_media(&self) {
|
||||
debug!("Pausing all media (without tracking for resume)");
|
||||
|
||||
let paused_count = tokio::task::spawn_blocking(|| {
|
||||
debug!("Listing DBus names for media players");
|
||||
let conn = Connection::new_session().unwrap();
|
||||
let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(5));
|
||||
let (names,): (Vec<String>,) = proxy.method_call("org.freedesktop.DBus", "ListNames", ()).unwrap();
|
||||
let mut paused_count = 0;
|
||||
|
||||
for service in names {
|
||||
if !service.starts_with("org.mpris.MediaPlayer2.") {
|
||||
continue;
|
||||
}
|
||||
if Self::is_kdeconnect_service(&service) {
|
||||
debug!("Skipping kdeconnect service: {}", service);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Checking playback status for service: {}", service);
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if let Ok(playback_status) = proxy.get::<String>("org.mpris.MediaPlayer2.Player", "PlaybackStatus") {
|
||||
if playback_status == "Playing" {
|
||||
debug!("Service {} is playing, attempting to pause", service);
|
||||
if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Pause", ()).is_ok() {
|
||||
info!("Paused playback for: {}", service);
|
||||
paused_count += 1;
|
||||
} else {
|
||||
debug!("Failed to pause service: {}", service);
|
||||
error!("Failed to pause {}", service);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paused_count
|
||||
}).await.unwrap();
|
||||
|
||||
if paused_count > 0 {
|
||||
info!("Paused {} media player(s) due to ownership loss", paused_count);
|
||||
let mut state = self.state.lock().await;
|
||||
state.is_playing = false;
|
||||
} else {
|
||||
debug!("No playing media players found to pause");
|
||||
}
|
||||
}
|
||||
|
||||
async fn resume(&self) {
|
||||
debug!("Entering resume method");
|
||||
debug!("Resuming playback");
|
||||
@@ -310,6 +458,11 @@ impl MediaController {
|
||||
let conn = Connection::new_session().unwrap();
|
||||
let mut resumed_count = 0;
|
||||
for service in services {
|
||||
if Self::is_kdeconnect_service(&service) {
|
||||
debug!("Skipping kdeconnect service: {}", service);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Attempting to resume service: {}", service);
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Play", ()).is_ok() {
|
||||
@@ -454,7 +607,7 @@ impl MediaController {
|
||||
}
|
||||
}
|
||||
|
||||
let introspector = context.introspect();
|
||||
let mut introspector = context.introspect();
|
||||
let card_info_list = Rc::new(RefCell::new(None));
|
||||
let op = introspector.get_card_info_list({
|
||||
let card_info_list = card_info_list.clone();
|
||||
|
||||
Reference in New Issue
Block a user