fix(harmony): await device agents and rebuild refused video forwards
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"id": "dev.trove.harmony-connector",
|
"id": "dev.trove.harmony-connector",
|
||||||
"name": "Harmony Connector",
|
"name": "Harmony Connector",
|
||||||
"description": "连接鸿蒙设备,收发完整文件夹、管理应用,在独立窗口中投屏并通过键鼠操作。",
|
"description": "连接鸿蒙设备,收发完整文件夹、管理应用,在独立窗口中投屏并通过键鼠操作。",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"engine": { "trove": ">=0.1.0 <1.0.0" },
|
"engine": { "trove": ">=0.1.0 <1.0.0" },
|
||||||
"icon": "assets/icon.svg",
|
"icon": "assets/icon.svg",
|
||||||
"ui": {
|
"ui": {
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ The native viewer additionally uses the unmodified Huawei components distributed
|
|||||||
in `xdevice-devicetest` 26.0.0.500 (Hypium 26):
|
in `xdevice-devicetest` 26.0.0.500 (Hypium 26):
|
||||||
|
|
||||||
- `devicetest/res/prototype/native/uitest_agent_v1.2.3.so` for touch control.
|
- `devicetest/res/prototype/native/uitest_agent_v1.2.3.so` for touch control.
|
||||||
- `devicetest/res/recorder/libscrcpy_server_unix_6.6-20260629.z.so` for H.264 video.
|
- `devicetest/res/recorder/libscrcpy_server_unix_*.z.so` for H.264 video:
|
||||||
|
6.6-20260629, 6.6-20260418, 6.5-20260313, 6.4-20260113 and 6.3.1-20260113.
|
||||||
|
These original official variants are tried in that order when a device cannot
|
||||||
|
start the previous variant; all use a device Unix socket.
|
||||||
|
|
||||||
Their wheel license is preserved in `hypium/LICENSE-26`; file identities and
|
Their wheel license is preserved in `hypium/LICENSE-26`; file identities and
|
||||||
SHA-256 checksums are in `hypium/provenance-26.json`. Upstream copyright belongs
|
SHA-256 checksums are in `hypium/provenance-26.json`. Upstream copyright belongs
|
||||||
|
|||||||
+1
-1
@@ -259,7 +259,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "harmony-connector"
|
name = "harmony-connector"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "harmony-connector"
|
name = "harmony-connector"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod files;
|
mod files;
|
||||||
mod screen;
|
mod screen;
|
||||||
mod screen_agent;
|
mod screen_agent;
|
||||||
|
mod screen_daemon;
|
||||||
mod screen_input;
|
mod screen_input;
|
||||||
mod screen_lease;
|
mod screen_lease;
|
||||||
mod screen_protocol;
|
mod screen_protocol;
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ impl Rpc {
|
|||||||
Ok((dimension("x")?, dimension("y")?))
|
Ok((dimension("x")?, dimension("y")?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(super) fn existing_agents(tool: &Tool, target: &str) -> Result<Vec<(String, String)>> {
|
pub(super) fn processes(tool: &Tool, target: &str) -> Result<Vec<(String, String)>> {
|
||||||
let pids = tool.run_output(&["-t", target, "shell", "pidof uitest xdevice_scrcpy"], 2)?;
|
let pids = tool.run_output(&["-t", target, "shell", "pidof uitest xdevice_scrcpy"], 2)?;
|
||||||
let mut owned = Vec::new();
|
let mut result = Vec::new();
|
||||||
for pid in pids.split_whitespace() {
|
for pid in pids.split_whitespace() {
|
||||||
if !pid.bytes().all(|b| b.is_ascii_digit()) {
|
if !pid.bytes().all(|b| b.is_ascii_digit()) {
|
||||||
return Err(fault("无法检查设备调试会话。"));
|
return Err(fault("无法检查设备调试会话。"));
|
||||||
@@ -149,9 +149,15 @@ pub(super) fn existing_agents(tool: &Tool, target: &str) -> Result<Vec<(String,
|
|||||||
&["-t", target, "shell", &format!("cat /proc/{pid}/cmdline")],
|
&["-t", target, "shell", &format!("cat /proc/{pid}/cmdline")],
|
||||||
2,
|
2,
|
||||||
)?;
|
)?;
|
||||||
if cmdline.contains("No such file") {
|
if !cmdline.contains("No such file") {
|
||||||
continue;
|
result.push((pid.to_owned(), cmdline));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
pub(super) fn existing_agents(tool: &Tool, target: &str) -> Result<Vec<(String, String)>> {
|
||||||
|
let mut owned = Vec::new();
|
||||||
|
for (pid, cmdline) in processes(tool, target)? {
|
||||||
let args: Vec<_> = cmdline.split('\0').filter(|s| !s.is_empty()).collect();
|
let args: Vec<_> = cmdline.split('\0').filter(|s| !s.is_empty()).collect();
|
||||||
let is_uitest = args
|
let is_uitest = args
|
||||||
.first()
|
.first()
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
//! Device-agent startup is complete only when its process and Unix socket exist.
|
||||||
|
//! Candidate versions are official ABI variants, not HarmonyOS product versions.
|
||||||
|
use super::screen_agent::{self, Rpc};
|
||||||
|
use super::screen_lease::{self, Lease, SOCKET};
|
||||||
|
use super::*;
|
||||||
|
use std::{io::Write, net::TcpListener, time::Instant};
|
||||||
|
|
||||||
|
const CONTROL: &[u8] =
|
||||||
|
include_bytes!("../../../../../vendor/harmony-connector/hypium/uitest_agent_v1.2.3.so");
|
||||||
|
const VIDEO_SOCKET: &str = "localabstract:scrcpy_grpc_socket";
|
||||||
|
const VIDEO_ARGS: &str =
|
||||||
|
"-m 0 -scale 2 -frameRate 60 -bitRate 8000000 -iFrameInterval 1000 -screenId 0";
|
||||||
|
macro_rules! recorder {
|
||||||
|
($version:literal) => {
|
||||||
|
(
|
||||||
|
$version,
|
||||||
|
include_bytes!(concat!(
|
||||||
|
"../../../../../vendor/harmony-connector/hypium/libscrcpy_server_unix_",
|
||||||
|
$version,
|
||||||
|
".z.so"
|
||||||
|
)) as &'static [u8],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const VIDEOS: &[(&str, &[u8])] = &[
|
||||||
|
recorder!("6.6-20260629"),
|
||||||
|
recorder!("6.6-20260418"),
|
||||||
|
recorder!("6.5-20260313"),
|
||||||
|
recorder!("6.4-20260113"),
|
||||||
|
recorder!("6.3.1-20260113"),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(super) struct Resources {
|
||||||
|
pub tool: Tool,
|
||||||
|
pub target: String,
|
||||||
|
files: Vec<String>,
|
||||||
|
forwards: Vec<(String, String)>,
|
||||||
|
lease: Lease,
|
||||||
|
video: Option<(usize, String)>,
|
||||||
|
}
|
||||||
|
impl Resources {
|
||||||
|
pub fn open(tool: Tool, target: String) -> Result<Self> {
|
||||||
|
tool.connected(&target)?;
|
||||||
|
let root = env::var_os("HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(env::temp_dir)
|
||||||
|
.join("Library/HarmonyConnector/sessions");
|
||||||
|
let lease = Lease::acquire(&root, &tool, &target)?;
|
||||||
|
let existing = screen_agent::existing_agents(&tool, &target)?;
|
||||||
|
for socket in [SOCKET, VIDEO_SOCKET] {
|
||||||
|
if !screen_lease::forwards_socket(&tool, &target, socket)?.is_empty() {
|
||||||
|
return Err(screen_lease::busy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (pid, cmdline) in existing {
|
||||||
|
screen_agent::terminate_agent(&tool, &target, &pid, &cmdline)?;
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
tool,
|
||||||
|
target,
|
||||||
|
files: Vec::new(),
|
||||||
|
forwards: Vec::new(),
|
||||||
|
lease,
|
||||||
|
video: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn launch(&mut self, bytes: &[u8], arguments: &str) -> Result<String> {
|
||||||
|
let mut file = tempfile::Builder::new()
|
||||||
|
.prefix("trove-screen-")
|
||||||
|
.suffix(".so")
|
||||||
|
.tempfile()?;
|
||||||
|
file.write_all(bytes)?;
|
||||||
|
let name = file
|
||||||
|
.path()
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
self.files.push(name.clone());
|
||||||
|
self.tool.run(
|
||||||
|
&[
|
||||||
|
"-t",
|
||||||
|
&self.target,
|
||||||
|
"file",
|
||||||
|
"send",
|
||||||
|
file.path().to_str().unwrap(),
|
||||||
|
&format!("/data/local/tmp/{name}"),
|
||||||
|
],
|
||||||
|
10,
|
||||||
|
)?;
|
||||||
|
// Keep loader errors from the daemon; a successful hdc shell exit is not readiness.
|
||||||
|
let result = self.tool.run_output(&["-t", &self.target, "shell",
|
||||||
|
&format!("uitest start-daemon singleness --extension-name {name} {arguments} > /data/local/tmp/{name}.log 2>&1")], 5);
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(name),
|
||||||
|
Err(e) if e.code == "hdc.command_failed" => Ok(name),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn ready(&self, name: &str, socket: &str) -> Result<bool> {
|
||||||
|
let process = screen_agent::existing_agents(&self.tool, &self.target)?
|
||||||
|
.iter()
|
||||||
|
.any(|(_, cmdline)| cmdline.split('\0').any(|arg| arg == name));
|
||||||
|
if !process {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let sockets = self
|
||||||
|
.tool
|
||||||
|
.run(&["-t", &self.target, "shell", "cat /proc/net/unix"], 2)?;
|
||||||
|
let name = socket.strip_prefix("localabstract:").unwrap();
|
||||||
|
Ok(sockets.lines().any(|line| {
|
||||||
|
line.split_whitespace()
|
||||||
|
.last()
|
||||||
|
.is_some_and(|path| path.trim_start_matches('@') == name)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
fn wait_ready(&self, name: &str, socket: &str, version: &str) -> Result<()> {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(3);
|
||||||
|
loop {
|
||||||
|
if self.tool.cancel.load(Ordering::Acquire) {
|
||||||
|
return Err(Fault::new("operation.cancelled", "投屏连接已取消。"));
|
||||||
|
}
|
||||||
|
if self.ready(name, socket)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
let output = self
|
||||||
|
.tool
|
||||||
|
.run_output(
|
||||||
|
&[
|
||||||
|
"-t",
|
||||||
|
&self.target,
|
||||||
|
"shell",
|
||||||
|
&format!("cat /data/local/tmp/{name}.log"),
|
||||||
|
],
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let detail: String = output
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !c.is_control() || matches!(c, '\n' | '\t'))
|
||||||
|
.take(800)
|
||||||
|
.collect();
|
||||||
|
return Err(Fault::new(
|
||||||
|
"screen.agent_start_failed",
|
||||||
|
format!("设备投屏组件 {version} 未启动监听。{}", detail.trim()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn forward(&mut self, socket: &str) -> Result<u16> {
|
||||||
|
let listener = TcpListener::bind(("127.0.0.1", 0)).map_err(|e| {
|
||||||
|
Fault::new(
|
||||||
|
"screen.forward_failed",
|
||||||
|
format!("无法分配本机投屏端口:{e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let port = listener
|
||||||
|
.local_addr()
|
||||||
|
.map_err(|e| Fault::new("screen.forward_failed", e.to_string()))?
|
||||||
|
.port();
|
||||||
|
drop(listener);
|
||||||
|
let local = format!("tcp:{port}");
|
||||||
|
self.forwards.push((local.clone(), socket.into()));
|
||||||
|
self.lease.record_forwards(&self.forwards)?;
|
||||||
|
self.tool
|
||||||
|
.run(&["-t", &self.target, "fport", &local, socket], 3)?;
|
||||||
|
if !screen_lease::forwards_socket(&self.tool, &self.target, socket)?.contains(&local) {
|
||||||
|
return Err(Fault::new(
|
||||||
|
"screen.forward_failed",
|
||||||
|
format!("HDC 未建立投屏转发:{socket}。"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
fn remove_forward(&mut self, socket: &str) -> Result<()> {
|
||||||
|
let registered = screen_lease::forwards_socket(&self.tool, &self.target, socket)?;
|
||||||
|
for (local, remote) in self.forwards.iter().filter(|(_, remote)| remote == socket) {
|
||||||
|
if registered.contains(local) {
|
||||||
|
self.tool
|
||||||
|
.run(&["-t", &self.target, "fport", "rm", local, remote], 3)?;
|
||||||
|
if screen_lease::forwards_socket(&self.tool, &self.target, remote)?.contains(local)
|
||||||
|
{
|
||||||
|
return Err(Fault::new(
|
||||||
|
"screen.forward_failed",
|
||||||
|
"HDC 未能释放旧的投屏转发。",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.forwards.retain(|(_, remote)| remote != socket);
|
||||||
|
self.lease.record_forwards(&self.forwards)
|
||||||
|
}
|
||||||
|
fn remove_agent(&mut self, name: &str) -> Result<()> {
|
||||||
|
// Only the exact file generated by this session can be terminated.
|
||||||
|
for (pid, cmdline) in screen_agent::processes(&self.tool, &self.target)? {
|
||||||
|
if cmdline.split('\0').any(|arg| arg == name) {
|
||||||
|
screen_agent::terminate_agent(&self.tool, &self.target, &pid, &cmdline)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tool.run(
|
||||||
|
&[
|
||||||
|
"-t",
|
||||||
|
&self.target,
|
||||||
|
"shell",
|
||||||
|
&format!("rm -f /data/local/tmp/{name} /data/local/tmp/{name}.log"),
|
||||||
|
],
|
||||||
|
2,
|
||||||
|
)?;
|
||||||
|
self.files.retain(|file| file != name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn control(&mut self) -> Result<(u16, Rpc)> {
|
||||||
|
let name = self.launch(CONTROL, "")?;
|
||||||
|
self.wait_ready(&name, SOCKET, "UITest 1.2.3")?;
|
||||||
|
let mut last = None;
|
||||||
|
for _ in 0..3 {
|
||||||
|
self.remove_forward(SOCKET)?;
|
||||||
|
let port = self.forward(SOCKET)?;
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(1);
|
||||||
|
loop {
|
||||||
|
match Rpc::connect(port).and_then(|mut rpc| {
|
||||||
|
rpc.create_driver()?;
|
||||||
|
Ok(rpc)
|
||||||
|
}) {
|
||||||
|
Ok(rpc) => return Ok((port, rpc)),
|
||||||
|
Err(e) if e.code == "screen.remote_error" => return Err(e),
|
||||||
|
Err(e) => last = Some(e),
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Fault::new(
|
||||||
|
"screen.control_connect_failed",
|
||||||
|
format!(
|
||||||
|
"设备控制组件已启动,但 HDC 控制通道无法连接:{}",
|
||||||
|
last.unwrap().message
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn video(&mut self) -> Result<u16> {
|
||||||
|
self.video_from(0)
|
||||||
|
}
|
||||||
|
fn video_from(&mut self, start: usize) -> Result<u16> {
|
||||||
|
let mut failures = Vec::new();
|
||||||
|
for (index, (version, bytes)) in VIDEOS.iter().enumerate().skip(start) {
|
||||||
|
let name = self.launch(bytes, VIDEO_ARGS)?;
|
||||||
|
match self.wait_ready(&name, VIDEO_SOCKET, version) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.video = Some((index, name));
|
||||||
|
return self.forward(VIDEO_SOCKET);
|
||||||
|
}
|
||||||
|
Err(error) if error.code == "screen.agent_start_failed" => {
|
||||||
|
failures.push(error.message);
|
||||||
|
self.remove_agent(&name)?;
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Fault::new(
|
||||||
|
"screen.video_agent_unavailable",
|
||||||
|
format!("官方视频组件均未能在设备上启动。{}", failures.join("\n")),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn reconnect_video(&mut self) -> Result<u16> {
|
||||||
|
self.remove_forward(VIDEO_SOCKET)?;
|
||||||
|
let (index, name) = self
|
||||||
|
.video
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| Fault::new("screen.closed", "视频组件已停止。"))?;
|
||||||
|
if self.ready(&name, VIDEO_SOCKET)? {
|
||||||
|
return self.forward(VIDEO_SOCKET);
|
||||||
|
}
|
||||||
|
self.remove_agent(&name)?;
|
||||||
|
self.video = None;
|
||||||
|
self.video_from(index + 1)
|
||||||
|
}
|
||||||
|
pub fn close(&mut self) -> Result<()> {
|
||||||
|
if self.files.is_empty() && self.forwards.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Cleanup must remain possible when the caller cancelled startup.
|
||||||
|
self.tool.cancel = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut error = None;
|
||||||
|
for socket in [VIDEO_SOCKET, SOCKET] {
|
||||||
|
if let Err(e) = self.remove_forward(socket) {
|
||||||
|
error.get_or_insert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name in self.files.clone() {
|
||||||
|
if let Err(e) = self.remove_agent(&name) {
|
||||||
|
error.get_or_insert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error.map_or(Ok(()), Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Resources {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,146 +1,13 @@
|
|||||||
//! Owns the official video and touch daemons as one USB debugging session.
|
//! Owns the official video and touch daemons as one USB debugging session.
|
||||||
use super::screen_agent::{self, Rpc};
|
use super::screen_agent::Rpc;
|
||||||
use super::screen_lease::{self, Lease, SOCKET};
|
use super::screen_daemon::Resources;
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashSet, VecDeque},
|
collections::{HashSet, VecDeque},
|
||||||
io::Write,
|
|
||||||
net::TcpListener,
|
|
||||||
sync::Condvar,
|
sync::Condvar,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTROL: &[u8] =
|
|
||||||
include_bytes!("../../../../../vendor/harmony-connector/hypium/uitest_agent_v1.2.3.so");
|
|
||||||
const VIDEO: &[u8] = include_bytes!(
|
|
||||||
"../../../../../vendor/harmony-connector/hypium/libscrcpy_server_unix_6.6-20260629.z.so"
|
|
||||||
);
|
|
||||||
const VIDEO_SOCKET: &str = "localabstract:scrcpy_grpc_socket";
|
|
||||||
|
|
||||||
struct Resources {
|
|
||||||
tool: Tool,
|
|
||||||
target: String,
|
|
||||||
files: Vec<String>,
|
|
||||||
forwards: Vec<(String, String)>,
|
|
||||||
lease: Lease,
|
|
||||||
}
|
|
||||||
impl Resources {
|
|
||||||
fn upload(&mut self, bytes: &[u8], arguments: &str) -> Result<()> {
|
|
||||||
let mut file = tempfile::Builder::new()
|
|
||||||
.prefix("trove-screen-")
|
|
||||||
.suffix(".so")
|
|
||||||
.tempfile()?;
|
|
||||||
file.write_all(bytes)?;
|
|
||||||
let name = file
|
|
||||||
.path()
|
|
||||||
.file_name()
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned();
|
|
||||||
self.files.push(name.clone());
|
|
||||||
self.tool.run(
|
|
||||||
&[
|
|
||||||
"-t",
|
|
||||||
&self.target,
|
|
||||||
"file",
|
|
||||||
"send",
|
|
||||||
file.path().to_str().unwrap(),
|
|
||||||
&format!("/data/local/tmp/{name}"),
|
|
||||||
],
|
|
||||||
10,
|
|
||||||
)?;
|
|
||||||
self.tool.run(
|
|
||||||
&[
|
|
||||||
"-t",
|
|
||||||
&self.target,
|
|
||||||
"shell",
|
|
||||||
&format!("uitest start-daemon singleness --extension-name {name} {arguments}"),
|
|
||||||
],
|
|
||||||
5,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn forward(&mut self, socket: &str) -> Result<u16> {
|
|
||||||
let listener = TcpListener::bind(("127.0.0.1", 0))?;
|
|
||||||
let port = listener.local_addr()?.port();
|
|
||||||
drop(listener);
|
|
||||||
let local = format!("tcp:{port}");
|
|
||||||
self.forwards.push((local.clone(), socket.into()));
|
|
||||||
// Record before the command, so a crash between forwarding and return is recoverable.
|
|
||||||
self.lease.record_forwards(&self.forwards)?;
|
|
||||||
self.tool
|
|
||||||
.run(&["-t", &self.target, "fport", &local, socket], 3)?;
|
|
||||||
Ok(port)
|
|
||||||
}
|
|
||||||
fn close(&mut self) -> Result<()> {
|
|
||||||
if self.files.is_empty() && self.forwards.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let cleanup = Tool {
|
|
||||||
path: self.tool.path.clone(),
|
|
||||||
cancel: Arc::new(AtomicBool::new(false)),
|
|
||||||
};
|
|
||||||
let mut error = None;
|
|
||||||
for (local, remote) in &self.forwards {
|
|
||||||
if let Err(e) = cleanup.run(&["-t", &self.target, "fport", "rm", local, remote], 2) {
|
|
||||||
error.get_or_insert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if error.is_none() {
|
|
||||||
self.forwards.clear();
|
|
||||||
self.lease.record(None)?;
|
|
||||||
}
|
|
||||||
// Inspect full command lines before signalling; never kill a foreign daemon.
|
|
||||||
let pids = cleanup.run_output(
|
|
||||||
&["-t", &self.target, "shell", "pidof uitest xdevice_scrcpy"],
|
|
||||||
2,
|
|
||||||
)?;
|
|
||||||
for pid in pids
|
|
||||||
.split_whitespace()
|
|
||||||
.filter(|s| s.bytes().all(|b| b.is_ascii_digit()))
|
|
||||||
{
|
|
||||||
let cmdline = cleanup.run_output(
|
|
||||||
&[
|
|
||||||
"-t",
|
|
||||||
&self.target,
|
|
||||||
"shell",
|
|
||||||
&format!("cat /proc/{pid}/cmdline"),
|
|
||||||
],
|
|
||||||
2,
|
|
||||||
)?;
|
|
||||||
if cmdline
|
|
||||||
.split('\0')
|
|
||||||
.any(|arg| self.files.iter().any(|file| file == arg))
|
|
||||||
{
|
|
||||||
if let Err(e) = screen_agent::terminate_agent(&cleanup, &self.target, pid, &cmdline)
|
|
||||||
{
|
|
||||||
error.get_or_insert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name in self.files.drain(..) {
|
|
||||||
if let Err(e) = cleanup.run(
|
|
||||||
&[
|
|
||||||
"-t",
|
|
||||||
&self.target,
|
|
||||||
"shell",
|
|
||||||
&format!("rm -f /data/local/tmp/{name}"),
|
|
||||||
],
|
|
||||||
2,
|
|
||||||
) {
|
|
||||||
error.get_or_insert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error.map_or(Ok(()), Err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for Resources {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = self.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(super) struct Dimensions {
|
pub(super) struct Dimensions {
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
@@ -159,6 +26,7 @@ pub(super) struct Recorder {
|
|||||||
pub cancel: Arc<AtomicBool>,
|
pub cancel: Arc<AtomicBool>,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub keyframe: AtomicBool,
|
pub keyframe: AtomicBool,
|
||||||
|
pub ready: AtomicBool,
|
||||||
pub error: Mutex<Option<Fault>>,
|
pub error: Mutex<Option<Fault>>,
|
||||||
dimensions: Arc<Mutex<Dimensions>>,
|
dimensions: Arc<Mutex<Dimensions>>,
|
||||||
queue: Arc<Queue>,
|
queue: Arc<Queue>,
|
||||||
@@ -167,51 +35,10 @@ pub(super) struct Recorder {
|
|||||||
}
|
}
|
||||||
impl Recorder {
|
impl Recorder {
|
||||||
pub fn open(tool: Tool, target: String) -> Result<Arc<Self>> {
|
pub fn open(tool: Tool, target: String) -> Result<Arc<Self>> {
|
||||||
tool.connected(&target)?;
|
let mut resources = Resources::open(tool, target)?;
|
||||||
let root = env::var_os("HOME")
|
let (control_port, mut control) = resources.control()?;
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(env::temp_dir)
|
|
||||||
.join("Library/HarmonyConnector/sessions");
|
|
||||||
let lease = Lease::acquire(&root, &tool, &target)?;
|
|
||||||
let existing = screen_agent::existing_agents(&tool, &target)?;
|
|
||||||
for socket in [SOCKET, VIDEO_SOCKET] {
|
|
||||||
if !screen_lease::forwards_socket(&tool, &target, socket)?.is_empty() {
|
|
||||||
return Err(screen_lease::busy());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (pid, cmdline) in existing {
|
|
||||||
screen_agent::terminate_agent(&tool, &target, &pid, &cmdline)?;
|
|
||||||
}
|
|
||||||
let mut resources = Resources {
|
|
||||||
tool,
|
|
||||||
target,
|
|
||||||
files: Vec::new(),
|
|
||||||
forwards: Vec::new(),
|
|
||||||
lease,
|
|
||||||
};
|
|
||||||
resources.upload(CONTROL, "")?;
|
|
||||||
let control_port = resources.forward(SOCKET)?;
|
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
|
||||||
let mut control = loop {
|
|
||||||
if resources.tool.cancel.load(Ordering::Acquire) {
|
|
||||||
return Err(Fault::new("screen.cancelled", "投屏连接已取消。"));
|
|
||||||
}
|
|
||||||
let attempt = Rpc::connect(control_port).and_then(|mut rpc| {
|
|
||||||
rpc.create_driver()?;
|
|
||||||
Ok(rpc)
|
|
||||||
});
|
|
||||||
match attempt {
|
|
||||||
Ok(rpc) => break rpc,
|
|
||||||
Err(e) if Instant::now() >= deadline => return Err(e),
|
|
||||||
Err(_) => thread::sleep(Duration::from_millis(100)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (width, height) = control.dimensions()?;
|
let (width, height) = control.dimensions()?;
|
||||||
resources.upload(
|
let port = resources.video()?;
|
||||||
VIDEO,
|
|
||||||
"-m 0 -scale 2 -frameRate 60 -bitRate 8000000 -iFrameInterval 1000 -screenId 0",
|
|
||||||
)?;
|
|
||||||
let port = resources.forward(VIDEO_SOCKET)?;
|
|
||||||
let tool = Tool {
|
let tool = Tool {
|
||||||
path: resources.tool.path.clone(),
|
path: resources.tool.path.clone(),
|
||||||
cancel: Arc::new(AtomicBool::new(false)),
|
cancel: Arc::new(AtomicBool::new(false)),
|
||||||
@@ -235,6 +62,7 @@ impl Recorder {
|
|||||||
cancel: Arc::new(AtomicBool::new(false)),
|
cancel: Arc::new(AtomicBool::new(false)),
|
||||||
port,
|
port,
|
||||||
keyframe: AtomicBool::new(false),
|
keyframe: AtomicBool::new(false),
|
||||||
|
ready: AtomicBool::new(false),
|
||||||
error: Mutex::new(None),
|
error: Mutex::new(None),
|
||||||
dimensions: Arc::new(Mutex::new(Dimensions {
|
dimensions: Arc::new(Mutex::new(Dimensions {
|
||||||
width,
|
width,
|
||||||
@@ -273,6 +101,17 @@ impl Recorder {
|
|||||||
*owner.worker.lock().unwrap() = Some(worker);
|
*owner.worker.lock().unwrap() = Some(worker);
|
||||||
Ok(owner)
|
Ok(owner)
|
||||||
}
|
}
|
||||||
|
pub fn reconnect_video(&self) -> Result<u16> {
|
||||||
|
if self.cancel.load(Ordering::Acquire) {
|
||||||
|
return Err(Fault::new("screen.closed", "投屏已停止。"));
|
||||||
|
}
|
||||||
|
self.resources
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| Fault::new("screen.closed", "投屏已停止。"))?
|
||||||
|
.reconnect_video()
|
||||||
|
}
|
||||||
pub fn dimensions(&self) -> Dimensions {
|
pub fn dimensions(&self) -> Dimensions {
|
||||||
*self.dimensions.lock().unwrap()
|
*self.dimensions.lock().unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,27 @@ pub(super) fn receive(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()?
|
.build()
|
||||||
|
.map_err(fault)?
|
||||||
.block_on(async {
|
.block_on(async {
|
||||||
let socket = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
|
let socket = tokio::time::timeout(
|
||||||
socket.set_nodelay(true)?;
|
Duration::from_secs(2),
|
||||||
|
tokio::net::TcpStream::connect(("127.0.0.1", port)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
Fault::new(
|
||||||
|
"screen.video_connect_failed",
|
||||||
|
format!("连接 HDC 视频转发超时:{e}"),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.map_err(|e| {
|
||||||
|
Fault::new(
|
||||||
|
"screen.video_connect_failed",
|
||||||
|
format!("无法连接 HDC 视频转发端口 {port}:{e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
socket.set_nodelay(true).map_err(fault)?;
|
||||||
let (mut sender, connection) = h2::client::handshake(socket).await.map_err(fault)?;
|
let (mut sender, connection) = h2::client::handshake(socket).await.map_err(fault)?;
|
||||||
let connection = tokio::spawn(connection);
|
let connection = tokio::spawn(connection);
|
||||||
let result = async {
|
let result = async {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ impl Window {
|
|||||||
current.as_ref().map_or(json!({"active":false}), |v| {
|
current.as_ref().map_or(json!({"active":false}), |v| {
|
||||||
json!({
|
json!({
|
||||||
"active":!v.recorder.cancel.load(Ordering::Acquire),"target":v.target,
|
"active":!v.recorder.cancel.load(Ordering::Acquire),"target":v.target,
|
||||||
|
"ready":v.recorder.ready.load(Ordering::Acquire),
|
||||||
"error":v.recorder.error.lock().unwrap().clone()
|
"error":v.recorder.error.lock().unwrap().clone()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -74,7 +75,9 @@ impl Window {
|
|||||||
return Err(screen_lease::busy());
|
return Err(screen_lease::busy());
|
||||||
}
|
}
|
||||||
viewer.pipe.send(json!({"kind":"focus"}), &[])?;
|
viewer.pipe.send(json!({"kind":"focus"}), &[])?;
|
||||||
return Ok(json!({"active":true,"target":request.target}));
|
return Ok(
|
||||||
|
json!({"active":true,"ready":viewer.recorder.ready.load(Ordering::Acquire),"target":request.target}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(viewer) = current.take() {
|
if let Some(viewer) = current.take() {
|
||||||
@@ -136,6 +139,8 @@ impl Window {
|
|||||||
*reader_recorder.error.lock().unwrap() = Some(error);
|
*reader_recorder.error.lock().unwrap() = Some(error);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else if value["event"] == "presented" {
|
||||||
|
reader_recorder.ready.store(true, Ordering::Release);
|
||||||
} else if value["event"] == "decoderError" {
|
} else if value["event"] == "decoderError" {
|
||||||
reader_recorder.keyframe.store(true, Ordering::Release);
|
reader_recorder.keyframe.store(true, Ordering::Release);
|
||||||
}
|
}
|
||||||
@@ -147,10 +152,11 @@ impl Window {
|
|||||||
let worker = thread::spawn(move || {
|
let worker = thread::spawn(move || {
|
||||||
let mut sequence = 0u64;
|
let mut sequence = 0u64;
|
||||||
let mut samples = VecDeque::new();
|
let mut samples = VecDeque::new();
|
||||||
let startup = Instant::now();
|
let mut port = worker_recorder.port;
|
||||||
|
let mut retries = 0;
|
||||||
let result = loop {
|
let result = loop {
|
||||||
let result = super::screen_video::receive(
|
let result = super::screen_video::receive(
|
||||||
worker_recorder.port,
|
port,
|
||||||
&worker_recorder.cancel,
|
&worker_recorder.cancel,
|
||||||
&worker_recorder.keyframe,
|
&worker_recorder.keyframe,
|
||||||
|bytes| {
|
|bytes| {
|
||||||
@@ -173,11 +179,16 @@ impl Window {
|
|||||||
);
|
);
|
||||||
if result.is_ok()
|
if result.is_ok()
|
||||||
|| sequence > 0
|
|| sequence > 0
|
||||||
|| startup.elapsed() > Duration::from_secs(5)
|
|| retries >= 2
|
||||||
|| worker_recorder.cancel.load(Ordering::Acquire)
|
|| worker_recorder.cancel.load(Ordering::Acquire)
|
||||||
{
|
{
|
||||||
break result;
|
break result;
|
||||||
}
|
}
|
||||||
|
retries += 1;
|
||||||
|
port = match worker_recorder.reconnect_video() {
|
||||||
|
Ok(port) => port,
|
||||||
|
Err(error) => break Err(error),
|
||||||
|
};
|
||||||
thread::sleep(Duration::from_millis(100));
|
thread::sleep(Duration::from_millis(100));
|
||||||
};
|
};
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
@@ -197,7 +208,7 @@ impl Window {
|
|||||||
target: request.target.clone(),
|
target: request.target.clone(),
|
||||||
worker: Some(worker),
|
worker: Some(worker),
|
||||||
});
|
});
|
||||||
Ok(json!({"active":true,"target":request.target}))
|
Ok(json!({"active":true,"ready":false,"target":request.target}))
|
||||||
}
|
}
|
||||||
pub fn stop(&self) -> Result<Value> {
|
pub fn stop(&self) -> Result<Value> {
|
||||||
if let Some(viewer) = self.current.lock().unwrap().take() {
|
if let Some(viewer) = self.current.lock().unwrap().take() {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
export function screen({ call, notice, onBusy }) {
|
export function screen({ call, notice, onBusy }) {
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const state = { target: "", available: false, blocked: false, working: false, active: false, disposed: false };
|
const state = { target: "", available: false, blocked: false, working: false, active: false, ready: false, disposed: false };
|
||||||
let timer, error;
|
let timer, error;
|
||||||
function render() {
|
function render() {
|
||||||
$("screen-start").disabled = state.working || state.blocked || !state.available;
|
$("screen-start").disabled = state.working || state.blocked || !state.available;
|
||||||
$("screen-start").textContent = state.working ? "正在连接…" : state.active ? "显示投屏窗口" : "打开投屏窗口";
|
$("screen-start").textContent = state.working ? "正在连接…" : state.active ? "显示投屏窗口" : "打开投屏窗口";
|
||||||
$("screen-stop").disabled = state.working || !state.active;
|
$("screen-stop").disabled = state.working || !state.active;
|
||||||
$("screen-status").textContent = state.active ? "正在独立窗口中投屏 · 默认鼠标控制" : "让手机,来到桌面。";
|
$("screen-status").textContent = state.active ? state.ready ? "正在独立窗口中投屏 · 默认鼠标控制" : "正在建立视频连接…" : "让手机,来到桌面。";
|
||||||
}
|
}
|
||||||
async function status() {
|
async function status() {
|
||||||
if (state.disposed) return;
|
if (state.disposed) return;
|
||||||
try {
|
try {
|
||||||
const result = await call("screenWindowStatus", {});
|
const result = await call("screenWindowStatus", {});
|
||||||
if (state.disposed) return;
|
if (state.disposed) return;
|
||||||
if (!state.working) state.active = result.active;
|
if (!state.working) { state.active = result.active; state.ready = !!result.ready; }
|
||||||
if (result.error && result.error.message !== error) {
|
if (result.error && result.error.message !== error) {
|
||||||
error = result.error.message;
|
error = result.error.message;
|
||||||
notice(error);
|
notice(error);
|
||||||
@@ -30,6 +30,7 @@ export function screen({ call, notice, onBusy }) {
|
|||||||
try {
|
try {
|
||||||
const result = await call(method, args);
|
const result = await call(method, args);
|
||||||
state.active = result.active;
|
state.active = result.active;
|
||||||
|
state.ready = !!result.ready;
|
||||||
error = undefined;
|
error = undefined;
|
||||||
} catch (error) { notice(error.message || String(error)); }
|
} catch (error) { notice(error.message || String(error)); }
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ final class ViewerApp: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
private var pending: (CVPixelBuffer, ScreenFrame)?
|
private var pending: (CVPixelBuffer, ScreenFrame)?
|
||||||
private var scheduled = false
|
private var scheduled = false
|
||||||
private var geometry: UInt64 = 0
|
private var geometry: UInt64 = 0
|
||||||
|
private var announced = false
|
||||||
private let deviceName = CommandLine.arguments.dropFirst().first ?? "Harmony"
|
private let deviceName = CommandLine.arguments.dropFirst().first ?? "Harmony"
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 380, height: 760), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
|
window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 380, height: 760), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
|
||||||
window.title = deviceName
|
window.title = "\(deviceName) · 正在连接视频…"
|
||||||
window.minSize = NSSize(width: 220, height: 260)
|
window.minSize = NSSize(width: 220, height: 260)
|
||||||
window.backgroundColor = .black
|
window.backgroundColor = .black
|
||||||
window.isReleasedWhenClosed = false
|
window.isReleasedWhenClosed = false
|
||||||
@@ -57,6 +58,10 @@ final class ViewerApp: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
self.pendingLock.unlock()
|
self.pendingLock.unlock()
|
||||||
guard let (image, frame) = current else { return }
|
guard let (image, frame) = current else { return }
|
||||||
self.view.display(image, frame: frame)
|
self.view.display(image, frame: frame)
|
||||||
|
if !self.announced {
|
||||||
|
self.announced = true
|
||||||
|
ViewerWire.send(["event": "presented"])
|
||||||
|
}
|
||||||
if self.geometry != frame.geometry {
|
if self.geometry != frame.geometry {
|
||||||
let initial = self.geometry == 0
|
let initial = self.geometry == 0
|
||||||
self.geometry = frame.geometry
|
self.geometry = frame.geometry
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Harmony Connector
|
# Harmony Connector
|
||||||
|
|
||||||
插件 ID:`dev.trove.harmony-connector`。当前版本:`0.5.0`。
|
插件 ID:`dev.trove.harmony-connector`。当前版本:`0.5.1`。
|
||||||
|
|
||||||
macOS 上的鸿蒙设备连接工具。通过 USB 收发文件与完整文件夹;使用官方 HDC 管理应用、保存截图、导出日志和调试应用数据。功能按连接工具、设备实际能力和授权开放,不按 HarmonyOS 大版本或机型名称限制。
|
macOS 上的鸿蒙设备连接工具。通过 USB 收发文件与完整文件夹;使用官方 HDC 管理应用、保存截图、导出日志和调试应用数据。功能按连接工具、设备实际能力和授权开放,不按 HarmonyOS 大版本或机型名称限制。
|
||||||
|
|
||||||
@@ -75,6 +75,8 @@ macOS 上的鸿蒙设备连接工具。通过 USB 收发文件与完整文件夹
|
|||||||
|
|
||||||
0.5.0 使用官方 Hypium 26 的 Unix H.264 recorder 和独立控制代理,经 HDC 转发视频及触摸通道。手机端编码,Mac 端 VideoToolbox 解码并立即显示。无需开发者账号、签名、手机应用或完整 SDK;用户只需官方 HDC 和已授权的设备 USB 调试。编码配置目标为 60 fps、8 Mbps、半尺寸,实际流畅度取决于设备,当前版本尚未进行真机性能验收。
|
0.5.0 使用官方 Hypium 26 的 Unix H.264 recorder 和独立控制代理,经 HDC 转发视频及触摸通道。手机端编码,Mac 端 VideoToolbox 解码并立即显示。无需开发者账号、签名、手机应用或完整 SDK;用户只需官方 HDC 和已授权的设备 USB 调试。编码配置目标为 60 fps、8 Mbps、半尺寸,实际流畅度取决于设备,当前版本尚未进行真机性能验收。
|
||||||
|
|
||||||
|
0.5.1 在设备代理进程及 Unix socket 就绪后才创建转发;按官方资源版本依次尝试兼容代理。首次连接失败会重新建立本插件的 HDC 转发。窗口收到首张解码画面之前显示连接中,失败区分组件启动、转发和视频连接,不再把网络错误归为 Mac 文件错误。
|
||||||
|
|
||||||
组件与传输依据见[官方视频与触控调研](research/harmony-screen-performance.md)。旧 JPEG 后端接口仅保留用于诊断;管理页已使用独立视频窗口。
|
组件与传输依据见[官方视频与触控调研](research/harmony-screen-performance.md)。旧 JPEG 后端接口仅保留用于诊断;管理页已使用独立视频窗口。
|
||||||
|
|
||||||
## 重连问题与 debug
|
## 重连问题与 debug
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Harmony Connector 投屏开发记录
|
# Harmony Connector 投屏开发记录
|
||||||
|
|
||||||
|
2026-09-10:0.5.1 针对用户报告的 `无法读写 Mac 文件:Connection refused (os error 61)` 修改启动流程。代码可确认该提示来自视频 TCP 连接使用了通用文件 I/O 错误转换;此前没有确认设备代理/Unix socket 就绪,且只重试同一端口。现在增加官方代理候选、就绪检查、仅重建自身转发、启动日志和首帧状态。具体设备上的代理兼容性与连接恢复效果仍等待用户反馈;本轮只构建打包,未运行测试或真机验证。
|
||||||
|
|
||||||
|
|
||||||
2026-09-10:0.5.0 改为独立 AppKit 窗口、官方 Unix H.264 视频流和默认实时触控。按用户要求,本轮只完成编码与构建,不运行测试、真机跑分或远程源安装验证。60 fps 是编码配置目标,不能作为已达到的性能结果。官方接口依据见[视频性能调研](research/harmony-screen-performance.md)。
|
2026-09-10:0.5.0 改为独立 AppKit 窗口、官方 Unix H.264 视频流和默认实时触控。按用户要求,本轮只完成编码与构建,不运行测试、真机跑分或远程源安装验证。60 fps 是编码配置目标,不能作为已达到的性能结果。官方接口依据见[视频性能调研](research/harmony-screen-performance.md)。
|
||||||
|
|
||||||
以下为旧版本历史记录。
|
以下为旧版本历史记录。
|
||||||
|
|||||||
@@ -227,3 +227,12 @@ macOS 14 及以上可使用该层的 sampleBufferRenderer,保留项目要求
|
|||||||
- 已完成:官方 26 包获取、静态解包、资源摘要、启动/选型脚本、proto 对比、Unix 绑定与触控参数的汇编核查。
|
- 已完成:官方 26 包获取、静态解包、资源摘要、启动/选型脚本、proto 对比、Unix 绑定与触控参数的汇编核查。
|
||||||
- 未完成且未声称完成:最新 Unix 原件在用户手机的加载兼容性、连续视频解码结果、流畅度/耗电/延迟测量、旧 1.2.2 agent 的触控全量契约复核。
|
- 未完成且未声称完成:最新 Unix 原件在用户手机的加载兼容性、连续视频解码结果、流畅度/耗电/延迟测量、旧 1.2.2 agent 的触控全量契约复核。
|
||||||
- 不需要为采用本路径继续下载完整 IDE、创建 HAP 或申请开发者签名;用户的实际前置条件是可用 HDC 与设备调试授权。原件与系统 ABI 的兼容性仍需清楚报告,不能失败后偷偷切回慢截图却继续显示“实时视频”。
|
- 不需要为采用本路径继续下载完整 IDE、创建 HAP 或申请开发者签名;用户的实际前置条件是可用 HDC 与设备调试授权。原件与系统 ABI 的兼容性仍需清楚报告,不能失败后偷偷切回慢截图却继续显示“实时视频”。
|
||||||
|
|
||||||
|
|
||||||
|
## 0.5.1 启动流程修正
|
||||||
|
|
||||||
|
用户报告的 `Connection refused` 被 0.5.0 视频 TCP 连接处的通用 `std::io::Error → Fault` 包装成 Mac 文件错误。该代码路径可直接定位,但没有真机复现证据可以把底层拒绝进一步归因为某个代理 ABI。
|
||||||
|
|
||||||
|
实现补齐官方 `RecordAgent` 已做的进程/Unix socket 就绪判断,按上表最新至最旧顺序选择能够启动的官方 Unix 原件。视频连接失败时移除并重建自身记录的转发;不会借用未知转发或终止外部进程。HDC 转发包含本机监听与对端激活阶段,发出 `fport` 命令本身不能代替端到端连接。[HDC 官方转发源码](https://github.com/openharmony/developtools_hdc/blob/master/src/common/forward.cpp)
|
||||||
|
|
||||||
|
产品错误信息区分设备组件启动、HDC 转发及视频连接;首帧显示前保持“正在建立视频连接”。按用户要求,本轮未跑测试、真机实验或远程安装验证。
|
||||||
|
|||||||
+16
-1
@@ -2,7 +2,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "trove-official",
|
"id": "trove-official",
|
||||||
"name": "Trove 官方软件源",
|
"name": "Trove 官方软件源",
|
||||||
"generatedAt": "2026-09-10T09:25:53.244Z",
|
"generatedAt": "2026-09-10T09:37:34.602Z",
|
||||||
"curios": [
|
"curios": [
|
||||||
{
|
{
|
||||||
"id": "dev.trove.qa.host",
|
"id": "dev.trove.qa.host",
|
||||||
@@ -200,6 +200,21 @@
|
|||||||
"name": "Harmony Connector",
|
"name": "Harmony Connector",
|
||||||
"description": "连接鸿蒙设备,收发完整文件夹、管理应用,在独立窗口中投屏并通过键鼠操作。",
|
"description": "连接鸿蒙设备,收发完整文件夹、管理应用,在独立窗口中投屏并通过键鼠操作。",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.5.1",
|
||||||
|
"engine": {
|
||||||
|
"trove": ">=0.1.0 <1.0.0"
|
||||||
|
},
|
||||||
|
"releasedAt": "2026-09-10T09:37:34.602Z",
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"target": "aarch64-apple-darwin",
|
||||||
|
"url": "packages/dev.trove.harmony-connector/0.5.1/aarch64-apple-darwin.curio",
|
||||||
|
"sha256": "1bfed01377ab519c58c71f0a1597c107edd7242c1d96e288979a44929016cb74",
|
||||||
|
"size": 4718083
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"engine": {
|
"engine": {
|
||||||
|
|||||||
Binary file not shown.
@@ -69,9 +69,9 @@ window.__TROVE_CONTEXT__ = {
|
|||||||
platform: "macos",
|
platform: "macos",
|
||||||
};
|
};
|
||||||
window.__TROVE_BRIDGE__ = createMockBridge({
|
window.__TROVE_BRIDGE__ = createMockBridge({
|
||||||
"@self.hdc.screenWindowOpen": () => ({ active: (screenActive = true) }),
|
"@self.hdc.screenWindowOpen": () => ({ active: (screenActive = true), ready: true }),
|
||||||
"@self.hdc.screenWindowStop": () => ({ active: (screenActive = false) }),
|
"@self.hdc.screenWindowStop": () => ({ active: (screenActive = false) }),
|
||||||
"@self.hdc.screenWindowStatus": () => ({ active: screenActive }),
|
"@self.hdc.screenWindowStatus": () => ({ active: screenActive, ready: screenActive }),
|
||||||
"@self.hdc.status": () => ({ busy: false, job: null }),
|
"@self.hdc.status": () => ({ busy: false, job: null }),
|
||||||
"@self.hdc.tool": () => ({
|
"@self.hdc.tool": () => ({
|
||||||
available: true,
|
available: true,
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ async function app(t) {
|
|||||||
if (model.screenError) throw model.screenError;
|
if (model.screenError) throw model.screenError;
|
||||||
model.screenActive = true;
|
model.screenActive = true;
|
||||||
model.screenTarget = target;
|
model.screenTarget = target;
|
||||||
return { active: true, target };
|
return { active: true, ready: true, target };
|
||||||
}),
|
}),
|
||||||
handler("hdc.screenWindowStop", () => ({ active: (model.screenActive = false) })),
|
handler("hdc.screenWindowStop", () => ({ active: (model.screenActive = false) })),
|
||||||
handler("hdc.screenWindowStatus", () => ({ active: model.screenActive, target: model.screenTarget, error: model.screenError })),
|
handler("hdc.screenWindowStatus", () => ({ active: model.screenActive, ready: model.screenActive, target: model.screenTarget, error: model.screenError })),
|
||||||
handler("hdc.status", () => ({ busy: false, job: null })),
|
handler("hdc.status", () => ({ busy: false, job: null })),
|
||||||
handler("hdc.tool", () => ({
|
handler("hdc.tool", () => ({
|
||||||
available: model.hdcAvailable,
|
available: model.hdcAvailable,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,30 @@
|
|||||||
"member": "devicetest/res/recorder/libscrcpy_server_unix_6.6-20260629.z.so",
|
"member": "devicetest/res/recorder/libscrcpy_server_unix_6.6-20260629.z.so",
|
||||||
"sha256": "b24231188ab0fffa84c2bd71cab6948c8ffad9d661c80301f32eaf580177ce8e",
|
"sha256": "b24231188ab0fffa84c2bd71cab6948c8ffad9d661c80301f32eaf580177ce8e",
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "libscrcpy_server_unix_6.6-20260418.z.so",
|
||||||
|
"member": "devicetest/res/recorder/libscrcpy_server_unix_6.6-20260418.z.so",
|
||||||
|
"sha256": "98bbc964fef1c4d17b446ec717102b855d09224b23fc1872bf70e1f928d010dd",
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "libscrcpy_server_unix_6.5-20260313.z.so",
|
||||||
|
"member": "devicetest/res/recorder/libscrcpy_server_unix_6.5-20260313.z.so",
|
||||||
|
"sha256": "2c8f1ef94d581281bf8e86d54f5939af9b205d5db5aa2cfb27f2562215c87b0a",
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "libscrcpy_server_unix_6.4-20260113.z.so",
|
||||||
|
"member": "devicetest/res/recorder/libscrcpy_server_unix_6.4-20260113.z.so",
|
||||||
|
"sha256": "b698b41ae1351337eeea3dcc87bf0160552196d669ba321b4963c342fa5a47f4",
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "libscrcpy_server_unix_6.3.1-20260113.z.so",
|
||||||
|
"member": "devicetest/res/recorder/libscrcpy_server_unix_6.3.1-20260113.z.so",
|
||||||
|
"sha256": "3e29e3a1d23ea6dbd4e6fb3b8f094ca5d4b15b4c580f421fba3d0e1f5429e3fe",
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user