refact: seperate audio device for voice call (#8703)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2024-07-14 04:07:02 +08:00 committed by GitHub
parent d18e95703e
commit 30afe4f779
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 162 additions and 74 deletions

View File

@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
const _kWindowsSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device); typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function( typedef AudioInputBuilder = Widget Function(
List<String> devices, String currentDevice, AudioINputSetDevice setDevice); List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
class AudioInput extends StatelessWidget { class AudioInput extends StatelessWidget {
final AudioInputBuilder builder; final AudioInputBuilder builder;
final bool isCm;
final bool isVoiceCall;
const AudioInput({Key? key, required this.builder}) : super(key: key); const AudioInput(
{Key? key,
required this.builder,
required this.isCm,
required this.isVoiceCall})
: super(key: key);
static String getDefault() { static String getDefault() {
if (isWindows) return translate('System Sound'); if (isWindows) return translate('System Sound');
return ''; return '';
} }
static Future<String> getValue() async { static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
String device = await bind.mainGetOption(key: 'audio-input'); if (isVoiceCall) {
return bind.getVoiceCallInputDevice(isCm: isCm);
} else {
return bind.mainGetOption(key: 'audio-input');
}
}
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
String device = await getAudioInput(isCm, isVoiceCall);
if (device.isNotEmpty) { if (device.isNotEmpty) {
return device; return device;
} else { } else {
@ -25,31 +42,39 @@ class AudioInput extends StatelessWidget {
} }
} }
static Future<void> setDevice(String device) async { static Future<void> setDevice(
String device, bool isCm, bool isVoiceCall) async {
if (device == getDefault()) device = ''; if (device == getDefault()) device = '';
await bind.mainSetOption(key: 'audio-input', value: device); if (isVoiceCall) {
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
} else {
await bind.mainSetOption(key: 'audio-input', value: device);
}
} }
static Future<Map<String, Object>> getDevicesInfo() async { static Future<Map<String, Object>> getDevicesInfo(
bool isCm, bool isVoiceCall) async {
List<String> devices = (await bind.mainGetSoundInputs()).toList(); List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) { if (isWindows) {
devices.insert(0, translate('System Sound')); devices.insert(0, translate(_kWindowsSystemSound));
} }
String current = await getValue(); String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current}; return {'devices': devices, 'current': current};
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return futureBuilder( return futureBuilder(
future: getDevicesInfo(), future: getDevicesInfo(isCm, isVoiceCall),
hasData: (data) { hasData: (data) {
String currentDevice = data['current']; String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>; List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) { if (devices.isEmpty) {
return const Offstage(); return const Offstage();
} }
return builder(devices, currentDevice, setDevice); return builder(devices, currentDevice, (devices) {
setDevice(devices, isCm, isVoiceCall);
});
}, },
); );
} }

View File

