Merge pull request #1759 from 21pages/fps

support adjust fps && fix statusbar ui
This commit is contained in:
RustDesk 2022-10-20 17:08:20 +08:00 committed by GitHub
commit 493126e328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 311 additions and 144 deletions

View File

@ -71,7 +71,7 @@ def make_parser():
parser.add_argument( parser.add_argument(
'--hwcodec', '--hwcodec',
action='store_true', action='store_true',
help='Enable feature hwcodec' help='Enable feature hwcodec' + ('' if windows or osx else ', need libva-dev, libvdpau-dev.')
) )
parser.add_argument( parser.add_argument(
'--portable', '--portable',

View File

@ -39,6 +39,10 @@ class _ConnectionPageState extends State<ConnectionPage>
final RxBool _idInputFocused = false.obs; final RxBool _idInputFocused = false.obs;
final FocusNode _idFocusNode = FocusNode(); final FocusNode _idFocusNode = FocusNode();
var svcStopped = false.obs;
var svcStatusCode = 0.obs;
var svcIsUsingPublicServer = true.obs;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -55,6 +59,18 @@ class _ConnectionPageState extends State<ConnectionPage>
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
updateStatus(); updateStatus();
}); });
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
});
Get.put<RxBool>(svcStopped, tag: 'service-stop');
}
@override
void dispose() {
_idController.dispose();
_updateTimer?.cancel();
Get.delete<RxBool>(tag: 'service-stop');
super.dispose();
} }
@override @override
@ -107,9 +123,8 @@ class _ConnectionPageState extends State<ConnectionPage>
).paddingOnly(left: 12.0), ).paddingOnly(left: 12.0),
), ),
), ),
const Divider(), const Divider(height: 1),
SizedBox(child: Obx(() => buildStatus())) buildStatus()
.paddingOnly(bottom: 12, top: 6),
], ],
); );
} }
@ -124,9 +139,6 @@ class _ConnectionPageState extends State<ConnectionPage>
/// UI for the remote ID TextField. /// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists. /// Search for a peer and connect to it if the id exists.
Widget _buildRemoteIDTextField(BuildContext context) { Widget _buildRemoteIDTextField(BuildContext context) {
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
});
var w = Container( var w = Container(
width: 320 + 20 * 2, width: 320 + 20 * 2,
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
@ -223,91 +235,74 @@ class _ConnectionPageState extends State<ConnectionPage>
constraints: const BoxConstraints(maxWidth: 600), child: w)); 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() { Widget buildStatus() {
final fontSize = 14.0; final em = 14.0;
final textStyle = TextStyle(fontSize: fontSize); return ConstrainedBox(
final light = Container( constraints: BoxConstraints.tightFor(height: 3 * em),
height: 8, child: Obx(() => Row(
width: 8, crossAxisAlignment: CrossAxisAlignment.center,
decoration: BoxDecoration( children: [
borderRadius: BorderRadius.circular(20), Container(
color: svcStopped.value || svcStatusCode.value == 0 height: 8,
? kColorWarn width: 8,
: (svcStatusCode.value == 1 decoration: BoxDecoration(
? Color.fromARGB(255, 50, 190, 166) borderRadius: BorderRadius.circular(4),
: Color.fromARGB(255, 224, 79, 95)), color: svcStopped.value || svcStatusCode.value == 0
), ? kColorWarn
).paddingSymmetric(horizontal: 12.0); : (svcStatusCode.value == 1
if (svcStopped.value) { ? Color.fromARGB(255, 50, 190, 166)
return Row( : Color.fromARGB(255, 224, 79, 95)),
crossAxisAlignment: CrossAxisAlignment.center, ),
children: [ ).marginSymmetric(horizontal: em),
light, Text(
Text(translate("Service is not running"), style: textStyle), svcStopped.value
TextButton( ? translate("Service is not running")
onPressed: () async { : svcStatusCode.value == 0
bool checked = await bind.mainCheckSuperUserPermission(); ? translate("connecting_status")
if (checked) { : svcStatusCode.value == -1
bind.mainSetOption(key: "stop-service", value: ""); ? translate("not_ready_status")
bind.mainSetOption(key: "access-mode", value: ""); : translate('Ready'),
} style: TextStyle(fontSize: em)),
}, // stop
child: Text(translate("Start Service"), style: textStyle)) Offstage(
], offstage: !svcStopped.value,
); child: GestureDetector(
} else { onTap: () async {
if (svcStatusCode.value == 0) { bool checked =
return Row( await bind.mainCheckSuperUserPermission();
crossAxisAlignment: CrossAxisAlignment.center, if (checked) {
children: [ bind.mainSetOption(key: "stop-service", value: "");
light, bind.mainSetOption(key: "access-mode", value: "");
Text(translate("connecting_status"), style: textStyle) }
], },
); child: Text(translate("Start Service"),
} else if (svcStatusCode.value == -1) { style: TextStyle(
return Row( decoration: TextDecoration.underline,
crossAxisAlignment: CrossAxisAlignment.center, fontSize: em)))
children: [ .marginOnly(left: em),
light, ),
Text(translate("not_ready_status"), style: textStyle) // ready && public
], Offstage(
); offstage: !(!svcStopped.value &&
} svcStatusCode.value == 1 &&
} svcIsUsingPublicServer.value),
return Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
light, Text(', ', style: TextStyle(fontSize: em)),
Text(translate('Ready'), style: textStyle), InkWell(
Offstage( onTap: onUsePublicServerGuide,
offstage: !svcIsUsingPublicServer.value, child: Text(
child: Row( translate('setup_server_tip'),
crossAxisAlignment: CrossAxisAlignment.center, style: TextStyle(
children: [ decoration: TextDecoration.underline, fontSize: em),
Text(', ', style: textStyle), ),
InkWell( )
onTap: onUsePublicServerGuide, ],
child: Text( ),
translate('setup_server_tip'), )
style: TextStyle( ],
decoration: TextDecoration.underline, )),
fontSize: fontSize),
),
)
],
))
],
); );
} }

