add mobile quality monitor

This commit is contained in:
csf 2022-07-30 21:12:08 +08:00
parent 86cc71f4d2
commit e53119a01a
5 changed files with 272 additions and 116 deletions

View File

@ -162,6 +162,8 @@ class FfiModel with ChangeNotifier {
FFI.serverModel.onClientAuthorized(evt); FFI.serverModel.onClientAuthorized(evt);
} else if (name == 'on_client_remove') { } else if (name == 'on_client_remove') {
FFI.serverModel.onClientRemove(evt); FFI.serverModel.onClientRemove(evt);
} else if (name == 'update_quality_status') {
FFI.qualityMonitorModel.updateQualityStatus(evt);
} }
}; };
PlatformFFI.setEventCallback(cb); PlatformFFI.setEventCallback(cb);
@ -655,6 +657,44 @@ class CursorModel with ChangeNotifier {
} }
} }
class QualityMonitorData {
String? speed;
String? fps;
String? delay;
String? targetBitrate;
String? codecFormat;
}
class QualityMonitorModel with ChangeNotifier {
var _show = FFI.getByName('toggle_option', 'show-quality-monitor') == 'true';
final _data = QualityMonitorData();
bool get show => _show;
QualityMonitorData get data => _data;
checkShowQualityMonitor() {
final show =
FFI.getByName('toggle_option', 'show-quality-monitor') == 'true';
if (_show != show) {
_show = show;
notifyListeners();
}
}
updateQualityStatus(Map<String, dynamic> evt) {
try {
if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"];
if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"];
if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"];
if ((evt["target_bitrate"] as String).isNotEmpty)
_data.targetBitrate = evt["target_bitrate"];
if ((evt["codec_format"] as String).isNotEmpty)
_data.codecFormat = evt["codec_format"];
notifyListeners();
} catch (e) {}
}
}
enum MouseButtons { left, right, wheel } enum MouseButtons { left, right, wheel }
extension ToString on MouseButtons { extension ToString on MouseButtons {
@ -684,6 +724,7 @@ class FFI {
static final serverModel = ServerModel(); static final serverModel = ServerModel();
static final chatModel = ChatModel(); static final chatModel = ChatModel();
static final fileModel = FileModel(); static final fileModel = FileModel();
static final qualityMonitorModel = QualityMonitorModel();
static String getId() { static String getId() {
return getByName('remote_id'); return getByName('remote_id');

View File

@ -592,6 +592,7 @@ class _RemotePageState extends State<RemotePage> {
child: Stack(children: [ child: Stack(children: [
ImagePaint(), ImagePaint(),
CursorPaint(), CursorPaint(),
QualityMonitor(),
getHelpTools(), getHelpTools(),
SizedBox( SizedBox(
width: 0, width: 0,
@ -948,6 +949,47 @@ class ImagePainter extends CustomPainter {
} }
} }
class QualityMonitor extends StatelessWidget {
@override
Widget build(BuildContext context) => ChangeNotifierProvider.value(
value: FFI.qualityMonitorModel,
child: Consumer<QualityMonitorModel>(
builder: (context, qualityMonitorModel, child) => Positioned(
top: 10,
right: 10,
child: qualityMonitorModel.show
? Container(
padding: EdgeInsets.all(8),
color: MyTheme.canvasColor.withAlpha(120),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Speed: ${qualityMonitorModel.data.speed}",
style: TextStyle(color: MyTheme.grayBg),
),
Text(
"FPS: ${qualityMonitorModel.data.fps}",
style: TextStyle(color: MyTheme.grayBg),
),
Text(
"Delay: ${qualityMonitorModel.data.delay} ms",
style: TextStyle(color: MyTheme.grayBg),
),
Text(
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate}kb",
style: TextStyle(color: MyTheme.grayBg),
),
Text(
"Codec: ${qualityMonitorModel.data.codecFormat}",
style: TextStyle(color: MyTheme.grayBg),
),
],
),
)
: SizedBox.shrink())));
}
CheckboxListTile getToggle( CheckboxListTile getToggle(
void Function(void Function()) setState, option, name) { void Function(void Function()) setState, option, name) {
return CheckboxListTile( return CheckboxListTile(
@ -956,6 +998,9 @@ CheckboxListTile getToggle(
setState(() { setState(() {
FFI.setByName('toggle_option', option); FFI.setByName('toggle_option', option);
}); });
if (option == "show-quality-monitor") {
FFI.qualityMonitorModel.checkShowQualityMonitor();
}
}, },
dense: true, dense: true,
title: Text(translate(name))); title: Text(translate(name)));

View File

@ -3,7 +3,10 @@ use std::{
time::Instant, time::Instant,
}; };
use hbb_common::{log, message_proto::{VideoFrame, video_frame}}; use hbb_common::{
log,
message_proto::{video_frame, VideoFrame},
};
const MAX_LATENCY: i64 = 500; const MAX_LATENCY: i64 = 500;
const MIN_LATENCY: i64 = 100; const MIN_LATENCY: i64 = 100;
@ -87,3 +90,12 @@ impl ToString for CodecFormat {
} }
} }
} }
#[derive(Debug, Default)]
pub struct QualityStatus {
pub speed: Option<String>,
pub fps: Option<i32>,
pub delay: Option<i32>,
pub target_bitrate: Option<i32>,
pub codec_format: Option<CodecFormat>,
}

