Merge pull request #1220 from fufesou/flutter_desktop_remote_menus
Flutter desktop remote menus
This commit is contained in:
		
						commit
						e197c8a264
					
				| @ -194,14 +194,20 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { | ||||
|               style: TextStyle(color: MyTheme.accent)))); | ||||
| 
 | ||||
|   SmartDialog.dismiss(); | ||||
|   final buttons = [ | ||||
|   List<Widget> buttons = []; | ||||
|   if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { | ||||
|     buttons.insert( | ||||
|         0, | ||||
|         wrap(Translator.call('OK'), () { | ||||
|           SmartDialog.dismiss(); | ||||
|           backToHome(); | ||||
|     }) | ||||
|   ]; | ||||
|         })); | ||||
|   } | ||||
|   if (hasCancel == null) { | ||||
|     hasCancel = type != 'error'; | ||||
|     // hasCancel = type != 'error'; | ||||
|     hasCancel = type.indexOf("error") < 0 && | ||||
|         type.indexOf("nocancel") < 0 && | ||||
|         type != "restarting"; | ||||
|   } | ||||
|   if (hasCancel) { | ||||
|     buttons.insert( | ||||
| @ -210,6 +216,14 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { | ||||
|           SmartDialog.dismiss(); | ||||
|         })); | ||||
|   } | ||||
|   // TODO: test this button | ||||
|   if (type.indexOf("hasclose") >= 0) { | ||||
|     buttons.insert( | ||||
|         0, | ||||
|         wrap(Translator.call('Close'), () { | ||||
|           SmartDialog.dismiss(); | ||||
|         })); | ||||
|   } | ||||
|   DialogManager.show((setState, close) => CustomAlertDialog( | ||||
|       title: Text(translate(title), style: TextStyle(fontSize: 21)), | ||||
|       content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), | ||||
|  | ||||
| @ -277,8 +277,7 @@ class _RemotePageState extends State<RemotePage> | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     super.build(context); | ||||
|     Provider.of<CanvasModel>(context, listen: false).tabBarHeight = | ||||
|         super.widget.tabBarHeight; | ||||
|     _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; | ||||
|     return WillPopScope( | ||||
|         onWillPop: () async { | ||||
|           clientClose(); | ||||
| @ -605,8 +604,12 @@ class _RemotePageState extends State<RemotePage> | ||||
|           await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != | ||||
|               true) { | ||||
|         more.add(PopupMenuItem<String>( | ||||
|             child: Text(translate((_ffi.ffiModel.inputBlocked ? 'Unb' : 'B') + | ||||
|                 'lock user input')), | ||||
|             child: Consumer<FfiModel>( | ||||
|                 builder: (_context, ffiModel, _child) => () { | ||||
|                       return Text(translate( | ||||
|                           (ffiModel.inputBlocked ? 'Unb' : 'B') + | ||||
|                               'lock user input')); | ||||
|                     }()), | ||||
|             value: 'block-input')); | ||||
|       } | ||||
|     } | ||||
| @ -882,11 +885,11 @@ class ImagePainter extends CustomPainter { | ||||
|     if (image == null) return; | ||||
|     canvas.scale(scale, scale); | ||||
|     // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 | ||||
|     // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html | ||||
|     var paint = new Paint(); | ||||
|     if (scale > 1.00001) { | ||||
|       paint.filterQuality = FilterQuality.high; | ||||
|     } else if (scale < 0.99999) { | ||||
|     paint.filterQuality = FilterQuality.medium; | ||||
|     if (scale > 10.00000) { | ||||
|       paint.filterQuality = FilterQuality.high; | ||||
|     } | ||||
|     canvas.drawImage(image!, new Offset(x, y), paint); | ||||
|   } | ||||
| @ -952,7 +955,11 @@ void showOptions(String id) async { | ||||
|       more.add(getToggle( | ||||
|           id, setState, 'lock-after-session-end', 'Lock after session end')); | ||||
|       if (pi.platform == 'Windows') { | ||||
|         more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); | ||||
|         more.add(Consumer<FfiModel>( | ||||
|             builder: (_context, _ffiModel, _child) => () { | ||||
|                   return getToggle( | ||||
|                       id, setState, 'privacy-mode', 'Privacy mode'); | ||||
|                 }())); | ||||
|       } | ||||
|     } | ||||
|     var setQuality = (String? value) { | ||||
|  | ||||
| @ -173,6 +173,10 @@ class FfiModel with ChangeNotifier { | ||||
|         parent.target?.serverModel.onClientRemove(evt); | ||||
|       } else if (name == 'update_quality_status') { | ||||
|         parent.target?.qualityMonitorModel.updateQualityStatus(evt); | ||||
|       } else if (name == 'update_block_input_state') { | ||||
|         updateBlockInputState(evt); | ||||
|       } else if (name == 'update_privacy_mode') { | ||||
|         updatePrivacyMode(evt); | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
| @ -228,6 +232,10 @@ class FfiModel with ChangeNotifier { | ||||
|         parent.target?.serverModel.onClientRemove(evt); | ||||
|       } else if (name == 'update_quality_status') { | ||||
|         parent.target?.qualityMonitorModel.updateQualityStatus(evt); | ||||
|       } else if (name == 'update_block_input_state') { | ||||
|         updateBlockInputState(evt); | ||||
|       } else if (name == 'update_privacy_mode') { | ||||
|         updatePrivacyMode(evt); | ||||
|       } | ||||
|     }; | ||||
|     platformFFI.setEventCallback(cb); | ||||
| @ -331,6 +339,15 @@ class FfiModel with ChangeNotifier { | ||||
|     } | ||||
|     notifyListeners(); | ||||
|   } | ||||
| 
 | ||||
|   updateBlockInputState(Map<String, dynamic> evt) { | ||||
|     _inputBlocked = evt['input_state'] == 'on'; | ||||
|     notifyListeners(); | ||||
|   } | ||||
| 
 | ||||
|   updatePrivacyMode(Map<String, dynamic> evt) { | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ImageModel with ChangeNotifier { | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:ui'; | ||||
| 
 | ||||
| import 'package:desktop_multi_window/desktop_multi_window.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
|  | ||||
| @ -1004,6 +1004,13 @@ impl LoginConfigHandler { | ||||
|         Some(msg_out) | ||||
|     } | ||||
| 
 | ||||
|     /// Get [`PeerConfig`] of the current [`LoginConfigHandler`].
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     pub fn get_config(&mut self) -> &mut PeerConfig { | ||||
|         &mut self.config | ||||
|     } | ||||
| 
 | ||||
|     /// Get [`OptionMessage`] of the current [`LoginConfigHandler`].
 | ||||
|     /// Return `None` if there's no option, for example, when the session is only for file transfer.
 | ||||
|     ///
 | ||||
|  | ||||
| @ -104,6 +104,19 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub async fn send_opts_after_login( | ||||
|     config: &crate::client::LoginConfigHandler, | ||||
|     peer: &mut hbb_common::tcp::FramedStream, | ||||
| ) { | ||||
|     if let Some(opts) = config.get_option_message_after_login() { | ||||
|         let mut misc = Misc::new(); | ||||
|         misc.set_option(opts); | ||||
|         let mut msg_out = Message::new(); | ||||
|         msg_out.set_misc(misc); | ||||
|         allow_err!(peer.send(&msg_out).await); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(feature = "use_rubato")] | ||||
| pub fn resample_channels( | ||||
|     data: &[f32], | ||||
|  | ||||
							
								
								
									
										194
									
								
								src/flutter.rs
									
									
									
									
									
								
							
							
						
						
									
										194
									
								
								src/flutter.rs
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| use std::{ | ||||
|     collections::{HashMap, VecDeque}, | ||||
|     sync::{ | ||||
|         atomic::{AtomicUsize, Ordering}, | ||||
|         atomic::{AtomicBool, AtomicUsize, Ordering}, | ||||
|         Arc, Mutex, RwLock, | ||||
|     }, | ||||
| }; | ||||
| @ -31,7 +31,10 @@ use hbb_common::{ | ||||
|     Stream, | ||||
| }; | ||||
| 
 | ||||
| use crate::common::make_fd_to_json; | ||||
| use crate::common::{ | ||||
|     self, check_clipboard, make_fd_to_json, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, | ||||
| }; | ||||
| 
 | ||||
| use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; | ||||
| 
 | ||||
| pub(super) const APP_TYPE_MAIN: &str = "main"; | ||||
| @ -44,6 +47,9 @@ lazy_static::lazy_static! { | ||||
|     pub static ref GLOBAL_EVENT_STREAM: RwLock<HashMap<String, StreamSink<String>>> = Default::default(); // rust to dart event channel
 | ||||
| } | ||||
| 
 | ||||
| static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); | ||||
| static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); | ||||
| 
 | ||||
| // pub fn get_session<'a>(id: &str) -> Option<&'a Session> {
 | ||||
| //     SESSIONS.read().unwrap().get(id)
 | ||||
| // }
 | ||||
| @ -70,7 +76,7 @@ impl Session { | ||||
|         // TODO close
 | ||||
|         // Self::close();
 | ||||
|         let events2ui = Arc::new(RwLock::new(events2ui)); | ||||
|         let mut session = Session { | ||||
|         let session = Session { | ||||
|             id: session_id.clone(), | ||||
|             sender: Default::default(), | ||||
|             lc: Default::default(), | ||||
| @ -657,6 +663,45 @@ struct Connection { | ||||
| } | ||||
| 
 | ||||
| impl Connection { | ||||
|     // TODO: Similar to remote::start_clipboard
 | ||||
|     // merge the code
 | ||||
|     fn start_clipboard( | ||||
|         tx_protobuf: mpsc::UnboundedSender<Data>, | ||||
|         lc: Arc<RwLock<LoginConfigHandler>>, | ||||
|     ) -> Option<std::sync::mpsc::Sender<()>> { | ||||
|         let (tx, rx) = std::sync::mpsc::channel(); | ||||
|         match ClipboardContext::new() { | ||||
|             Ok(mut ctx) => { | ||||
|                 let old_clipboard: Arc<Mutex<String>> = Default::default(); | ||||
|                 // ignore clipboard update before service start
 | ||||
|                 check_clipboard(&mut ctx, Some(&old_clipboard)); | ||||
|                 std::thread::spawn(move || loop { | ||||
|                     std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); | ||||
|                     match rx.try_recv() { | ||||
|                         Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { | ||||
|                             log::debug!("Exit clipboard service of client"); | ||||
|                             break; | ||||
|                         } | ||||
|                         _ => {} | ||||
|                     } | ||||
|                     if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) | ||||
|                         || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) | ||||
|                         || lc.read().unwrap().disable_clipboard | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|                     if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { | ||||
|                         tx_protobuf.send(Data::Message(msg)).ok(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 log::error!("Failed to start clipboard service of client: {}", err); | ||||
|             } | ||||
|         } | ||||
|         Some(tx) | ||||
|     } | ||||
| 
 | ||||
|     /// Create a new connection.
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
| @ -667,6 +712,10 @@ impl Connection { | ||||
|     async fn start(session: Session, is_file_transfer: bool) { | ||||
|         let mut last_recv_time = Instant::now(); | ||||
|         let (sender, mut receiver) = mpsc::unbounded_channel::<Data>(); | ||||
|         let mut stop_clipboard = None; | ||||
|         if !is_file_transfer { | ||||
|             stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); | ||||
|         } | ||||
|         *session.sender.write().unwrap() = Some(sender); | ||||
|         let conn_type = if is_file_transfer { | ||||
|             session.lc.write().unwrap().is_file_transfer = true; | ||||
| @ -695,6 +744,9 @@ impl Connection { | ||||
| 
 | ||||
|         match Client::start(&session.id, &key, &token, conn_type).await { | ||||
|             Ok((mut peer, direct)) => { | ||||
|                 SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); | ||||
|                 SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); | ||||
| 
 | ||||
|                 session.push_event( | ||||
|                     "connection_ready", | ||||
|                     vec![ | ||||
| @ -774,6 +826,12 @@ impl Connection { | ||||
|                 session.msgbox("error", "Connection Error", &err.to_string()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if let Some(stop) = stop_clipboard { | ||||
|             stop.send(()).ok(); | ||||
|         } | ||||
|         SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); | ||||
|         SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); | ||||
|     } | ||||
| 
 | ||||
|     /// Handle message from peer.
 | ||||
| @ -786,6 +844,7 @@ impl Connection { | ||||
|                 Some(message::Union::VideoFrame(vf)) => { | ||||
|                     if !self.first_frame { | ||||
|                         self.first_frame = true; | ||||
|                         common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; | ||||
|                     } | ||||
|                     let incomming_format = CodecFormat::from(&vf); | ||||
|                     if self.video_format != incomming_format { | ||||
| @ -1027,6 +1086,11 @@ impl Connection { | ||||
|                         self.session.msgbox("error", "Connection Error", &c); | ||||
|                         return false; | ||||
|                     } | ||||
|                     Some(misc::Union::BackNotification(notification)) => { | ||||
|                         if !self.handle_back_notification(notification).await { | ||||
|                             return false; | ||||
|                         } | ||||
|                     } | ||||
|                     _ => {} | ||||
|                 }, | ||||
|                 Some(message::Union::TestDelay(t)) => { | ||||
| @ -1051,6 +1115,130 @@ impl Connection { | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { | ||||
|         match notification.union { | ||||
|             Some(back_notification::Union::BlockInputState(state)) => { | ||||
|                 self.handle_back_msg_block_input( | ||||
|                     state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), | ||||
|                 ) | ||||
|                 .await; | ||||
|             } | ||||
|             Some(back_notification::Union::PrivacyModeState(state)) => { | ||||
|                 if !self | ||||
|                     .handle_back_msg_privacy_mode( | ||||
|                         state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), | ||||
|                     ) | ||||
|                     .await | ||||
|                 { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|             _ => {} | ||||
|         } | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     #[inline(always)] | ||||
|     fn update_block_input_state(&mut self, on: bool) { | ||||
|         self.session.push_event( | ||||
|             "update_block_input_state", | ||||
|             [("input_state", if on { "on" } else { "off" })].into(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { | ||||
|         match state { | ||||
|             back_notification::BlockInputState::BlkOnSucceeded => { | ||||
|                 self.update_block_input_state(true); | ||||
|             } | ||||
|             back_notification::BlockInputState::BlkOnFailed => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Block user input", "Failed"); | ||||
|                 self.update_block_input_state(false); | ||||
|             } | ||||
|             back_notification::BlockInputState::BlkOffSucceeded => { | ||||
|                 self.update_block_input_state(false); | ||||
|             } | ||||
|             back_notification::BlockInputState::BlkOffFailed => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Unblock user input", "Failed"); | ||||
|             } | ||||
|             _ => {} | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[inline(always)] | ||||
|     fn update_privacy_mode(&mut self, on: bool) { | ||||
|         let mut config = self.session.load_config(); | ||||
|         config.privacy_mode = on; | ||||
|         self.session.save_config(&config); | ||||
|         self.session.lc.write().unwrap().get_config().privacy_mode = on; | ||||
|         self.session.push_event("update_privacy_mode", [].into()); | ||||
|     } | ||||
| 
 | ||||
|     async fn handle_back_msg_privacy_mode( | ||||
|         &mut self, | ||||
|         state: back_notification::PrivacyModeState, | ||||
|     ) -> bool { | ||||
|         match state { | ||||
|             back_notification::PrivacyModeState::PrvOnByOther => { | ||||
|                 self.session.msgbox( | ||||
|                     "error", | ||||
|                     "Connecting...", | ||||
|                     "Someone turns on privacy mode, exit", | ||||
|                 ); | ||||
|                 return false; | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvNotSupported => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Unsupported"); | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOnSucceeded => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); | ||||
|                 self.update_privacy_mode(true); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOnFailedDenied => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Peer denied"); | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOnFailedPlugin => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Please install plugins"); | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOnFailed => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Failed"); | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOffSucceeded => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOffByPeer => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Peer exit"); | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOffFailed => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Failed to turn off"); | ||||
|             } | ||||
|             back_notification::PrivacyModeState::PrvOffUnknown => { | ||||
|                 self.session | ||||
|                     .msgbox("custom-error", "Privacy mode", "Turned off"); | ||||
|                 // log::error!("Privacy mode is turned off with unknown reason");
 | ||||
|                 self.update_privacy_mode(false); | ||||
|             } | ||||
|             _ => {} | ||||
|         } | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { | ||||
|         match data { | ||||
|             Data::Close => { | ||||
|  | ||||
| @ -25,8 +25,12 @@ use clipboard::{ | ||||
| use enigo::{self, Enigo, KeyboardControllable}; | ||||
| use hbb_common::{ | ||||
|     allow_err, | ||||
|     config::{Config, LocalConfig, PeerConfig}, | ||||
|     fs, log, | ||||
|     config::{Config, LocalConfig, PeerConfig, TransferSerde}, | ||||
|     fs::{ | ||||
|         self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, | ||||
|         DigestCheckResult, RemoveJobMeta, TransferJobMeta, | ||||
|     }, | ||||
|     get_version_number, log, | ||||
|     message_proto::{permission_info::Permission, *}, | ||||
|     protobuf::Message as _, | ||||
|     rendezvous_proto::ConnType, | ||||
| @ -38,14 +42,6 @@ use hbb_common::{ | ||||
|     }, | ||||
|     Stream, | ||||
| }; | ||||
| use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; | ||||
| use hbb_common::{ | ||||
|     fs::{ | ||||
|         can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, | ||||
|         RemoveJobMeta, | ||||
|     }, | ||||
|     get_version_number, | ||||
| }; | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| use crate::clipboard_file::*; | ||||
| @ -2071,22 +2067,6 @@ impl Remote { | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     async fn send_opts_after_login(&self, peer: &mut Stream) { | ||||
|         if let Some(opts) = self | ||||
|         .handler | ||||
|         .lc | ||||
|         .read() | ||||
|         .unwrap() | ||||
|         .get_option_message_after_login() | ||||
|     { | ||||
|         let mut misc = Misc::new(); | ||||
|         misc.set_option(opts); | ||||
|         let mut msg_out = Message::new(); | ||||
|         msg_out.set_misc(misc); | ||||
|         allow_err!(peer.send(&msg_out).await); | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { | ||||
|         if let Ok(msg_in) = Message::parse_from_bytes(&data) { | ||||
|             match msg_in.union { | ||||
| @ -2095,7 +2075,7 @@ impl Remote { | ||||
|                         self.first_frame = true; | ||||
|                         self.handler.call2("closeSuccess", &make_args!()); | ||||
|                         self.handler.call("adaptSize", &make_args!()); | ||||
|                         self.send_opts_after_login(peer).await; | ||||
|                         common::send_opts_after_login(&self.handler.lc.read().unwrap(), peer).await; | ||||
|                     } | ||||
|                     let incomming_format = CodecFormat::from(&vf); | ||||
|                     if self.video_format != incomming_format { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user