This commit is contained in:
EightyDollars 2024-12-05 19:00:49 +08:00
commit 20e8a59b62
9 changed files with 146 additions and 67 deletions

View file

@ -346,6 +346,12 @@ class EmulatorPart(ConfModel):
rotate: bool = False
"将截图旋转180度"
class DroidCastRawConf(ConfModel):
orientation: Literal["portrait", "landscape"] = "landscape"
"屏幕方向"
rotate: Literal[0, 90, 180, 270] = 0
"截图旋转角度"
adb: str = "127.0.0.1:16384"
"ADB连接地址"
emulator: EmulatorConf
@ -356,6 +362,8 @@ class EmulatorPart(ConfModel):
"游戏服务器"
droidcast: DroidCastConf
"DroidCast截图设置"
droidcast_raw: DroidCastRawConf
"DroidCast截图设置"
custom_screenshot: CustomScreenshotConf
"自定义截图"
tap_to_launch_game: TapToLaunchGameConf
@ -369,10 +377,11 @@ class EmulatorPart(ConfModel):
screencap_strategy: Literal[
"adb",
"droidcast",
"droidcast_raw",
"mumuipc",
"diy",
"scrcpy",
] = "droidcast"
] = "droidcast_raw"
"截图方案"
control_strategy: Literal["scrcpy", "mumuipc"] = "scrcpy"
"触控方案"

View file

