diff --git a/README-zh.md b/README-zh.md index af87233..e8794fa 100644 --- a/README-zh.md +++ b/README-zh.md @@ -17,7 +17,7 @@ - [x] 国际化 - [ ] 手柄按键映射 - [ ] 更好的宏 -- [ ] 通过 WebSocket 提供外部接口 +- [x] 通过 WebSocket 提供外部控制,见[外部控制](https://github.com/AkiChase/scrcpy-mask-external-control) - [ ] 帮助文档 ## 视频演示 @@ -27,6 +27,12 @@ - [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff) - [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5) +## 实现原理 + +- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?架构、通信篇 - 掘金](https://juejin.cn/post/7366799820734939199) +- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇 - 掘金](https://juejin.cn/post/7367620233140748299) +- [Scrcpy Mask 实现原理剖析,如何在前端实现王者荣耀中技能的准确释放? - 掘金](https://juejin.cn/post/7367568884198047807) + ## 截图 - 设备控制 diff --git a/README.md b/README.md index e19edfc..26f8edb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This project only implements the Scrcpy control protocol and **does not provide - [x] Internationalization (i18n) - [ ] Gamepad key mapping - [ ] Better macro support -- [ ] Provide external interface through websocket +- [x] Provide external control through websocket, see [external control](https://github.com/AkiChase/scrcpy-mask-external-control) - [ ] Help document ## Demonstration video @@ -29,6 +29,12 @@ This project only implements the Scrcpy control protocol and **does not provide - [如何用 PC 控制安卓手机打王者?只要思想不滑坡,办法总比困难多!-哔哩哔哩](https://b23.tv/dmUOpff) - [M 芯片 Mac 怎么用 Android Studio 模拟器打王者?这是 Up 耗时数个月给出的答案-哔哩哔哩](https://b23.tv/ckJgyK5) +## Implementation principle + +- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?架构、通信篇 - 掘金](https://juejin.cn/post/7366799820734939199) +- [Scrcpy Mask 实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇 - 掘金](https://juejin.cn/post/7367620233140748299) +- [Scrcpy Mask 实现原理剖析,如何在前端实现王者荣耀中技能的准确释放? - 掘金](https://juejin.cn/post/7367568884198047807) + ## Screenshot - Device control diff --git a/package.json b/package.json index 1ae15c7..792933c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "scrcpy-mask", "private": true, - "version": "0.2.0", + "version": "0.2.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c35b8ff..fed3a7b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scrcpy-mask" -version = "0.2.0" +version = "0.2.1" description = "A Tauri App" authors = ["AkiChase"] edition = "2021" @@ -16,6 +16,7 @@ tauri-plugin-store = "2.0.0-beta" serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1.0" +lazy_static = "1.4.0" tokio = { version = "1.36.0", features = ["rt-multi-thread", "net", "macros", "io-util", "time", "sync"] } tauri-plugin-process = "2.0.0-beta" tauri-plugin-shell = "2.0.0-beta" diff --git a/src-tauri/src/client.rs b/src-tauri/src/client.rs index ebb59a1..85bbc28 100644 --- a/src-tauri/src/client.rs +++ b/src-tauri/src/client.rs @@ -3,7 +3,7 @@ use std::{io::BufRead, path::PathBuf}; use crate::{ adb::{Adb, Device}, - resource::{ResHelper, ResourceName}, + resource::{ResHelper, ResourceName}, share, }; /** @@ -119,6 +119,8 @@ impl ScrcpyClient { // clear string to store new line only s.clear(); } + + *share::CLIENT_INFO.lock().unwrap() = None; println!("Scrcpy server closed"); Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 120ab6b..85eb83b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,3 +5,4 @@ pub mod control_msg; pub mod resource; pub mod scrcpy_mask_cmd; pub mod socket; +pub mod share; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b31b9ae..4347073 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,6 +5,7 @@ use scrcpy_mask::{ adb::{Adb, Device}, client::ScrcpyClient, resource::{ResHelper, ResourceName}, + share, socket::connect_socket, }; use std::{fs::read_to_string, sync::Arc}; @@ -54,6 +55,17 @@ fn start_scrcpy_server( address: String, app: tauri::AppHandle, ) -> Result<(), String> { + let mut client_info = share::CLIENT_INFO.lock().unwrap(); + if let Some(_) = &*client_info { + return Err("client already exists".to_string()); + } + + *client_info = Some(share::ClientInfo::new( + "unknow".to_string(), + id.clone(), + scid.clone(), + )); + let dir = app.path().resource_dir().unwrap().join("resource"); let version = ScrcpyClient::get_scrcpy_version(); @@ -108,6 +120,15 @@ fn start_scrcpy_server( Ok(()) } +#[tauri::command] +fn get_cur_client_info() -> Result, String> { + let client_info = share::CLIENT_INFO.lock().unwrap(); + match &*client_info { + Some(client) => Ok(Some(client.clone())), + None => Ok(None), + } +} + #[tauri::command] /// get device screen size fn get_device_screen_size(id: String, app: tauri::AppHandle) -> Result<(u32, u32), String> { @@ -217,6 +238,7 @@ async fn main() { forward_server_port, push_server_file, start_scrcpy_server, + get_cur_client_info, get_device_screen_size, adb_connect, load_default_keyconfig diff --git a/src-tauri/src/scrcpy_mask_cmd.rs b/src-tauri/src/scrcpy_mask_cmd.rs index f98c5ce..f2de2df 100644 --- a/src-tauri/src/scrcpy_mask_cmd.rs +++ b/src-tauri/src/scrcpy_mask_cmd.rs @@ -35,7 +35,7 @@ pub async fn handle_sm_cmd( // up let buf = gen_inject_key_ctrl_msg( ctrl_msg_type, - 0, // AKEY_EVENT_ACTION_DOWN + 1, // AKEY_EVENT_ACTION_UP keycode, 0, metastate, diff --git a/src-tauri/src/share.rs b/src-tauri/src/share.rs new file mode 100644 index 0000000..74173fa --- /dev/null +++ b/src-tauri/src/share.rs @@ -0,0 +1,23 @@ +use lazy_static::lazy_static; +use std::sync::Mutex; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ClientInfo { + pub device_name: String, + pub device_id: String, + pub scid: String, +} + +impl ClientInfo { + pub fn new(device_name: String, device_id: String, scid: String) -> Self { + Self { + device_name, + device_id, + scid, + } + } +} + +lazy_static! { + pub static ref CLIENT_INFO: Mutex> = Mutex::new(None); +} diff --git a/src-tauri/src/socket.rs b/src-tauri/src/socket.rs index 8a1df23..5c53396 100644 --- a/src-tauri/src/socket.rs +++ b/src-tauri/src/socket.rs @@ -13,6 +13,7 @@ use tokio::{ use crate::{ control_msg::{self, ControlMsgType}, scrcpy_mask_cmd::{self, ScrcpyMaskCmdType}, + share, }; pub async fn connect_socket( @@ -26,7 +27,7 @@ pub async fn connect_socket( .await .context("Socket connect failed")?; - println!("成功连接scrcpy-server:{:?}", client.local_addr()); + println!("connect to scrcpy-server:{:?}", client.local_addr()); let (read_half, write_half) = client.into_split(); @@ -71,6 +72,9 @@ async fn read_socket( end -= 1; } let device_name = std::str::from_utf8(&buf[..end]).unwrap(); + // update device name for share + share::CLIENT_INFO.lock().unwrap().as_mut().unwrap().device_name = device_name.to_string(); + let msg = json!({ "type": "MetaData", "deviceName": device_name, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bcdd7e5..d8a87ed 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "scrcpy-mask", - "version": "0.2.0", + "version": "0.2.1", "identifier": "com.akichase.mask", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/components/Device.vue b/src/components/Device.vue index 0965d99..0fb6115 100644 --- a/src/components/Device.vue +++ b/src/components/Device.vue @@ -17,6 +17,7 @@ import { startScrcpyServer, getDeviceScreenSize, adbConnect, + getCurClientInfo, } from "../invoke"; import { NH4, @@ -39,13 +40,13 @@ import { useMessage, NInputGroup, } from "naive-ui"; -import { CloseCircle, InformationCircle } from "@vicons/ionicons5"; -import { Refresh } from "@vicons/ionicons5"; +import { CloseCircle, InformationCircle, Refresh } from "@vicons/ionicons5"; import { UnlistenFn, listen } from "@tauri-apps/api/event"; import { Store } from "@tauri-apps/plugin-store"; import { shutdown } from "../frontcommand/scrcpyMaskCmd"; import { useGlobalStore } from "../store/global"; import { useI18n } from "vue-i18n"; +import { closeExternalControl, connectExternalControl } from "../websocket"; const { t } = useI18n(); const dialog = useDialog(); @@ -53,7 +54,8 @@ const store = useGlobalStore(); const message = useMessage(); const port = ref(27183); -const address = ref(""); +const wireless_address = ref(""); +const ws_address = ref(""); const localStore = new Store("store.bin"); @@ -86,6 +88,26 @@ onMounted(async () => { }); onActivated(async () => { + let curClientInfo = await getCurClientInfo(); + if (store.controledDevice) { + // update controledDevice if client not exists + if (!curClientInfo) { + await shutdown(); + store.controledDevice = null; + message.warning(t("pages.Device.alreadyDisconnected")); + } + } else { + // restore controledDevice if client exists + if (curClientInfo) { + message.warning(t("pages.Device.alreadyControled")); + store.controledDevice = { + scid: curClientInfo.scid, + deviceName: curClientInfo.device_name, + deviceID: curClientInfo.device_id, + }; + } + } + await refreshDevices(); }); @@ -98,7 +120,7 @@ onUnmounted(() => { const devices: Ref = ref([]); const availableDevice = computed(() => { return devices.value.filter((d) => { - return store.controledDevice?.device.id !== d.id; + return store.controledDevice?.deviceID !== d.id; }); }); const tableCols: DataTableColumns = [ @@ -178,6 +200,18 @@ function onMenuClickoutside() { } async function deviceControl() { + let curClientInfo = await getCurClientInfo(); + if (curClientInfo) { + message.warning(t("pages.Device.alreadyControled")); + store.controledDevice = { + scid: curClientInfo.scid, + deviceName: curClientInfo.device_name, + deviceID: curClientInfo.device_id, + }; + store.hideLoading(); + return; + } + if (!port.value) { port.value = 27183; } @@ -227,7 +261,7 @@ async function deviceControl() { store.controledDevice = { scid, deviceName, - device, + deviceID: device.id, }; nextTick(() => { deviceWaitForMetadataTask = null; @@ -267,15 +301,29 @@ async function refreshDevices() { } async function connectDevice() { - if (!address.value) { + if (!wireless_address.value) { message.error(t("pages.Device.inputWirelessAddress")); return; } store.showLoading(); - message.info(await adbConnect(address.value)); + message.info(await adbConnect(wireless_address.value)); await refreshDevices(); } + +function connectWS() { + if (!ws_address.value) { + message.error(t("pages.Device.inputWsAddress")); + return; + } + + store.showLoading(); + connectExternalControl(ws_address.value, message, store, t); +} + +function closeWS() { + closeExternalControl(); +} - scid: {{ store.controledDevice.scid }}
status: - {{ store.controledDevice.device.status }} + scid: {{ store.controledDevice.scid }}