View File

@ -432,6 +432,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled(); bool locked = bind.mainIsInstalled();
final scrollController = ScrollController(); final scrollController = ScrollController();
final RxBool serviceStop = Get.find<RxBool>(tag: 'service-stop');
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -465,17 +466,15 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
} }
Widget permissions(context) { Widget permissions(context) {
bool enabled = !locked; return Obx(() => _permissions(context, serviceStop.value));
}
Widget _permissions(context, bool stopService) {
bool enabled = !locked;
return _futureBuilder(future: () async { return _futureBuilder(future: () async {
bool stopService = option2bool( return await bind.mainGetOption(key: 'access-mode');
'stop-service', await bind.mainGetOption(key: 'stop-service'));
final accessMode = await bind.mainGetOption(key: 'access-mode');
return {'stopService': stopService, 'accessMode': accessMode};
}(), hasData: (data) { }(), hasData: (data) {
var map = data! as Map<String, dynamic>; String accessMode = data! as String;
bool stopService = map['stopService'] as bool;
String accessMode = map['accessMode'] as String;
_AccessMode mode; _AccessMode mode;
if (stopService) { if (stopService) {
mode = _AccessMode.deny; mode = _AccessMode.deny;

View File

@ -742,59 +742,147 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
await bind.sessionSetImageQuality(id: widget.id, value: newValue); 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') { if (newValue == 'custom') {
final btnCancel = msgBoxButton(translate('Close'), () { final btnClose = msgBoxButton(translate('Close'), () async {
await setCustomValues();
widget.ffi.dialogManager.dismissAll(); widget.ffi.dialogManager.dismissAll();
}); });
// quality
final quality = final quality =
await bind.sessionGetCustomImageQuality(id: widget.id); await bind.sessionGetCustomImageQuality(id: widget.id);
double initValue = quality != null && quality.isNotEmpty qualityInitValue = quality != null && quality.isNotEmpty
? quality[0].toDouble() ? quality[0].toDouble()
: 50.0; : 50.0;
const minValue = 10.0; const qualityMinValue = 10.0;
const maxValue = 100.0; const qualityMaxValue = 100.0;
if (initValue < minValue) { if (qualityInitValue < qualityMinValue) {
initValue = minValue; qualityInitValue = qualityMinValue;
} }
if (initValue > maxValue) { if (qualityInitValue > qualityMaxValue) {
initValue = maxValue; qualityInitValue = qualityMaxValue;
} }
final RxDouble sliderValue = RxDouble(initValue); final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
final rxReplay = rxdart.ReplaySubject<double>(); final qualityRxReplay = rxdart.ReplaySubject<double>();
rxReplay qualityRxReplay
.throttleTime(const Duration(milliseconds: 1000), .throttleTime(const Duration(milliseconds: 1000),
trailing: true, leading: false) trailing: true, leading: false)
.listen((double v) { .listen((double v) {
() async { () async {
await bind.sessionSetCustomImageQuality( await setCustomValues(quality: v);
id: widget.id, value: v.toInt());
}(); }();
}); });
final slider = Obx(() { final qualitySlider = Obx(() => Row(
return Slider( children: [
value: sliderValue.value, Slider(
min: minValue, value: qualitySliderValue.value,
max: maxValue, min: qualityMinValue,
divisions: 90, max: qualityMaxValue,
onChanged: (double value) { divisions: 90,
sliderValue.value = value; onChanged: (double value) {
rxReplay.add(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<double>();
fpsRxReplay
.throttleTime(const Duration(milliseconds: 1000),
trailing: true, leading: false)
.listen((double v) {
() async {
await setCustomValues(fps: v);
}();
}); });
final content = Row( bool? direct;
children: [ try {
slider, direct = ConnectionTypeState.find(widget.id).direct.value ==
SizedBox( ConnectionType.strDirect;
width: 90, } catch (_) {}
child: Obx(() => Text( final fpsSlider = Offstage(
'${sliderValue.value.round()}% Bitrate', 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), style: const TextStyle(fontSize: 15),
))) );
], }))
],
),
);
final content = Column(
children: [qualitySlider, fpsSlider],
); );
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
content, [btnCancel]); content, [btnClose]);
} }
}, },
padding: padding, padding: padding,

View File

@ -480,6 +480,7 @@ message OptionMessage {
BoolOption disable_clipboard = 8; BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9; BoolOption enable_file_transfer = 9;
VideoCodecState video_codec_state = 10; VideoCodecState video_codec_state = 10;
int32 custom_fps = 11;
} }
message TestDelay { message TestDelay {

View File

@ -1092,7 +1092,12 @@ impl LoginConfigHandler {
n += 1; n += 1;
} else if q == "custom" { } else if q == "custom" {
let config = PeerConfig::load(&self.id); 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; n += 1;
} }
if self.get_toggle_option("show-remote-cursor") { if self.get_toggle_option("show-remote-cursor") {
@ -1253,6 +1258,27 @@ impl LoginConfigHandler {
res 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 { pub fn get_option(&self, k: &str) -> String {
if let Some(v) = self.config.options.get(k) { if let Some(v) = self.config.options.get(k) {
v.clone() v.clone()

View File

@ -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) { pub fn session_lock_screen(id: String) {
if let Some(session) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
session.lock_screen(); session.lock_screen();
@ -1000,6 +1006,10 @@ pub fn query_onlines(ids: Vec<String>) {
crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) 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<bool> { pub fn main_is_installed() -> SyncReturn<bool> {
SyncReturn(is_installed()) SyncReturn(is_installed())
} }

View File

@ -1316,16 +1316,25 @@ impl Connection {
if o.custom_image_quality > 0 { if o.custom_image_quality > 0 {
image_quality = o.custom_image_quality; image_quality = o.custom_image_quality;
} else { } else {
image_quality = ImageQuality::Balanced.value(); image_quality = -1;
} }
} else { } else {
image_quality = q.value(); 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 video_service::VIDEO_QOS
.lock() .lock()
.unwrap() .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 let Ok(q) = o.lock_after_session_end.enum_value() {
if q != BoolOption::NotSet { if q != BoolOption::NotSet {
self.lock_after_session_end = q == BoolOption::Yes; self.lock_after_session_end = q == BoolOption::Yes;

View File

@ -1,6 +1,8 @@
use super::*; use super::*;
use std::time::Duration; use std::time::Duration;
const FPS: u8 = 30; const FPS: u8 = 30;
const MIN_FPS: u8 = 10;
const MAX_FPS: u8 = 120;
trait Percent { trait Percent {
fn as_percent(&self) -> u32; fn as_percent(&self) -> u32;
} }
@ -23,7 +25,8 @@ pub struct VideoQoS {
current_image_quality: u32, current_image_quality: u32,
enable_abr: bool, enable_abr: bool,
pub current_delay: u32, pub current_delay: u32,
pub fps: u8, // abr pub fps: u8, // abr
pub user_fps: u8,
pub target_bitrate: u32, // abr pub target_bitrate: u32, // abr
updated: bool, updated: bool,
state: DelayState, state: DelayState,
@ -56,6 +59,7 @@ impl Default for VideoQoS {
fn default() -> Self { fn default() -> Self {
VideoQoS { VideoQoS {
fps: FPS, fps: FPS,
user_fps: FPS,
user_image_quality: ImageQuality::Balanced.as_percent(), user_image_quality: ImageQuality::Balanced.as_percent(),
current_image_quality: ImageQuality::Balanced.as_percent(), current_image_quality: ImageQuality::Balanced.as_percent(),
enable_abr: false, enable_abr: false,
@ -80,12 +84,19 @@ impl VideoQoS {
} }
pub fn spf(&mut self) -> Duration { pub fn spf(&mut self) -> Duration {
if self.fps <= 0 { if self.fps < MIN_FPS || self.fps > MAX_FPS {
self.fps = FPS; self.fps = self.base_fps();
} }
Duration::from_secs_f32(1. / (self.fps as f32)) 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 // update_network_delay periodically
// decrease the bitrate when the delay gets bigger // decrease the bitrate when the delay gets bigger
pub fn update_network_delay(&mut self, delay: u32) { pub fn update_network_delay(&mut self, delay: u32) {
@ -124,19 +135,19 @@ impl VideoQoS {
fn refresh_quality(&mut self) { fn refresh_quality(&mut self) {
match self.state { match self.state {
DelayState::Normal => { DelayState::Normal => {
self.fps = FPS; self.fps = self.base_fps();
self.current_image_quality = self.user_image_quality; self.current_image_quality = self.user_image_quality;
} }
DelayState::LowDelay => { DelayState::LowDelay => {
self.fps = FPS; self.fps = self.base_fps();
self.current_image_quality = std::cmp::min(self.user_image_quality, 50); self.current_image_quality = std::cmp::min(self.user_image_quality, 50);
} }
DelayState::HighDelay => { DelayState::HighDelay => {
self.fps = FPS / 2; self.fps = self.base_fps() / 2;
self.current_image_quality = std::cmp::min(self.user_image_quality, 25); self.current_image_quality = std::cmp::min(self.user_image_quality, 25);
} }
DelayState::Broken => { DelayState::Broken => {
self.fps = FPS / 4; self.fps = self.base_fps() / 4;
self.current_image_quality = 10; self.current_image_quality = 10;
} }
} }
@ -146,6 +157,14 @@ impl VideoQoS {
// handle image_quality change from peer // handle image_quality change from peer
pub fn update_image_quality(&mut self, image_quality: i32) { 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 _; let image_quality = Self::convert_quality(image_quality) as _;
if self.current_image_quality != image_quality { if self.current_image_quality != image_quality {
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; 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<u32> { pub fn generate_bitrate(&mut self) -> ResultType<u32> {
// https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/ // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
if self.width == 0 || self.height == 0 { if self.width == 0 || self.height == 0 {

View File

@ -134,6 +134,11 @@ impl<T: InvokeUiSession> Session<T> {
} }
} }
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 { pub fn get_remember(&self) -> bool {
self.lc.read().unwrap().remember self.lc.read().unwrap().remember
} }
@ -1181,7 +1186,12 @@ impl<T: InvokeUiSession> Interface for Session<T> {
if self.is_file_transfer() { if self.is_file_transfer() {
self.close_success(); self.close_success();
} else if !self.is_port_forward() { } else if !self.is_port_forward() {
self.msgbox("success", "Successful", "Connected, waiting for image...", ""); self.msgbox(
"success",
"Successful",
"Connected, waiting for image...",
"",
);
} }
#[cfg(windows)] #[cfg(windows)]
{ {