@ -12,6 +12,7 @@ from mower.utils.log import logger, save_screenshot
method_map = {
"adb": "mower.utils.device.method.adb.ADB",
"droidcast": "mower.utils.device.method.droidcast.DroidCast",
"droidcast_raw": "mower.utils.device.method.droidcast.DroidCast_raw",
"scrcpy": "mower.utils.device.method.scrcpy.Scrcpy",
"mumuipc": "mower.utils.device.method.mumu_ipc.MuMu12IPC",
"diy": "mower.utils.device.method.adb.DIY",

View file

@ -285,7 +285,7 @@ class ADB:
dpi = (override_dpi or physical_dpi).partition("density:")[2].strip()
if (
config.conf.emulator.name == "MuMu12"
and config.conf.screencap_strategy == "droidcast"
and config.conf.screencap_strategy.startswith("droidcast")
):
from mower.utils.device.method.mumumanager import MuMuManager

View file

@ -2,15 +2,16 @@ import time
from functools import cached_property, wraps
import cv2
import numpy as np
import requests
from mower import __rootdir__
from mower.utils import config
from mower.utils.csleep import MowerExit
from mower.utils.device.method.adb import ADB
from mower.utils.image import bytes2img
from mower.utils.log import logger
from mower.utils.network import get_new_port, is_port_in_use
from mower.utils.path import get_path
def retry_droidcast(func):
@ -29,6 +30,10 @@ def retry_droidcast(func):
class DroidCast:
display_name = "DroidCast"
package_name = "com.rayworks.droidcast"
apk_file = "DroidCast-debug-1.2.1.apk"
def __init__(self):
self.session = requests.Session()
self.port = 0
@ -40,7 +45,7 @@ class DroidCast:
def get_droidcast_classpath(self) -> str | None:
# TODO: 退出时(并非结束mower线程时)关闭DroidCast进程、取消ADB转发
out = self.adb.adb_shell("pm path com.rayworks.droidcast")
out = self.adb.adb_shell(f"pm path {self.package_name}")
if out is None:
logger.exception("无法获取CLASSPATH")
return None
@ -54,15 +59,18 @@ class DroidCast:
return class_path
def start_droidcast(self) -> bool:
class_path = self.get_droidcast_classpath()
try:
class_path = self.get_droidcast_classpath()
except Exception:
class_path = None
if not class_path:
logger.info("安装DroidCast")
apk_path = f"{__rootdir__}/vendor/DroidCast-debug-1.2.1.apk"
logger.info(f"安装{self.display_name}")
try:
self.adb.adb.install(apk_path)
logger.info("DroidCast安装完成,获取CLASSPATH")
apk_path = get_path(f"@install/mower/vendor/{self.apk_file}")
self.adb.adb.install(str(apk_path))
logger.info(f"{self.display_name}安装完成,获取CLASSPATH")
except Exception as e:
logger.error(f"DroidCast安装失败: {e}")
logger.error(f"{self.display_name}安装失败: {e}")
return False
class_path = self.get_droidcast_classpath()
if not class_path:
@ -83,24 +91,68 @@ class DroidCast:
if port == 0:
port = get_new_port()
self.port = port
logger.info(f"更新DroidCast端口为{port}")
logger.info(f"更新{self.display_name}端口为{port}")
else:
logger.info(f"保持DroidCast端口为{port}")
logger.info(f"保持{self.display_name}端口为{port}")
self.adb.adb.forward(f"tcp:{port}", f"tcp:{port}")
logger.info("ADB端口转发成功,启动DroidCast")
cmd = f"{class_path} app_process / com.rayworks.droidcast.Main --port={port}"
logger.info("ADB端口转发成功,启动{self.display_name}")
cmd = f"{class_path} app_process / {self.package_name}.Main --port={port}"
self.socket = self.adb.adb_shell(cmd, True)
time.sleep(1) # TODO: 更好地等待DroidCast启动
return True
@retry_droidcast
def capture_display(self):
if self.port == 0:
raise Exception("DroidCast未启动")
url = f"http://127.0.0.1:{self.port}/screenshot"
logger.debug(f"GET {url}")
r = self.session.get(url)
img = bytes2img(r.content)
def decode(self, data):
img = bytes2img(data)
if config.conf.droidcast.rotate:
img = cv2.rotate(img, cv2.ROTATE_180)
return img
@retry_droidcast
def capture_display(self):
if self.port == 0:
raise Exception(f"{self.display_name}未启动")
url = f"http://127.0.0.1:{self.port}/screenshot"
logger.debug(f"GET {url}")
r = self.session.get(url)
return self.decode(r.content)
class DroidCast_raw(DroidCast):
display_name = "DroidCast_raw"
package_name = "ink.mol.droidcast_raw"
apk_file = "DroidCast_raw-release-1.1.apk"
def decode(self, data):
# https://github.com/LmeSzinc/StarRailCopilot/blob/bf62eccaaa6f46ea9796a83175df0ab7882dd137/module/device/method/droidcast.py#L226
arr = np.frombuffer(data, dtype=np.uint16)
if config.conf.droidcast_raw.orientation == "portrait":
shape = (1080, 1920)
else:
shape = (1920, 1080)
arr = arr.reshape(shape)
r = cv2.bitwise_and(arr, 0b1111100000000000)
r = cv2.convertScaleAbs(r, alpha=0.00390625)
m = cv2.convertScaleAbs(r, alpha=0.03125)
cv2.add(r, m, dst=r)
g = cv2.bitwise_and(arr, 0b0000011111100000)
g = cv2.convertScaleAbs(g, alpha=0.125)
m = cv2.convertScaleAbs(g, alpha=0.015625, dst=m)
cv2.add(g, m, dst=g)
b = cv2.bitwise_and(arr, 0b0000000000011111)
b = cv2.convertScaleAbs(b, alpha=8)
m = cv2.convertScaleAbs(b, alpha=0.03125, dst=m)
cv2.add(b, m, dst=b)
img = cv2.merge([r, g, b])
if config.conf.droidcast_raw.rotate == 90:
img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
elif config.conf.droidcast_raw.rotate == 180:
img = cv2.rotate(img, cv2.ROTATE_180)
elif config.conf.droidcast_raw.rotate == 270:
img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
return img

View file

@ -145,7 +145,10 @@ class MuMuManager:
width = int(float(self.get_field_value(data, "resolution_width.custom")))
height = int(float(self.get_field_value(data, "resolution_height.custom")))
dpi = int(float(self.get_field_value(data, "resolution_dpi.custom")))
if config.conf.screencap_strategy == "droidcast" and self.app_kept_alive():
if (
config.conf.screencap_strategy.startswith("droidcast")
and self.app_kept_alive()
):
logger.error("MuMu12的droidcast截图策略下,需要关闭应用后台保活")
try:
self.set_app_kept_alive_false()

Binary file not shown.

View file

@ -1 +1 @@
.sss-container[data-v-2dffb781]{display:flex;width:100%;gap:8px}.wrapper[data-v-2dffb781]{white-space:pre-wrap;-webkit-user-select:text;user-select:text}.title[data-v-2dffb781]{font-size:18px;font-weight:500;margin-bottom:6px}p[data-v-d479d5bf]{margin:0 0 10px}.misc-container[data-v-d479d5bf]{display:flex;align-items:center;gap:12px}.header[data-v-d479d5bf]{margin:12px 0}.tasktable[data-v-6e42e723]{margin-top:4px;width:100%;overflow:scroll}.tasktable table[data-v-6e42e723]{border-collapse:collapse;width:100%}.btn-clear[data-v-6e42e723]{margin:4px}@media screen and (max-width: 1399px){.tasktable[data-v-6e42e723]{max-height:300px}.tasktable td[data-v-6e42e723]{width:10.2857142857%}.tasktable td[data-v-6e42e723]:first-child{width:10%}.tasktable td[data-v-6e42e723]:nth-child(2){width:18%}.tasktable thead[data-v-6e42e723]{position:sticky;top:0;background-color:#7ea5b4;z-index:1}}@media screen and (min-width: 1400px){.tasktable td[data-v-6e42e723]{width:10.2857142857%}.tasktable td[data-v-6e42e723]:first-child{width:10%}.tasktable td[data-v-6e42e723]:nth-child(2){width:18%}.tasktable thead[data-v-6e42e723]{background-color:#7ea5b4}}.class1[data-v-6e42e723]{background-color:var(--06f96d6a);text-align:center;vertical-align:middle}.class2[data-v-6e42e723]{background-color:var(--292d8683);text-align:center;vertical-align:middle}.custom-tag[data-v-6e42e723]{width:100%;height:100%;justify-content:space-between}.today[data-v-6e42e723]{font-weight:400;font-size:12px}.activity[data-v-6e42e723]{align-items:center;gap:4px!important}.card-title[data-v-6e42e723]{transition:.3s}.disabled[data-v-6e42e723]{color:var(--6d545694)}.form-item[data-v-6e42e723]{margin:0 0 4px}p[data-v-748f667d]{margin:2px 0}h4[data-v-748f667d]{margin:12px 0 8px}table[data-v-748f667d]{width:100%}td[data-v-748f667d]:nth-child(1){width:80px}.ignore-blacklist[data-v-748f667d]{margin-bottom:10px;display:flex;gap:12px}.h4[data-v-748f667d]{font-size:16px;font-weight:500}.maa-shop[data-v-748f667d]{margin:8px 0}.item[data-v-748f667d]{font-weight:500;font-size:16px}p[data-v-707c6f48]{margin:0 0 8px}h4[data-v-707c6f48]{margin:12px 0 10px}.big-table[data-v-707c6f48]{margin-top:10px;max-width:320px}.big-table th[data-v-707c6f48]{text-align:center}.big-table tr[data-v-707c6f48]{width:70px}.big-table td[data-v-707c6f48]{height:24px}.big-table td[data-v-707c6f48]:nth-child(1){width:70px;text-align:center}.big-table td[data-v-707c6f48]:nth-child(2){width:420px}.final[data-v-707c6f48]{margin:16px 0 0}.item[data-v-e95953da]{font-weight:500;font-size:16px}.n-divider[data-v-e95953da]:not(.n-divider--vertical){margin:6px 0}.subtitle[data-v-31c5d919]{margin:12px 0 6px}.misc-container{margin-top:12px;display:flex;align-items:center;gap:12px}.email-title[data-v-7cbc22ce]{width:100%}.expand[data-v-7cbc22ce]{flex-grow:1}.email-table[data-v-7cbc22ce]{width:100%;margin-bottom:12px}.email-test[data-v-7cbc22ce]{display:flex;align-items:center;gap:16px}.email-mode[data-v-7cbc22ce]{margin-left:20px}.email-label[data-v-7cbc22ce]{width:68px}p[data-v-7cbc22ce]{margin:0 0 10px}.mt-16[data-v-7cbc22ce]{margin-top:16px}.threshold[data-v-43df5096]{display:flex;align-items:center;gap:14px;width:100%}.mower-basic[data-v-43df5096]{width:100%}.mower-basic td[data-v-43df5096]:nth-child(1){width:120px}.mower-basic td[data-v-43df5096]:nth-child(3){padding-left:6px;width:40px}.riic-conf[data-v-43df5096]{width:100%}.riic-conf td[data-v-43df5096]:nth-child(1){width:130px}.riic-conf td[data-v-43df5096]:nth-child(3){padding-left:12px;width:120px}.coord td[data-v-43df5096]{width:120px}.coord td[data-v-43df5096]:nth-child(1),.coord td[data-v-43df5096]:nth-child(3){width:30px}.coord td[data-v-43df5096]:nth-child(2){padding-right:30px}.coord-label[data-v-43df5096]{width:40px;padding-left:8px}p[data-v-43df5096]{margin:0 0 8px}h4[data-v-43df5096]{margin:12px 0 10px}.time-table[data-v-43df5096]{width:100%;margin-bottom:12px}.time-table td[data-v-43df5096]:nth-child(1){width:40px}.scale[data-v-43df5096]{width:60px;text-align:right}.scale-apply[data-v-43df5096]{margin-left:24px}.waiting-table th[data-v-43df5096],.waiting-table td[data-v-43df5096]{padding:4px;min-width:70px;width:100px}.waiting-table th[data-v-43df5096]:first-child,.waiting-table td[data-v-43df5096]:first-child{width:auto;padding:4px 8px}@media (max-width: 1399px){.grid-two{margin:0 0 -10px;width:100%;max-width:600px}.grid-left{display:grid;row-gap:10px;grid-template-columns:100%}.grid-right{display:grid;row-gap:10px;grid-template-columns:100%;margin-top:10px}}@media (min-width: 1400px){.grid-two{display:grid;grid-template-columns:minmax(0px,1fr) minmax(0px,1fr);align-items:flex-start;gap:5px}.grid-left,.grid-right{display:grid;gap:5px;grid-template-columns:100%;max-width:600px}}.n-divider:not(.n-divider--vertical){margin:14px 0 8px}
.sss-container[data-v-2dffb781]{display:flex;width:100%;gap:8px}.wrapper[data-v-2dffb781]{white-space:pre-wrap;-webkit-user-select:text;user-select:text}.title[data-v-2dffb781]{font-size:18px;font-weight:500;margin-bottom:6px}p[data-v-d479d5bf]{margin:0 0 10px}.misc-container[data-v-d479d5bf]{display:flex;align-items:center;gap:12px}.header[data-v-d479d5bf]{margin:12px 0}.tasktable[data-v-6e42e723]{margin-top:4px;width:100%;overflow:scroll}.tasktable table[data-v-6e42e723]{border-collapse:collapse;width:100%}.btn-clear[data-v-6e42e723]{margin:4px}@media screen and (max-width: 1399px){.tasktable[data-v-6e42e723]{max-height:300px}.tasktable td[data-v-6e42e723]{width:10.2857142857%}.tasktable td[data-v-6e42e723]:first-child{width:10%}.tasktable td[data-v-6e42e723]:nth-child(2){width:18%}.tasktable thead[data-v-6e42e723]{position:sticky;top:0;background-color:#7ea5b4;z-index:1}}@media screen and (min-width: 1400px){.tasktable td[data-v-6e42e723]{width:10.2857142857%}.tasktable td[data-v-6e42e723]:first-child{width:10%}.tasktable td[data-v-6e42e723]:nth-child(2){width:18%}.tasktable thead[data-v-6e42e723]{background-color:#7ea5b4}}.class1[data-v-6e42e723]{background-color:var(--06f96d6a);text-align:center;vertical-align:middle}.class2[data-v-6e42e723]{background-color:var(--292d8683);text-align:center;vertical-align:middle}.custom-tag[data-v-6e42e723]{width:100%;height:100%;justify-content:space-between}.today[data-v-6e42e723]{font-weight:400;font-size:12px}.activity[data-v-6e42e723]{align-items:center;gap:4px!important}.card-title[data-v-6e42e723]{transition:.3s}.disabled[data-v-6e42e723]{color:var(--6d545694)}.form-item[data-v-6e42e723]{margin:0 0 4px}p[data-v-748f667d]{margin:2px 0}h4[data-v-748f667d]{margin:12px 0 8px}table[data-v-748f667d]{width:100%}td[data-v-748f667d]:nth-child(1){width:80px}.ignore-blacklist[data-v-748f667d]{margin-bottom:10px;display:flex;gap:12px}.h4[data-v-748f667d]{font-size:16px;font-weight:500}.maa-shop[data-v-748f667d]{margin:8px 0}.item[data-v-748f667d]{font-weight:500;font-size:16px}p[data-v-707c6f48]{margin:0 0 8px}h4[data-v-707c6f48]{margin:12px 0 10px}.big-table[data-v-707c6f48]{margin-top:10px;max-width:320px}.big-table th[data-v-707c6f48]{text-align:center}.big-table tr[data-v-707c6f48]{width:70px}.big-table td[data-v-707c6f48]{height:24px}.big-table td[data-v-707c6f48]:nth-child(1){width:70px;text-align:center}.big-table td[data-v-707c6f48]:nth-child(2){width:420px}.final[data-v-707c6f48]{margin:16px 0 0}.item[data-v-e95953da]{font-weight:500;font-size:16px}.n-divider[data-v-e95953da]:not(.n-divider--vertical){margin:6px 0}.subtitle[data-v-31c5d919]{margin:12px 0 6px}.misc-container{margin-top:12px;display:flex;align-items:center;gap:12px}.email-title[data-v-7cbc22ce]{width:100%}.expand[data-v-7cbc22ce]{flex-grow:1}.email-table[data-v-7cbc22ce]{width:100%;margin-bottom:12px}.email-test[data-v-7cbc22ce]{display:flex;align-items:center;gap:16px}.email-mode[data-v-7cbc22ce]{margin-left:20px}.email-label[data-v-7cbc22ce]{width:68px}p[data-v-7cbc22ce]{margin:0 0 10px}.mt-16[data-v-7cbc22ce]{margin-top:16px}.threshold[data-v-58034e1c]{display:flex;align-items:center;gap:14px;width:100%}.mower-basic[data-v-58034e1c]{width:100%}.mower-basic td[data-v-58034e1c]:nth-child(1){width:120px}.mower-basic td[data-v-58034e1c]:nth-child(3){padding-left:6px;width:40px}.riic-conf[data-v-58034e1c]{width:100%}.riic-conf td[data-v-58034e1c]:nth-child(1){width:130px}.riic-conf td[data-v-58034e1c]:nth-child(3){padding-left:12px;width:120px}.coord td[data-v-58034e1c]{width:120px}.coord td[data-v-58034e1c]:nth-child(1),.coord td[data-v-58034e1c]:nth-child(3){width:30px}.coord td[data-v-58034e1c]:nth-child(2){padding-right:30px}.coord-label[data-v-58034e1c]{width:40px;padding-left:8px}p[data-v-58034e1c]{margin:0 0 8px}h4[data-v-58034e1c]{margin:12px 0 10px}.time-table[data-v-58034e1c]{width:100%;margin-bottom:12px}.time-table td[data-v-58034e1c]:nth-child(1){width:40px}.scale[data-v-58034e1c]{width:60px;text-align:right}.scale-apply[data-v-58034e1c]{margin-left:24px}.waiting-table th[data-v-58034e1c],.waiting-table td[data-v-58034e1c]{padding:4px;min-width:70px;width:100px}.waiting-table th[data-v-58034e1c]:first-child,.waiting-table td[data-v-58034e1c]:first-child{width:auto;padding:4px 8px}@media (max-width: 1399px){.grid-two{margin:0 0 -10px;width:100%;max-width:600px}.grid-left{display:grid;row-gap:10px;grid-template-columns:100%}.grid-right{display:grid;row-gap:10px;grid-template-columns:100%;margin-top:10px}}@media (min-width: 1400px){.grid-two{display:grid;grid-template-columns:minmax(0px,1fr) minmax(0px,1fr);align-items:flex-start;gap:5px}.grid-left,.grid-right{display:grid;gap:5px;grid-template-columns:100%;max-width:600px}}.n-divider:not(.n-divider--vertical){margin:14px 0 8px}

File diff suppressed because one or more lines are too long

View file

@ -70,9 +70,10 @@ async function select_maa_adb_path() {
const screencap_options = computed(() => {
const result = [
{ label: 'ADB+Gzip(主动截图,无损压缩,兼容性好,速度慢)', value: 'adb' },
{ label: 'DroidCast(主动截图,有损压缩,速度较快)', value: 'droidcast' },
{ label: 'scrcpy(被动截图,有损压缩,视频流,特殊)', value: 'scrcpy' },
{ label: 'DroidCast_raw(主动,无损,较快)', value: 'droidcast_raw' },
{ label: 'ADB+Gzip(主动,无损,很慢)', value: 'adb' },
{ label: 'DroidCast(主动,有损,较快)', value: 'droidcast' },
{ label: 'scrcpy(被动,有损,特殊)', value: 'scrcpy' },
{ label: '自定义(以下命令需要向STDOUT打印截图文件)', value: 'diy' }
]
if (conf.value.emulator.name == 'MuMu12') {
@ -275,15 +276,6 @@ const message = useMessage()
>
通过共享内存的方式直接获取无损截图避免了编码解码跨进程传输等环节的开销截图速度远高于通用截图方案
</n-alert>
<n-alert
title="请关闭MuMu模拟器12的后台保活"
closable
style="margin: 8px 0"
type="warning"
v-if="conf.emulator.name == 'MuMu12' && conf.screencap_strategy == 'droidcast'"
>
在MuMu模拟器12上使用DroidCast截图方式需关闭后台保活或换用ADB+Gzip截图方式
</n-alert>
<n-form-item label="触控方案">
<n-select v-model:value="conf.control_strategy" :options="touch_options" />
</n-form-item>
@ -298,6 +290,26 @@ const message = useMessage()
</n-flex>
</n-radio-group>
</n-form-item>
<template v-if="conf.screencap_strategy == 'droidcast_raw'">
<n-form-item label="屏幕朝向">
<n-radio-group v-model:value="conf.droidcast_raw.orientation">
<n-flex>
<n-radio value="portrait">竖屏</n-radio>
<n-radio value="landscape">横屏</n-radio>
</n-flex>
</n-radio-group>
</n-form-item>
<n-form-item label="旋转截图" v-if="conf.screencap_strategy == 'droidcast_raw'">
<n-radio-group v-model:value="conf.droidcast_raw.rotate">
<n-flex>
<n-radio :value="0">不旋转</n-radio>
<n-radio :value="90">旋转90度</n-radio>
<n-radio :value="180">旋转180度</n-radio>
<n-radio :value="270">旋转270度</n-radio>
</n-flex>
</n-radio-group>
</n-form-item>
</template>
<n-form-item label="截图命令" v-if="conf.screencap_strategy == 'diy'">
<n-input
v-model:value="conf.custom_screenshot.command"
@ -313,8 +325,10 @@ const message = useMessage()
<div v-if="screenshot_success">
截图用时<n-text strong :type="screenshot_type">{{ elapsed }}ms</n-text>
</div>
<n-text type="error" v-else>截图失败</n-text>
<div>{{ reason }}</div>
<template v-else>
<n-text type="error">截图失败</n-text>
<div>{{ reason }}</div>
</template>
</template>
</n-flex>
<n-image