Skip to content

support harmony remote by mjpeg #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ UI Inspector for Android and iOS, help inspector element properties, and auto ge
pip install uiautodev
```

To support harmony, run the following command

```bash
pip install "uiautodev[harmony]"
```

# Usage
```bash
Usage: uiauto.dev [OPTIONS] COMMAND [ARGS]...
Expand Down
187 changes: 187 additions & 0 deletions examples/harmony-video.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Video Stream</title>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* Fill the entire viewport height */
background-color: #000;
}
#videoCanvasContainer {
position: relative;
width: 100vw; /* Occupy the entire viewport width */
height: 100vh; /* Occupy the entire viewport height */
display: flex;
justify-content: center;
align-items: center;
}
#videoCanvas {
background-color: #000; /* Prevent white background on load */
display: block;
margin: auto; /* Center the canvas */
}
</style>
</head>
<body>
<div id="videoCanvasContainer">
<canvas id="videoCanvas"></canvas>
</div>

<script>
// Assume phone screen dimensions are 1260x2720
const phoneScreenWidth = 1260;
const phoneScreenHeight = 2720;

const canvasContainer = document.getElementById('videoCanvasContainer');
const canvas = document.getElementById('videoCanvas');
const context = canvas.getContext('2d');

const ws = new WebSocket('ws://localhost:8899'); // Ensure this URL matches your backend WS connection
ws.binaryType = 'arraybuffer';

ws.onmessage = function(event) {
const arrayBuffer = event.data;
const blob = new Blob([arrayBuffer], {type: 'image/jpeg'});
const url = URL.createObjectURL(blob);

const img = new Image();
img.onload = function() {
// Calculate aspect ratio and adjust canvas size
const containerWidth = canvasContainer.offsetWidth;
const containerHeight = canvasContainer.offsetHeight;
const aspectRatio = img.width / img.height;

if (containerWidth / containerHeight > aspectRatio) {
canvas.height = containerHeight;
canvas.width = Math.floor(containerHeight * aspectRatio);
} else {
canvas.width = containerWidth;
canvas.height = Math.floor(containerWidth / aspectRatio);
}

// Clear and draw the image on the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(url);
};
img.src = url;
};

let downCoordinates = { x: 0, y: 0 }
let upCoordinates = { x: 0, y: 0 }

canvas.addEventListener('mousedown', function(event) {
// isMouseDown = true;
downCoordinates = calculateCoordinates(event)


// 按下即触发点击事件
// sendMouseEvent('normal', downCoordinates);
// endX = startX;
// endY = startY;

// Long press detection
// longPressTimer = setTimeout(function() {
// if (isMouseDown) {
// sendMouseEvent('long', { clientX: startX, clientY: startY });
// }
// }, 500); // Long press time threshold in milliseconds
});

// canvas.addEventListener('mousemove', function(event) {
// console.log('move', event.clientX, event.clientY)
// if (isMouseDown) {
// // Send mouse move event with both current and previous coordinates
// const {startX, startY} = calculateCoordinates({ clientX: startX, clientY: startY })
// const {endX, endY} = calculateCoordinates(event)
// ws.send(JSON.stringify({
// type: 'touch',
// action: 'move',
// x1: startX,
// y1: startY,
// x2: endX,
// y2: endY
// }));
// }
// });


canvas.addEventListener('mouseup', function(event) {
// 1-up 和 down是同一个点,说明是点击事件
// 2-up 和 down是不同一个点,说明是滑动事件
console.log('down ', downCoordinates)
// isMouseDown = false;
upCoordinates = calculateCoordinates(event)
let { mouseX: startX, mouseY: startY } = downCoordinates;
let { mouseX: endX, mouseY: endY } = upCoordinates;
// clearTimeout(longPressTimer);
console.log(startX); // 输出 0
console.log(startY); // 输出 0
console.log(endX); // 输出 0
console.log(endY); // 输出 0
if (startX === endX && startY === endY) {
console.log("Click event detected");
sendMouseEvent('normal', startX, startY);
} else {
console.log("Move event detected");
sendMouseMoveEvent('move', startX, startY, endX, endY);
}
});

// canvas.addEventListener('click', function(event) {
// sendMouseEvent('normal', event);
// });

// canvas.addEventListener('dblclick', function(event) {
// sendMouseEvent('double', event);
// });

function sendMouseEvent(action, mouseX, mouseY) {
// Send mouse event to backend via WebSocket
ws.send(JSON.stringify({ type: 'touch', action: action, x: mouseX, y: mouseY }));
}
function sendMouseMoveEvent(action, startX, startY, endX, endY) {
ws.send(JSON.stringify({
type: 'touch',
action: action,
x1: startX,
y1: startY,
x2: endX,
y2: endY
}));
}

function calculateCoordinates(event) {
const rect = canvas.getBoundingClientRect();
const mouseXCanvas = event.clientX - rect.left;
const mouseYCanvas = event.clientY - rect.top;

const scaleX = phoneScreenWidth / canvas.width;
const scaleY = phoneScreenHeight / canvas.height;
const mouseX = Math.floor(mouseXCanvas * scaleX);
const mouseY = Math.floor(mouseYCanvas * scaleY);

console.log(`Canvas coordinates: (${mouseXCanvas}, ${mouseYCanvas})`);
console.log(`Scaled coordinates: (${mouseX}, ${mouseY})`);

return { mouseX, mouseY };
}



