diff --git a/build.py b/build.py index 0cf62daf1..a81a9a999 100755 --- a/build.py +++ b/build.py @@ -71,7 +71,7 @@ def make_parser(): parser.add_argument( '--hwcodec', action='store_true', - help='Enable feature hwcodec' + help='Enable feature hwcodec' + ('' if windows or osx else ', need libva-dev, libvdpau-dev.') ) parser.add_argument( '--portable', diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 318843699..8964191f8 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -39,6 +39,10 @@ class _ConnectionPageState extends State final RxBool _idInputFocused = false.obs; final FocusNode _idFocusNode = FocusNode(); + var svcStopped = false.obs; + var svcStatusCode = 0.obs; + var svcIsUsingPublicServer = true.obs; + @override void initState() { super.initState(); @@ -55,6 +59,18 @@ class _ConnectionPageState extends State _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { updateStatus(); }); + _idFocusNode.addListener(() { + _idInputFocused.value = _idFocusNode.hasFocus; + }); + Get.put(svcStopped, tag: 'service-stop'); + } + + @override + void dispose() { + _idController.dispose(); + _updateTimer?.cancel(); + Get.delete(tag: 'service-stop'); + super.dispose(); } @override @@ -107,9 +123,8 @@ class _ConnectionPageState extends State ).paddingOnly(left: 12.0), ), ), - const Divider(), - SizedBox(child: Obx(() => buildStatus())) - .paddingOnly(bottom: 12, top: 6), + const Divider(height: 1), + buildStatus() ], ); } @@ -124,9 +139,6 @@ class _ConnectionPageState extends State /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField(BuildContext context) { - _idFocusNode.addListener(() { - _idInputFocused.value = _idFocusNode.hasFocus; - }); var w = Container( width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), @@ -223,91 +235,74 @@ class _ConnectionPageState extends State constraints: const BoxConstraints(maxWidth: 600), child: w)); } - @override - void dispose() { - _idController.dispose(); - _updateTimer?.cancel(); - super.dispose(); - } - - var svcStopped = false.obs; - var svcStatusCode = 0.obs; - var svcIsUsingPublicServer = true.obs; - Widget buildStatus() { - final fontSize = 14.0; - final textStyle = TextStyle(fontSize: fontSize); - final light = Container( - height: 8, - width: 8, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: svcStopped.value || svcStatusCode.value == 0 - ? kColorWarn - : (svcStatusCode.value == 1 - ? Color.fromARGB(255, 50, 190, 166) - : Color.fromARGB(255, 224, 79, 95)), - ), - ).paddingSymmetric(horizontal: 12.0); - if (svcStopped.value) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate("Service is not running"), style: textStyle), - TextButton( - onPressed: () async { - bool checked = await bind.mainCheckSuperUserPermission(); - if (checked) { - bind.mainSetOption(key: "stop-service", value: ""); - bind.mainSetOption(key: "access-mode", value: ""); - } - }, - child: Text(translate("Start Service"), style: textStyle)) - ], - ); - } else { - if (svcStatusCode.value == 0) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate("connecting_status"), style: textStyle) - ], - ); - } else if (svcStatusCode.value == -1) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate("not_ready_status"), style: textStyle) - ], - ); - } - } - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate('Ready'), style: textStyle), - Offstage( - offstage: !svcIsUsingPublicServer.value, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(', ', style: textStyle), - InkWell( - onTap: onUsePublicServerGuide, - child: Text( - translate('setup_server_tip'), - style: TextStyle( - decoration: TextDecoration.underline, - fontSize: fontSize), - ), - ) - ], - )) - ], + final em = 14.0; + return ConstrainedBox( + constraints: BoxConstraints.tightFor(height: 3 * em), + child: Obx(() => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: svcStopped.value || svcStatusCode.value == 0 + ? kColorWarn + : (svcStatusCode.value == 1 + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), + ), + ).marginSymmetric(horizontal: em), + Text( + svcStopped.value + ? translate("Service is not running") + : svcStatusCode.value == 0 + ? translate("connecting_status") + : svcStatusCode.value == -1 + ? translate("not_ready_status") + : translate('Ready'), + style: TextStyle(fontSize: em)), + // stop + Offstage( + offstage: !svcStopped.value, + child: GestureDetector( + onTap: () async { + bool checked = + await bind.mainCheckSuperUserPermission(); + if (checked) { + bind.mainSetOption(key: "stop-service", value: ""); + bind.mainSetOption(key: "access-mode", value: ""); + } + }, + child: Text(translate("Start Service"), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em))) + .marginOnly(left: em), + ), + // ready && public + Offstage( + offstage: !(!svcStopped.value && + svcStatusCode.value == 1 && + svcIsUsingPublicServer.value), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: TextStyle(fontSize: em)), + InkWell( + onTap: onUsePublicServerGuide, + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, fontSize: em), + ), + ) + ], + ), + ) + ], + )), ); } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9787048fa..23d832580 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -432,6 +432,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { bool get wantKeepAlive => true; bool locked = bind.mainIsInstalled(); final scrollController = ScrollController(); + final RxBool serviceStop = Get.find(tag: 'service-stop'); @override Widget build(BuildContext context) { @@ -465,17 +466,15 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { } Widget permissions(context) { - bool enabled = !locked; + return Obx(() => _permissions(context, serviceStop.value)); + } + Widget _permissions(context, bool stopService) { + bool enabled = !locked; return _futureBuilder(future: () async { - bool stopService = option2bool( - 'stop-service', await bind.mainGetOption(key: 'stop-service')); - final accessMode = await bind.mainGetOption(key: 'access-mode'); - return {'stopService': stopService, 'accessMode': accessMode}; + return await bind.mainGetOption(key: 'access-mode'); }(), hasData: (data) { - var map = data! as Map; - bool stopService = map['stopService'] as bool; - String accessMode = map['accessMode'] as String; + String accessMode = data! as String; _AccessMode mode; if (stopService) { mode = _AccessMode.deny; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 72cc56cad..e670e5d80 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -742,59 +742,147 @@ class _RemoteMenubarState extends State { await bind.sessionSetImageQuality(id: widget.id, value: newValue); } + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + id: widget.id, fps: fpsInitValue.toInt()); + } + } + if (newValue == 'custom') { - final btnCancel = msgBoxButton(translate('Close'), () { + final btnClose = msgBoxButton(translate('Close'), () async { + await setCustomValues(); widget.ffi.dialogManager.dismissAll(); }); + + // quality final quality = await bind.sessionGetCustomImageQuality(id: widget.id); - double initValue = quality != null && quality.isNotEmpty + qualityInitValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - const minValue = 10.0; - const maxValue = 100.0; - if (initValue < minValue) { - initValue = minValue; + const qualityMinValue = 10.0; + const qualityMaxValue = 100.0; + if (qualityInitValue < qualityMinValue) { + qualityInitValue = qualityMinValue; } - if (initValue > maxValue) { - initValue = maxValue; + if (qualityInitValue > qualityMaxValue) { + qualityInitValue = qualityMaxValue; } - final RxDouble sliderValue = RxDouble(initValue); - final rxReplay = rxdart.ReplaySubject(); - rxReplay + final RxDouble qualitySliderValue = RxDouble(qualityInitValue); + final qualityRxReplay = rxdart.ReplaySubject(); + qualityRxReplay .throttleTime(const Duration(milliseconds: 1000), trailing: true, leading: false) .listen((double v) { () async { - await bind.sessionSetCustomImageQuality( - id: widget.id, value: v.toInt()); + await setCustomValues(quality: v); }(); }); - final slider = Obx(() { - return Slider( - value: sliderValue.value, - min: minValue, - max: maxValue, - divisions: 90, - onChanged: (double value) { - sliderValue.value = value; - rxReplay.add(value); - }, - ); + final qualitySlider = Obx(() => Row( + children: [ + Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 90, + onChanged: (double value) { + qualitySliderValue.value = value; + qualityRxReplay.add(value); + }, + ), + SizedBox( + width: 90, + child: Obx(() => Text( + '${qualitySliderValue.value.round()}% Bitrate', + style: const TextStyle(fontSize: 15), + ))) + ], + )); + // fps + final fpsOption = + await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); + fpsInitValue = + fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 10 || fpsInitValue > 120) { + fpsInitValue = 30; + } + final RxDouble fpsSliderValue = RxDouble(fpsInitValue); + final fpsRxReplay = rxdart.ReplaySubject(); + fpsRxReplay + .throttleTime(const Duration(milliseconds: 1000), + trailing: true, leading: false) + .listen((double v) { + () async { + await setCustomValues(fps: v); + }(); }); - final content = Row( - children: [ - slider, - SizedBox( - width: 90, - child: Obx(() => Text( - '${sliderValue.value.round()}% Bitrate', + bool? direct; + try { + direct = ConnectionTypeState.find(widget.id).direct.value == + ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: + (await bind.mainIsUsingPublicServer() && direct != true) || + (await bind.versionToNumber( + v: widget.ffi.ffiModel.pi.version) < + await bind.versionToNumber(v: '1.2.0')), + child: Row( + children: [ + Obx((() => Slider( + value: fpsSliderValue.value, + min: 10, + max: 120, + divisions: 22, + onChanged: (double value) { + fpsSliderValue.value = value; + fpsRxReplay.add(value); + }, + ))), + SizedBox( + width: 90, + child: Obx(() { + final fps = fpsSliderValue.value.round(); + String text; + if (fps < 100) { + text = '$fps FPS'; + } else { + text = '$fps FPS'; + } + return Text( + text, style: const TextStyle(fontSize: 15), - ))) - ], + ); + })) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], ); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnCancel]); + content, [btnClose]); } }, padding: padding, diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index a48ec9d14..4bb015866 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -480,6 +480,7 @@ message OptionMessage { BoolOption disable_clipboard = 8; BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; + int32 custom_fps = 11; } message TestDelay { diff --git a/src/client.rs b/src/client.rs index 6b3917790..9e2627d55 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1092,7 +1092,12 @@ impl LoginConfigHandler { n += 1; } else if q == "custom" { let config = PeerConfig::load(&self.id); - msg.custom_image_quality = config.custom_image_quality[0] << 8; + let quality = if config.custom_image_quality.is_empty() { + 50 + } else { + config.custom_image_quality[0] + }; + msg.custom_image_quality = quality << 8; n += 1; } if self.get_toggle_option("show-remote-cursor") { @@ -1253,6 +1258,27 @@ impl LoginConfigHandler { res } + /// Create a [`Message`] for saving custom fps. + /// + /// # Arguments + /// + /// * `fps` - The given fps. + pub fn set_custom_fps(&mut self, fps: i32) -> Message { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps: fps, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + let mut config = self.load_config(); + config + .options + .insert("custom-fps".to_owned(), fps.to_string()); + self.save_config(config); + msg_out + } + pub fn get_option(&self, k: &str) -> String { if let Some(v) = self.config.options.get(k) { v.clone() diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 873248cca..5fdb3122c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -186,6 +186,12 @@ pub fn session_set_custom_image_quality(id: String, value: i32) { } } +pub fn session_set_custom_fps(id: String, fps: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.set_custom_fps(fps); + } +} + pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.lock_screen(); @@ -1000,6 +1006,10 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } +pub fn version_to_number(v: String) -> i64 { + hbb_common::get_version_number(&v) +} + pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 4f122b59d..c4dc615be 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1316,16 +1316,25 @@ impl Connection { if o.custom_image_quality > 0 { image_quality = o.custom_image_quality; } else { - image_quality = ImageQuality::Balanced.value(); + image_quality = -1; } } else { image_quality = q.value(); } + if image_quality > 0 { + video_service::VIDEO_QOS + .lock() + .unwrap() + .update_image_quality(image_quality); + } + } + if o.custom_fps > 0 { video_service::VIDEO_QOS .lock() .unwrap() - .update_image_quality(image_quality); + .update_user_fps(o.custom_fps as _); } + if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index b0e06bc03..7aaf12d92 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -1,6 +1,8 @@ use super::*; use std::time::Duration; const FPS: u8 = 30; +const MIN_FPS: u8 = 10; +const MAX_FPS: u8 = 120; trait Percent { fn as_percent(&self) -> u32; } @@ -23,7 +25,8 @@ pub struct VideoQoS { current_image_quality: u32, enable_abr: bool, pub current_delay: u32, - pub fps: u8, // abr + pub fps: u8, // abr + pub user_fps: u8, pub target_bitrate: u32, // abr updated: bool, state: DelayState, @@ -56,6 +59,7 @@ impl Default for VideoQoS { fn default() -> Self { VideoQoS { fps: FPS, + user_fps: FPS, user_image_quality: ImageQuality::Balanced.as_percent(), current_image_quality: ImageQuality::Balanced.as_percent(), enable_abr: false, @@ -80,12 +84,19 @@ impl VideoQoS { } pub fn spf(&mut self) -> Duration { - if self.fps <= 0 { - self.fps = FPS; + if self.fps < MIN_FPS || self.fps > MAX_FPS { + self.fps = self.base_fps(); } Duration::from_secs_f32(1. / (self.fps as f32)) } + fn base_fps(&self) -> u8 { + if self.user_fps >= MIN_FPS && self.user_fps <= MAX_FPS { + return self.user_fps; + } + return FPS; + } + // update_network_delay periodically // decrease the bitrate when the delay gets bigger pub fn update_network_delay(&mut self, delay: u32) { @@ -124,19 +135,19 @@ impl VideoQoS { fn refresh_quality(&mut self) { match self.state { DelayState::Normal => { - self.fps = FPS; + self.fps = self.base_fps(); self.current_image_quality = self.user_image_quality; } DelayState::LowDelay => { - self.fps = FPS; + self.fps = self.base_fps(); self.current_image_quality = std::cmp::min(self.user_image_quality, 50); } DelayState::HighDelay => { - self.fps = FPS / 2; + self.fps = self.base_fps() / 2; self.current_image_quality = std::cmp::min(self.user_image_quality, 25); } DelayState::Broken => { - self.fps = FPS / 4; + self.fps = self.base_fps() / 4; self.current_image_quality = 10; } } @@ -146,6 +157,14 @@ impl VideoQoS { // handle image_quality change from peer pub fn update_image_quality(&mut self, image_quality: i32) { + if image_quality == ImageQuality::Low.value() + || image_quality == ImageQuality::Balanced.value() + || image_quality == ImageQuality::Best.value() + { + // not custom + self.user_fps = FPS; + self.fps = FPS; + } let image_quality = Self::convert_quality(image_quality) as _; if self.current_image_quality != image_quality { self.current_image_quality = image_quality; @@ -156,6 +175,16 @@ impl VideoQoS { self.user_image_quality = self.current_image_quality; } + pub fn update_user_fps(&mut self, fps: u8) { + if fps >= MIN_FPS && fps <= MAX_FPS { + if self.user_fps != fps { + self.user_fps = fps; + self.fps = fps; + self.updated = true; + } + } + } + pub fn generate_bitrate(&mut self) -> ResultType { // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/ if self.width == 0 || self.height == 0 { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cf550ed63..f95a743c2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -134,6 +134,11 @@ impl Session { } } + pub fn set_custom_fps(&mut self, custom_fps: i32) { + let msg = self.lc.write().unwrap().set_custom_fps(custom_fps); + self.send(Data::Message(msg)); + } + pub fn get_remember(&self) -> bool { self.lc.read().unwrap().remember } @@ -1181,7 +1186,12 @@ impl Interface for Session { if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { - self.msgbox("success", "Successful", "Connected, waiting for image...", ""); + self.msgbox( + "success", + "Successful", + "Connected, waiting for image...", + "", + ); } #[cfg(windows)] {