@ -500,7 +500,7 @@ class _GeneralState extends State<_General> {
return const Offstage(); return const Offstage();
} }
return AudioInput(builder: (devices, currentDevice, setDevice) { builder(devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [ return _Card(title: 'Audio Input Device', children: [
...devices.map((device) => _Radio<String>(context, ...devices.map((device) => _Radio<String>(context,
value: device, value: device,
@ -511,7 +511,9 @@ class _GeneralState extends State<_General> {
setState(() {}); setState(() {});
})) }))
]); ]);
}); }
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
} }
Widget record(BuildContext context) { Widget record(BuildContext context) {

View File

@ -732,7 +732,7 @@ class _CmControlPanel extends StatelessWidget {
child: buildButton(context, child: buildButton(context,
color: MyTheme.accent, color: MyTheme.accent,
onClick: null, onTapDown: (details) async { onClick: null, onTapDown: (details) async {
final devicesInfo = await AudioInput.getDevicesInfo(); final devicesInfo = await AudioInput.getDevicesInfo(true, true);
List<String> devices = devicesInfo['devices'] as List<String>; List<String> devices = devicesInfo['devices'] as List<String>;
if (devices.isEmpty) { if (devices.isEmpty) {
msgBox( msgBox(
@ -758,13 +758,13 @@ class _CmControlPanel extends StatelessWidget {
value: d, value: d,
height: 18, height: 18,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onTap: () => AudioInput.setDevice(d), onTap: () => AudioInput.setDevice(d, true, true),
child: IgnorePointer( child: IgnorePointer(
child: RadioMenuButton( child: RadioMenuButton(
value: d, value: d,
groupValue: currentDevice, groupValue: currentDevice,
onChanged: (v) { onChanged: (v) {
if (v != null) AudioInput.setDevice(v); if (v != null) AudioInput.setDevice(v, true, true);
}, },
child: Container( child: Container(
child: Text( child: Text(

View File

@ -1984,28 +1984,31 @@ class _VoiceCallMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
menuChildrenGetter() { menuChildrenGetter() {
final audioInput = final audioInput = AudioInput(
AudioInput(builder: (devices, currentDevice, setDevice) { builder: (devices, currentDevice, setDevice) {
return Column( return Column(
children: devices children: devices
.map((d) => RdoMenuButton<String>( .map((d) => RdoMenuButton<String>(
child: Container( child: Container(
child: Text( child: Text(
d, d,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
),
constraints: BoxConstraints(maxWidth: 250),
), ),
constraints: BoxConstraints(maxWidth: 250), value: d,
), groupValue: currentDevice,
value: d, onChanged: (v) {
groupValue: currentDevice, if (v != null) setDevice(v);
onChanged: (v) { },
if (v != null) setDevice(v); ffi: ffi,
}, ))
ffi: ffi, .toList(),
)) );
.toList(), },
); isCm: false,
}); isVoiceCall: true,
);
return [ return [
audioInput, audioInput,
Divider(), Divider(),

View File

@ -42,7 +42,7 @@ use crate::client::{
}; };
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL}; use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL};
use crate::common::{get_default_sound_input, set_sound_input}; use crate::common::get_default_sound_input;
use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::ui_session_interface::{InvokeUiSession, Session};
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
use crate::{audio_service, ConnInner, CLIENT_SERVER}; use crate::{audio_service, ConnInner, CLIENT_SERVER};
@ -387,11 +387,12 @@ impl<T: InvokeUiSession> Remote<T> {
if self.handler.is_file_transfer() || self.handler.is_port_forward() { if self.handler.is_file_transfer() || self.handler.is_port_forward() {
return None; return None;
} }
// Switch to default input device // NOTE:
let default_sound_device = get_default_sound_input(); // The client server and --server both use the same sound input device.
if let Some(device) = default_sound_device { // It's better to distinguish the server side and client side.
set_sound_input(device); // But it' not necessary for now, because it's not a common case.
} // And it is immediately known when the input device is changed.
crate::audio_service::set_voice_call_input_device(get_default_sound_input(), false);
// iOS does not have this server. // iOS does not have this server.
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
{ {
@ -421,6 +422,7 @@ impl<T: InvokeUiSession> Remote<T> {
client_conn_inner, client_conn_inner,
false, false,
); );
crate::audio_service::set_voice_call_input_device(None, true);
break; break;
} }
_ => {} _ => {}

View File

@ -1319,6 +1319,25 @@ pub fn cm_close_voice_call(id: i32) {
crate::ui_cm_interface::close_voice_call(id); crate::ui_cm_interface::close_voice_call(id);
} }
pub fn set_voice_call_input_device(is_cm: bool, device: String) {
if is_cm {
let _ = crate::ipc::set_config("voice-call-input", device);
} else {
crate::audio_service::set_voice_call_input_device(Some(device), true);
}
}
pub fn get_voice_call_input_device(is_cm: bool) -> String {
if is_cm {
match crate::ipc::get_config("voice-call-input") {
Ok(Some(device)) => device,
_ => "".to_owned(),
}
} else {
crate::audio_service::get_voice_call_input_device().unwrap_or_default()
}
}
pub fn main_get_last_remote_id() -> String { pub fn main_get_last_remote_id() -> String {
LocalConfig::get_remote_id() LocalConfig::get_remote_id()
} }

View File

@ -308,7 +308,7 @@ pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
} }
} }
pub struct CheckIfRestart(String, Vec<String>, String); pub struct CheckIfRestart(String, Vec<String>, String, String);
impl CheckIfRestart { impl CheckIfRestart {
pub fn new() -> CheckIfRestart { pub fn new() -> CheckIfRestart {
@ -316,6 +316,7 @@ impl CheckIfRestart {
Config::get_option("stop-service"), Config::get_option("stop-service"),
Config::get_rendezvous_servers(), Config::get_rendezvous_servers(),
Config::get_option("audio-input"), Config::get_option("audio-input"),
Config::get_option("voice-call-input"),
) )
} }
} }
@ -329,6 +330,12 @@ impl Drop for CheckIfRestart {
if self.2 != Config::get_option("audio-input") { if self.2 != Config::get_option("audio-input") {
crate::audio_service::restart(); crate::audio_service::restart();
} }
if self.3 != Config::get_option("voice-call-input") {
crate::audio_service::set_voice_call_input_device(
Some(Config::get_option("voice-call-input")),
true,
)
}
} }
} }
@ -457,6 +464,8 @@ async fn handle(data: Data, stream: &mut Connection) {
} else { } else {
None None
}; };
} else if name == "voice-call-input" {
value = crate::audio_service::get_voice_call_input_device();
} else { } else {
value = None; value = None;
} }
@ -472,6 +481,8 @@ async fn handle(data: Data, stream: &mut Connection) {
Config::set_permanent_password(&value); Config::set_permanent_password(&value);
} else if name == "salt" { } else if name == "salt" {
Config::set_salt(&value); Config::set_salt(&value);
} else if name == "voice-call-input" {
crate::audio_service::set_voice_call_input_device(Some(value), true);
} else { } else {
return; return;
} }
@ -488,12 +499,7 @@ async fn handle(data: Data, stream: &mut Connection) {
if let Some(v) = value.get("privacy-mode-impl-key") { if let Some(v) = value.get("privacy-mode-impl-key") {
crate::privacy_mode::switch(v); crate::privacy_mode::switch(v);
} }
let pre_opts = Config::get_options();
let new_audio_input = pre_opts.get("audio-input");
Config::set_options(value); Config::set_options(value);
if new_audio_input != pre_opts.get("audio-input") {
crate::audio_service::restart();
}
allow_err!(stream.send(&Data::Options(None)).await); allow_err!(stream.send(&Data::Options(None)).await);
} }
}, },

View File

@ -22,6 +22,10 @@ pub const NAME: &'static str = "audio";
pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo
static RESTARTING: AtomicBool = AtomicBool::new(false); static RESTARTING: AtomicBool = AtomicBool::new(false);
lazy_static::lazy_static! {
static ref VOICE_CALL_INPUT_DEVICE: Arc::<Mutex::<Option<String>>> = Default::default();
}
#[cfg(not(any(target_os = "linux", target_os = "android")))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn new() -> GenericService { pub fn new() -> GenericService {
let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
@ -36,6 +40,33 @@ pub fn new() -> GenericService {
svc.sp svc.sp
} }
#[inline]
pub fn get_voice_call_input_device() -> Option<String> {
VOICE_CALL_INPUT_DEVICE.lock().unwrap().clone()
}
#[inline]
pub fn set_voice_call_input_device(device: Option<String>, set_if_present: bool) {
if !set_if_present && VOICE_CALL_INPUT_DEVICE.lock().unwrap().is_some() {
return;
}
if *VOICE_CALL_INPUT_DEVICE.lock().unwrap() == device {
return;
}
*VOICE_CALL_INPUT_DEVICE.lock().unwrap() = device;
restart();
}
#[inline]
fn get_audio_input() -> String {
VOICE_CALL_INPUT_DEVICE
.lock()
.unwrap()
.clone()
.unwrap_or(Config::get_option("audio-input"))
}
pub fn restart() { pub fn restart() {
log::info!("restart the audio service, freezing now..."); log::info!("restart the audio service, freezing now...");
if RESTARTING.load(Ordering::SeqCst) { if RESTARTING.load(Ordering::SeqCst) {
@ -62,7 +93,7 @@ mod pa_impl {
stream stream
.send(&crate::ipc::Data::Config(( .send(&crate::ipc::Data::Config((
"audio-input".to_owned(), "audio-input".to_owned(),
Some(Config::get_option("audio-input")) Some(super::get_audio_input())
))) )))
.await .await
); );
@ -132,6 +163,7 @@ mod cpal_impl {
} }
fn run_restart(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { fn run_restart(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> {
println!("REMOVE ME ========================= run_restart");
state.reset(); state.reset();
sp.snapshot(|_sps: ServiceSwap<_>| Ok(()))?; sp.snapshot(|_sps: ServiceSwap<_>| Ok(()))?;
match &state.stream { match &state.stream {
@ -198,7 +230,8 @@ mod cpal_impl {
#[cfg(windows)] #[cfg(windows)]
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
let audio_input = Config::get_option("audio-input"); let audio_input = super::get_audio_input();
println!("REMOVE ME =============================== use audio input: {}", &audio_input);
if !audio_input.is_empty() { if !audio_input.is_empty() {
return get_audio_input(&audio_input); return get_audio_input(&audio_input);
} }
@ -219,7 +252,7 @@ mod cpal_impl {
#[cfg(not(windows))] #[cfg(not(windows))]
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
let audio_input = Config::get_option("audio-input"); let audio_input = super::get_audio_input();
get_audio_input(&audio_input) get_audio_input(&audio_input)
} }

View File

@ -17,7 +17,6 @@ use crate::{
client::{ client::{
new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender,
}, },
common::{get_default_sound_input, set_sound_input},
display_service, ipc, privacy_mode, video_service, VERSION, display_service, ipc, privacy_mode, video_service, VERSION,
}; };
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
@ -218,7 +217,6 @@ pub struct Connection {
portable: PortableState, portable: PortableState,
from_switch: bool, from_switch: bool,
voice_call_request_timestamp: Option<NonZeroI64>, voice_call_request_timestamp: Option<NonZeroI64>,
audio_input_device_before_voice_call: Option<String>,
options_in_login: Option<OptionMessage>, options_in_login: Option<OptionMessage>,
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
pressed_modifiers: HashSet<rdev::Key>, pressed_modifiers: HashSet<rdev::Key>,
@ -367,7 +365,6 @@ impl Connection {
from_switch: false, from_switch: false,
audio_sender: None, audio_sender: None,
voice_call_request_timestamp: None, voice_call_request_timestamp: None,
audio_input_device_before_voice_call: None,
options_in_login: None, options_in_login: None,
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
pressed_modifiers: Default::default(), pressed_modifiers: Default::default(),
@ -2061,12 +2058,13 @@ impl Connection {
cb.content.into() cb.content.into()
}; };
if let Ok(content) = String::from_utf8(content) { if let Ok(content) = String::from_utf8(content) {
let data = HashMap::from([ let data =
("name", "clipboard"), HashMap::from([("name", "clipboard"), ("content", &content)]);
("content", &content),
]);
if let Ok(data) = serde_json::to_string(&data) { if let Ok(data) = serde_json::to_string(&data) {
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); let _ = crate::flutter::push_global_event(
crate::flutter::APP_TYPE_MAIN,
data,
);
} }
} }
} }
@ -2720,15 +2718,10 @@ impl Connection {
if let Some(ts) = self.voice_call_request_timestamp.take() { if let Some(ts) = self.voice_call_request_timestamp.take() {
let msg = new_voice_call_response(ts.get(), accepted); let msg = new_voice_call_response(ts.get(), accepted);
if accepted { if accepted {
// Backup the default input device. crate::audio_service::set_voice_call_input_device(
let audio_input_device = Config::get_option("audio-input"); crate::get_default_sound_input(),
log::debug!("Backup the sound input device {}", audio_input_device); false,
self.audio_input_device_before_voice_call = Some(audio_input_device); );
// Switch to default input device
let default_sound_device = get_default_sound_input();
if let Some(device) = default_sound_device {
set_sound_input(device);
}
self.send_to_cm(Data::StartVoiceCall); self.send_to_cm(Data::StartVoiceCall);
} else { } else {
self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
@ -2740,12 +2733,7 @@ impl Connection {
} }
pub async fn close_voice_call(&mut self) { pub async fn close_voice_call(&mut self) {
// Restore to the prior audio device. crate::audio_service::set_voice_call_input_device(None, true);
if let Some(sound_input) =
std::mem::replace(&mut self.audio_input_device_before_voice_call, None)
{
set_sound_input(sound_input);
}
// Notify the connection manager that the voice call has been closed. // Notify the connection manager that the voice call has been closed.
self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
} }
@ -3035,6 +3023,16 @@ impl Connection {
return; return;
} }
self.closed = true; self.closed = true;
// If voice A,B -> C, and A,B has voice call
// B disconnects, C will reset the voice call input.
//
// It may be acceptable, because it's not a common case,
// and it's immediately known when the input device changes.
// C can change the input device manually in cm interface.
//
// We can add a (Vec<conn_id>, input device) to avoid this.
// But it's not necessary now and we have to consider two audio services(client, server).
crate::audio_service::set_voice_call_input_device(None, true);
log::info!("#{} Connection closed: {}", self.inner.id(), reason); log::info!("#{} Connection closed: {}", self.inner.id(), reason);
if lock && self.lock_after_session_end && self.keyboard { if lock && self.lock_after_session_end && self.keyboard {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]