ws.onopen = function() {
console.log('WebSocket connection opened');
};

ws.onclose = function() {
console.log('WebSocket connection closed');
};
</script>
</body>
</html>
26 changes: 0 additions & 26 deletions mobile_tests/test_udt.py

This file was deleted.

8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ include = [

[tool.poetry.dependencies]
python = "^3.8"
setuptools = "*"
adbutils = ">=2.8.10,<3"
click = "^8.1.7"
pygments = ">=2"
Expand All @@ -28,6 +29,13 @@ httpx = "*"
uvicorn = ">=0.33.0"
rich = "*"
python-multipart = ">=0.0.18"
xdevice = { url = "https://public.uiauto.devsleep.com/harmony/xdevice-5.0.7.200.tar.gz" }
xdevice-devicetest = { url = "https://public.uiauto.devsleep.com/harmony/xdevice-devicetest-5.0.7.200.tar.gz" }
xdevice-ohos = { url = "https://public.uiauto.devsleep.com/harmony/xdevice-ohos-5.0.7.200.tar.gz" }
hypium = { url = "https://public.uiauto.devsleep.com/harmony/hypium-5.0.7.200.tar.gz" }

[tool.poetry.extras]
harmony = ["setuptools", "xdevice", "xdevice-devicetest", "xdevice-ohos", "hypium"]

[tool.poetry.scripts]
"uiauto.dev" = "uiautodev.__main__:main"
Expand Down
51 changes: 41 additions & 10 deletions uiautodev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def get_features(platform: str) -> Dict[str, bool]:
features[feature_name] = True
return features


class InfoResponse(BaseModel):
version: str
description: str
Expand Down Expand Up @@ -140,15 +141,41 @@ def index_redirect():
return RedirectResponse(url)


def get_scrcpy_server(serial: str):
# 这里主要是为了避免两次websocket建立建立,启动两个scrcpy进程
logger.info("create scrcpy server for %s", serial)
device = adbutils.device(serial)
return ScrcpyServer(device)
@app.websocket("/ws/android/scrcpy/{serial}")
async def handle_android_ws(websocket: WebSocket, serial: str):
"""
Args:
serial: device serial
websocket: WebSocket
"""
await websocket.accept()

try:
logger.info(f"WebSocket serial: {serial}")
device = adbutils.device(serial)
server = ScrcpyServer(device)
await server.handle_unified_websocket(websocket, serial)
except WebSocketDisconnect:
logger.info(f"WebSocket disconnected by client.")
except Exception as e:
logger.exception(f"WebSocket error for serial={serial}: {e}")
await websocket.close(code=1000, reason=str(e))
finally:
logger.info(f"WebSocket closed for serial={serial}")


def get_harmony_mjpeg_server(serial: str):
from hypium import UiDriver

from uiautodev.remote.harmony_mjpeg import HarmonyMjpegServer
driver = UiDriver.connect(device_sn=serial)
logger.info("create harmony mjpeg server for %s", serial)
logger.info(f'device wake_up_display: {driver.wake_up_display()}')
return HarmonyMjpegServer(driver)

@app.websocket("/ws/android/scrcpy/{serial}")
async def unified_ws(websocket: WebSocket, serial: str):

@app.websocket("/ws/harmony/mjpeg/{serial}")
async def unified_harmony_ws(websocket: WebSocket, serial: str):
"""
Args:
serial: device serial
Expand All @@ -159,9 +186,13 @@ async def unified_ws(websocket: WebSocket, serial: str):
try:
logger.info(f"WebSocket serial: {serial}")

# 获取 ScrcpyServer 实例
server = get_scrcpy_server(serial)
await server.handle_unified_websocket(websocket, serial)
# 获取 HarmonyScrcpyServer 实例
server = get_harmony_mjpeg_server(serial)
server.start()
await server.handle_ws(websocket)
except ImportError as e:
logger.error(f"missing library for harmony: {e}")
await websocket.close(code=1000, reason="missing library, fix by \"pip install uiautodev[harmony]\"")
except WebSocketDisconnect:
logger.info(f"WebSocket disconnected by client.")
except Exception as e:
Expand Down
22 changes: 8 additions & 14 deletions uiautodev/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,19 @@ class UiautoException(Exception):
pass


class IOSDriverException(UiautoException):
class DriverException(UiautoException):
"""Base class for all driver-related exceptions."""
pass


class AndroidDriverException(UiautoException):
pass


class AppiumDriverException(UiautoException):
pass
class IOSDriverException(DriverException): ...
class AndroidDriverException(DriverException): ...
class HarmonyDriverException(DriverException): ...
class AppiumDriverException(DriverException): ...


class MethodError(UiautoException):
pass


class ElementNotFoundError(MethodError):
pass


class RequestError(UiautoException):
pass
class ElementNotFoundError(MethodError): ...
class RequestError(UiautoException): ...
Loading
Loading