View File

@ -1,12 +1,16 @@
use crate::client::*; use crate::client::*;
use crate::common::{make_fd_to_json}; use crate::common::make_fd_to_json;
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
compress::decompress, compress::decompress,
config::{Config, LocalConfig}, config::{Config, LocalConfig},
fs, log, fs,
fs::{can_enable_overwrite_detection, new_send_confirm, DigestCheckResult, get_string, transform_windows_path}, fs::{
can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path,
DigestCheckResult,
},
log,
message_proto::*, message_proto::*,
protobuf::Message as _, protobuf::Message as _,
rendezvous_proto::ConnType, rendezvous_proto::ConnType,
@ -17,6 +21,7 @@ use hbb_common::{
}, },
Stream, Stream,
}; };
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
sync::{Arc, Mutex, RwLock}, sync::{Arc, Mutex, RwLock},
@ -397,6 +402,26 @@ impl Session {
log::debug!("{:?}", msg_out); log::debug!("{:?}", msg_out);
self.send_msg(msg_out); self.send_msg(msg_out);
} }
fn update_quality_status(&self, status: QualityStatus) {
const NULL: String = String::new();
self.push_event(
"update_quality_status",
vec![
("speed", &status.speed.map_or(NULL, |it| it)),
("fps", &status.fps.map_or(NULL, |it| it.to_string())),
("delay", &status.delay.map_or(NULL, |it| it.to_string())),
(
"target_bitrate",
&status.target_bitrate.map_or(NULL, |it| it.to_string()),
),
(
"codec_format",
&status.codec_format.map_or(NULL, |it| it.to_string()),
),
],
);
}
} }
impl FileManager for Session {} impl FileManager for Session {}
@ -438,7 +463,11 @@ impl Interface for Session {
if lc.is_file_transfer { if lc.is_file_transfer {
if pi.username.is_empty() { if pi.username.is_empty() {
self.msgbox("error", "Error", "No active console user logged on, please connect and logon first."); self.msgbox(
"error",
"Error",
"No active console user logged on, please connect and logon first.",
);
return; return;
} }
} else { } else {
@ -487,7 +516,14 @@ impl Interface for Session {
} }
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) {
handle_test_delay(t, peer).await; if !t.from_client {
self.update_quality_status(QualityStatus {
delay: Some(t.last_delay as _),
target_bitrate: Some(t.target_bitrate as _),
..Default::default()
});
handle_test_delay(t, peer).await;
}
} }
} }
@ -502,6 +538,9 @@ struct Connection {
write_jobs: Vec<fs::TransferJob>, write_jobs: Vec<fs::TransferJob>,
timer: Interval, timer: Interval,
last_update_jobs_status: (Instant, HashMap<i32, u64>), last_update_jobs_status: (Instant, HashMap<i32, u64>),
data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>,
video_format: CodecFormat,
} }
impl Connection { impl Connection {
@ -528,6 +567,9 @@ impl Connection {
write_jobs: Vec::new(), write_jobs: Vec::new(),
timer: time::interval(SEC30), timer: time::interval(SEC30),
last_update_jobs_status: (Instant::now(), Default::default()), last_update_jobs_status: (Instant::now(), Default::default()),
data_count: Arc::new(AtomicUsize::new(0)),
frame_count: Arc::new(AtomicUsize::new(0)),
video_format: CodecFormat::Unknown,
}; };
let key = Config::get_option("key"); let key = Config::get_option("key");
let token = Config::get_option("access_token"); let token = Config::get_option("access_token");
@ -541,6 +583,9 @@ impl Connection {
("direct", &direct.to_string()), ("direct", &direct.to_string()),
], ],
); );
let mut status_timer = time::interval(Duration::new(1, 0));
loop { loop {
tokio::select! { tokio::select! {
res = peer.next() => { res = peer.next() => {
@ -553,6 +598,7 @@ impl Connection {
} }
Ok(ref bytes) => { Ok(ref bytes) => {
last_recv_time = Instant::now(); last_recv_time = Instant::now();
conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed);
if !conn.handle_msg_from_peer(bytes, &mut peer).await { if !conn.handle_msg_from_peer(bytes, &mut peer).await {
break break
} }
@ -586,6 +632,16 @@ impl Connection {
conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); conn.timer = time::interval_at(Instant::now() + SEC30, SEC30);
} }
} }
_ = status_timer.tick() => {
let speed = conn.data_count.swap(0, Ordering::Relaxed);
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _;
conn.session.update_quality_status(QualityStatus {
speed:Some(speed),
fps:Some(fps),
..Default::default()
});
}
} }
} }
log::debug!("Exit io_loop of id={}", session.id); log::debug!("Exit io_loop of id={}", session.id);
@ -603,10 +659,19 @@ impl Connection {
if !self.first_frame { if !self.first_frame {
self.first_frame = true; self.first_frame = true;
} }
let incomming_format = CodecFormat::from(&vf);
if self.video_format != incomming_format {
self.video_format = incomming_format.clone();
self.session.update_quality_status(QualityStatus {
codec_format: Some(incomming_format),
..Default::default()
})
};
if let (Ok(true), Some(s)) = ( if let (Ok(true), Some(s)) = (
self.video_handler.handle_frame(vf), self.video_handler.handle_frame(vf),
RGBA_STREAM.read().unwrap().as_ref(), RGBA_STREAM.read().unwrap().as_ref(),
) { ) {
self.frame_count.fetch_add(1, Ordering::Relaxed);
s.add(ZeroCopyBuffer(self.video_handler.rgb.clone())); s.add(ZeroCopyBuffer(self.video_handler.rgb.clone()));
} }
} }
@ -664,113 +729,114 @@ impl Connection {
vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())],
); );
} }
Some(message::Union::FileResponse(fr)) => match fr.union { Some(message::Union::FileResponse(fr)) => {
Some(file_response::Union::Dir(fd)) => { match fr.union {
let mut entries = fd.entries.to_vec(); Some(file_response::Union::Dir(fd)) => {
if self.session.peer_platform() == "Windows" { let mut entries = fd.entries.to_vec();
fs::transform_windows_path(&mut entries); if self.session.peer_platform() == "Windows" {
} fs::transform_windows_path(&mut entries);
let id = fd.id; }
self.session.push_event( let id = fd.id;
"file_dir", self.session.push_event(
vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], "file_dir",
); vec![("value", &make_fd_to_json(fd)), ("is_local", "false")],
if let Some(job) = fs::get_job(id, &mut self.write_jobs) { );
job.set_files(entries); if let Some(job) = fs::get_job(id, &mut self.write_jobs) {
} job.set_files(entries);
}
Some(file_response::Union::Block(block)) => {
if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) {
if let Err(_err) = job.write(block, None).await {
// to-do: add "skip" for writing job
} }
self.update_jobs_status();
} }
} Some(file_response::Union::Block(block)) => {
Some(file_response::Union::Done(d)) => { if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) {
if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { if let Err(_err) = job.write(block, None).await {
job.modify_time(); // to-do: add "skip" for writing job
fs::remove_job(d.id, &mut self.write_jobs); }
self.update_jobs_status();
}
} }
self.handle_job_status(d.id, d.file_num, None); Some(file_response::Union::Done(d)) => {
} if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) {
Some(file_response::Union::Error(e)) => { job.modify_time();
self.handle_job_status(e.id, e.file_num, Some(e.error)); fs::remove_job(d.id, &mut self.write_jobs);
} }
Some(file_response::Union::Digest(digest)) => { self.handle_job_status(d.id, d.file_num, None);
if digest.is_upload { }
if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { Some(file_response::Union::Error(e)) => {
if let Some(file) = job.files().get(digest.file_num as usize) { self.handle_job_status(e.id, e.file_num, Some(e.error));
let read_path = get_string(&job.join(&file.name)); }
let overwrite_strategy = job.default_overwrite_strategy(); Some(file_response::Union::Digest(digest)) => {
if let Some(overwrite) = overwrite_strategy { if digest.is_upload {
let req = FileTransferSendConfirmRequest { if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) {
id: digest.id, if let Some(file) = job.files().get(digest.file_num as usize) {
file_num: digest.file_num, let read_path = get_string(&job.join(&file.name));
union: Some(if overwrite { let overwrite_strategy = job.default_overwrite_strategy();
file_transfer_send_confirm_request::Union::OffsetBlk(0) if let Some(overwrite) = overwrite_strategy {
} else { let req = FileTransferSendConfirmRequest {
file_transfer_send_confirm_request::Union::Skip( id: digest.id,
true, file_num: digest.file_num,
) union: Some(if overwrite {
}), file_transfer_send_confirm_request::Union::OffsetBlk(0)
..Default::default() } else {
}; file_transfer_send_confirm_request::Union::Skip(
job.confirm(&req); true,
let msg = new_send_confirm(req); )
allow_err!(peer.send(&msg).await); }),
} else { ..Default::default()
self.handle_override_file_confirm( };
digest.id, job.confirm(&req);
digest.file_num, let msg = new_send_confirm(req);
read_path, allow_err!(peer.send(&msg).await);
true, } else {
); self.handle_override_file_confirm(
digest.id,
digest.file_num,
read_path,
true,
);
}
} }
} }
} } else {
} else { if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) {
if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) {
if let Some(file) = job.files().get(digest.file_num as usize) { let write_path = get_string(&job.join(&file.name));
let write_path = get_string(&job.join(&file.name)); let overwrite_strategy = job.default_overwrite_strategy();
let overwrite_strategy = job.default_overwrite_strategy(); match fs::is_write_need_confirmation(&write_path, &digest) {
match fs::is_write_need_confirmation(&write_path, &digest) { Ok(res) => match res {
Ok(res) => match res { DigestCheckResult::IsSame => {
DigestCheckResult::IsSame => { let msg= new_send_confirm(FileTransferSendConfirmRequest {
let msg= new_send_confirm(FileTransferSendConfirmRequest {
id: digest.id, id: digest.id,
file_num: digest.file_num, file_num: digest.file_num,
union: Some(file_transfer_send_confirm_request::Union::Skip(true)), union: Some(file_transfer_send_confirm_request::Union::Skip(true)),
..Default::default() ..Default::default()
}); });
self.session.send_msg(msg);
}
DigestCheckResult::NeedConfirm(digest) => {
if let Some(overwrite) = overwrite_strategy {
let msg = new_send_confirm(
FileTransferSendConfirmRequest {
id: digest.id,
file_num: digest.file_num,
union: Some(if overwrite {
file_transfer_send_confirm_request::Union::OffsetBlk(0)
} else {
file_transfer_send_confirm_request::Union::Skip(true)
}),
..Default::default()
},
);
self.session.send_msg(msg); self.session.send_msg(msg);
} else {
self.handle_override_file_confirm(
digest.id,
digest.file_num,
write_path.to_string(),
false,
);
} }
} DigestCheckResult::NeedConfirm(digest) => {
DigestCheckResult::NoSuchFile => { if let Some(overwrite) = overwrite_strategy {
let msg = new_send_confirm( let msg = new_send_confirm(
FileTransferSendConfirmRequest {
id: digest.id,
file_num: digest.file_num,
union: Some(if overwrite {
file_transfer_send_confirm_request::Union::OffsetBlk(0)
} else {
file_transfer_send_confirm_request::Union::Skip(true)
}),
..Default::default()
},
);
self.session.send_msg(msg);
} else {
self.handle_override_file_confirm(
digest.id,
digest.file_num,
write_path.to_string(),
false,
);
}
}
DigestCheckResult::NoSuchFile => {
let msg = new_send_confirm(
FileTransferSendConfirmRequest { FileTransferSendConfirmRequest {
id: digest.id, id: digest.id,
file_num: digest.file_num, file_num: digest.file_num,
@ -778,19 +844,20 @@ impl Connection {
..Default::default() ..Default::default()
}, },
); );
self.session.send_msg(msg); self.session.send_msg(msg);
}
},
Err(err) => {
println!("error recving digest: {}", err);
} }
},
Err(err) => {
println!("error recving digest: {}", err);
} }
} }
} }
} }
} }
_ => {}
} }
_ => {} }
},
Some(message::Union::Misc(misc)) => match misc.union { Some(message::Union::Misc(misc)) => match misc.union {
Some(misc::Union::AudioFormat(f)) => { Some(misc::Union::AudioFormat(f)) => {
self.audio_handler.handle_format(f); // self.audio_handler.handle_format(f); //

View File

@ -239,15 +239,6 @@ impl sciter::EventHandler for Handler {
} }
} }
#[derive(Debug, Default)]
struct QualityStatus {
speed: Option<String>,
fps: Option<i32>,
delay: Option<i32>,
target_bitrate: Option<i32>,
codec_format: Option<CodecFormat>,
}
impl Handler { impl Handler {
pub fn new(cmd: String, id: String, password: String, args: Vec<String>) -> Self { pub fn new(cmd: String, id: String, password: String, args: Vec<String>) -> Self {
let me = Self { let me = Self {