@ expo/ hub- 客户端
React app中的Render活iOS模拟器和Android模拟器. 连接通过
博览设备枢纽设备
DeviceScreen,并发送触控,键盘,设备控制.
所需资源
- 一个运行中的 Expo 设备枢纽服务器 。 要么启动一个有
expo-device-hub安装或运行 DevTools 插件npx expo-device-hub独立. 供应商连接到枢纽/vendor/serve-sim财务报告和审定财务报表/vendor/serve-emu路线. - 反应18或更新.
- 一个显示视频并发送输入的浏览器.
安装
npm install @expo/hub-client
连接到设备
将屏幕、控件和度量标准包入 DeviceClientProvider他们共享一个
连接,每个组件都会读取它所需要的客户端属性.
添加提供者
包装需要设备连接的组件 。 下面的组件全部使用此功能 提供者 :
import { DeviceClientProvider } from "@expo/hub-client";
export function LiveSession({ udid }: { udid: string }) {
return (
<DeviceClientProvider
key={udid}
platform="ios"
options={{
baseUrl: "http://localhost:3400/vendor/serve-sim",
device: udid,
streamMode: "webrtc",
}}
>
<Screen />
<Controls />
<Cpu />
</DeviceClientProvider>
);
}
用于Android, 使用 platform="android",您的服务-emu URL,以及诸如
'emulator-5554' 页:1 device。 。 。 .
使用 Expo DevTools 插件时,服务器 URL 包括
/_expo/plugins/expo-device-hub 在此之前 /vendor/serve-sim 或者说 /vendor/serve-emu。 。 。 .
streamMode 需要: 'mjpeg', (中文(简体) ), 'h264',或 'webrtc'. iOS支持所有三个.
Android 映射一种它无法使用的模式.
每个会话使用一个提供者 。 当切换会话时, 设置其 key 会话编号
所以旧的连接关闭。 设定 options.enabled 改为 false 切换或使用
platform={null} 没有选择设备.
读取状态和控件
解构你需要的一切 useDeviceClient() 电话 :
import { useDeviceClient } from "@expo/hub-client";
function Controls() {
const { status, pressButton, rotate, reload } = useDeviceClient();
const connected = status === "streaming";
return (
<div>
<span>{status}</span>
<button disabled={!connected} onClick={() => pressButton("home")}>
Home
</button>
<button disabled={!connected} onClick={rotate}>
Rotate
</button>
<button disabled={!connected} onClick={reload}>
Reload
</button>
</div>
);
}
状态和控制变化更新此组件。 FPS, 度量衡和日志变化没有.
状态可以是 'idle', (中文(简体) ), 'connecting', (中文(简体) ), 'reconnecting', (中文(简体) ), 'streaming' 或者说 'error'。 。 。 .
'connecting' 表示屏幕尚未显示帧: 显示装入 UI 。 它变成
'error' 30秒后没有帧,客户端继续重试. 之后
第一帧,丢失的流是 'reconnecting': 最后一个框架保留, 所以显示不装入 UI .
在iOS上,一个持续30秒的再连接会变成 'error',客户端继续重试.
framePresented 显示屏幕是否显示框架 。 一个新的设备,服务器,代号,
运输,或显示表面重新开始于 'connecting'读取 error 最后一个
失败消息 .
读取设备设置
deviceSettingsStatus 独立设置两个平台的可用性
视频和应用程序活动。 没错 'idle' 被禁用时, 'loading' 发现时
然后,第一个读,然后 'ready' 或者说 'error'设置在5后读取超时
秒数; 'ready' 包括空设置.
import { useDeviceClient } from "@expo/hub-client";
function DeviceSettingsSummary() {
const { deviceSettingsStatus, deviceSettings } = useDeviceClient();
if (deviceSettingsStatus === "idle") return null;
if (deviceSettingsStatus === "loading") return <span>Loading settings…</span>;
if (deviceSettingsStatus === "error") return <span>Settings unavailable</span>;
return <span>Appearance: {deviceSettings?.appearance ?? "Unavailable"}</span>;
}
刷新失败报告 'error' 并保留缓存值。 使用 'ready' 启用
控件; 恢复它而不再次显示初始装入状态.
显示屏幕
使用 useDeviceScreenClient 与 DeviceScreen。它跟踪屏幕和输入变化
不更新参数或日志.
DeviceScreen 填充其父母,因此给父母一个大小 position: relative。 。 。 .
displayScreen 计算旋转后的屏幕大小.
DeviceScreen 没有绘制背景、 装入状态或出错: 自己绘制, 结束
它,从 status, (中文(简体) ), error,以及 framePresented。 。 。 .
import { DeviceScreen, displayScreen, useDeviceScreenClient } from "@expo/hub-client";
function Screen() {
const client = useDeviceScreenClient();
const screen = displayScreen(client.screen);
const aspectRatio = screen ? `${screen.width} / ${screen.height}` : "9 / 19.5";
const loading = client.status !== "error" && client.status !== "idle" && !client.framePresented;
return (
<div style={{ position: "relative", width: 360, aspectRatio, background: "#eee" }}>
<DeviceScreen client={client} />
{loading && <Spinner />}
{client.status === "error" && <p role="alert">{client.error}</p>}
</div>
);
}
以设备形状显示屏幕
导入 DeviceShape 从 @expo/hub-client/shapes 将屏幕剪接到形状
每个iPhone、iPad和像素模型的角,以及形状
显示 iPhone Duo 显示。 喜欢 DeviceScreen,它需要客户端, 它转
与装置. 它只是剪辑,所以它画不出来,而指针输入则继续工作. A 类
没有已知形状的模型没有剪辑。 不导入的应用程序
@expo/hub-client/shapes 不要捆绑形状.
import { DeviceScreen, useDeviceClient } from "@expo/hub-client";
import { DeviceShape, useScreenShape } from "@expo/hub-client/shapes";
function ShapedScreen({ model }: { model: string }) {
const client = useDeviceClient();
// The shape as the viewer sees it, turned with the device, for the box's proportions.
const shape = useScreenShape({ client, model });
const aspectRatio = shape ? `${shape.width} / ${shape.height}` : "9 / 19.5";
return (
<div style={{ position: "relative", width: 360, aspectRatio }}>
<DeviceShape client={client} model={model} style={{ position: "absolute", inset: 0 }}>
<DeviceScreen client={client} />
</DeviceShape>
</div>
);
}
客户端的设备名称是默认模式, 但模拟器可以报告它的名称
迟了 过 model 当您知道时: iOS 上的模拟设备类型名称, 例如
"iPhone 17 Pro",或者AVD硬件配置文件在Android上的代号,例如 "pixel_9"。 。 。 .
platform, (中文(简体) ), display, (中文(简体) ), orientation,以及 shape 也覆盖客户端,以及
没有客户,他们是唯一的来源,例如录音:
<DeviceShape
platform="ios"
model="iPhone Duo"
display="inner"
style={{ position: "absolute", inset: 0 }}
>
<video src={recordingUrl} />
</DeviceShape>
useScreenShape 返回布局的相同形状,例如框上方的宽比.
夹子保持角向右 以形状的自己的比例, 所以给盒子这些
比例,或者通过框的 aspectRatio 改为 DeviceShape。 。 。 .
screenShapeClipPath 返回 CSS clip-path 用于您无法包接的元素.
折叠设备
client.fold 具有可折叠设备的折叠状态,例如 iPhone Duo,并且是
null 其他设备. client.capabilities.fold 列出设备的命令
接受 : 角度范围、 命名的姿势及其连接角度和表格模式。 没错
false 当设备不提供折叠命令时。 只发送它列出的.
import { useDeviceClient } from "@expo/hub-client";
function FoldButtons() {
const { fold, capabilities } = useDeviceClient();
const controls = capabilities.fold;
if (!fold || !controls) return null;
const range = controls.angle;
return (
<>
<span>
{fold.angle ?? "?"}° {fold.pose ?? "custom"} {fold.pending ? "(moving)" : ""}
</span>
{controls.poses.book && (
<button onClick={() => fold.setControl({ control: "pose", value: "book" })}>Book</button>
)}
{range && (
<button onClick={() => fold.setControl({ control: "angle", value: range.max })}>
Most open
</button>
)}
{controls.tableMode && fold.tableMode?.available && (
<button onClick={() => fold.setControl({ control: "tableMode", value: true })}>
Table Mode
</button>
)}
</>
);
}
fold.tableMode 财务报告和审定财务报表 fold.physical 只存在于拥有它们的设备上。 这个
显示一个词汇, FOLD_POSES数字 : closed, (中文(简体) ), open, (中文(简体) ), laptop, (中文(简体) ), book,以及
tent。 。 。 . fold.observe 从请求到命令
最终结果,无论哪个控制发送。 队列中的命令, 更新的将只获得
请求.
显示每个可折叠的显示
在一个iPhone Duo上, client.displays 列出 cover 编辑 inner 显示。 附加
每个显示的元素类型 client.videoKind 名称, 要流出每个显示
你自己来 虽然任何显示都有元素,但客户端会停放平流
status 跟踪当前显示。 将输入发送给显示 id,在那个中
显示坐标。 客户端放弃了从显示开始的手势
设备不显示( e)client.screen.display) (中文(简体) ).
import { useDeviceClient } from "@expo/hub-client";
function Displays() {
const client = useDeviceClient();
// A tap in the middle of the inner display.
const tapInner = () => {
client.sendTouch({ phase: "begin", display: "inner", x: 0.5, y: 0.5 });
client.sendTouch({ phase: "end", display: "inner", x: 0.5, y: 0.5 });
};
return (
<>
{client.displays?.map((display) =>
client.videoKind === "video" ? (
<video key={display.id} ref={display.attachVideo} autoPlay muted playsInline />
) : client.videoKind === "canvas" ? (
<canvas key={display.id} ref={display.attachVideo} />
) : (
<img key={display.id} ref={display.attachVideo} alt="" />
),
)}
<button onClick={tapInner}>Tap the inner display</button>
</>
);
}
deviceModelUrl 是设备的3D模型,Xcode用于iPhone Duo. 枢纽的三维
预览用它 displays 财务报告和审定财务报表 fold.observe。 。 。 .
显示 CPU 使用情况
使用 useDeviceClientSelector 当您需要大对象的一个值时。 本构成部分
当最新的CPU百分比变化时更新,即使内存或网络读取有变化.
import { useDeviceClientSelector } from "@expo/hub-client";
function Cpu() {
const cpuPct = useDeviceClientSelector((client) => client.activity?.samples.at(-1)?.cpuPct);
return <span>{cpuPct == null ? "Waiting for metrics" : `CPU ${cpuPct.toFixed(1)}%`}</span>;
}
阅读 activity 与 useDeviceClient 任何部分随时更新
联 合 国 activity 变化.
保持更新焦点
- 读取您组件体内的状态, 以便更新设备 。 数据仅读取于 a
事件处理器可能过时 。 诸如:
client.rotate()使用最新的回调. - 解结构时选择命名属性 。 传播客户端或使用
{ status, ...rest }订阅所有财产。 阅读activity.samples全体订阅者activity对象. - 保持选择者简单。 返回数字、字符串、布尔或已存在的对象。 狦 构建新对象,通过比较函数作为第二个参数.
- 将客户端及其数据视为只读 .
连接到带有符号的枢纽
一个枢纽开始于 npx expo-device-hub --require-token 需要它的会话符号
请求。 Hub仪表板本身不需要额外的: 打开与符号的链接
浏览器是覆盖每个请求的饼干。 在另一个来源的一页 没有这样的饼干,
因此,把信使交给提供者:
<DeviceClientProvider
key={udid}
platform="ios"
options={{
baseUrl: "http://127.0.0.1:3400/vendor/serve-sim",
device: udid,
streamMode: "mjpeg",
token,
}}
>
<Screen />
</DeviceClientProvider>
用于Android, 使用 platform="android" 编辑 /vendor/serve-emu URL (中文(简体) ).
客户端发送符号为 Authorization: Bearer <token>作为 serve-sim.token.<token> 或者说
serve-emu.token.<token>
和作为 ?token= 仅当浏览器无法设置信头时( MJPEG)
<img>, (中文(简体) ), EventSource) (中文(简体) ).
该符号不取代 CORS 。 以 --cors-origin 和您的页面来源,
每个来源一次:
npx expo-device-hub --require-token --cors-origin https://example.com
旗帜适用于iOS和Android. 没有它,只有回路源的一页,例如
http://localhost:5173,可以读取Hub的答复. 对于Android,Hub也必须听
一个回转地址。 在该页面中,Android拒绝 HTTP 请求更改设备状态,例如
作为旋转、截图和设备设置。 iOS 拒绝 service-sim 的控制套接字,它携带
日志、事件、衡量标准、设备设置、位置和应用程序动作.
调用设备控制
每个控制者都生活在 DeviceClient控制是无操作的,而没有任何连接,
而每个后端都会忽略其平台没有的按钮.
// Hardware buttons: 'home' | 'back' | 'recents' | 'power' | 'appSwitcher' | 'hideKeyboard'
client.pressButton("home");
client.rotate();
// client.capabilities.orientation is false, or { orientations: DeviceOrientation[] } with the
// poses that setOrientation accepts. client.orientation shows the choice at once, then the
// backend's report; read client.orientationPending and client.orientationError like location.
client.setOrientation("landscape_left");
// Observed iOS devices separate app screenOrientation from physical deviceOrientation.
// A null observation is unknown; omitted fields mean the helper lacks this contract.
// screen.orientation follows the app interface in this mode; displayScreen/streamGeometry
// use physical pose for framing and map video/input accordingly. Duo and older helpers
// retain their existing geometry. client.orientation follows physical pose on observed iOS,
// including Duo, so portrait-only apps do not reset the Rotate cursor;
// Native-pose Duo keeps its clockwise physical order; older Duo uses legacy names.
// Ordinary observed iOS uses CLOCKWISE_ORIENTATIONS; older helpers use DEVICE_ORIENTATIONS.
// framesUpright means the backend already sends upright frames (serve-emu).
// While an Android recording could break, client.recordingLock is "status-unknown" or
// "recording" and rotate, setOrientation and the stream setters are silently ignored. Disable
// those controls from it. client.orientationUnavailable gives the reason orientation cannot
// change now, also while the iOS input socket cannot take input.
client.reload(); // reload the running React Native bundle
client.setAppearance("dark"); // 'light' | 'dark'; read it back from client.appearance
// { blob, artifact, metadata? }, or null when capture fails. `artifact` is the session artifact
// outcome: { status: 'saved' } | { status: 'disabled' } | { status: 'failed', error?: string }
// | null (null for a backend that does not report one). `metadata` is the capture's
// { width, height, displayRotation, screen? }; it is absent on older servers, and a zero width
// or height or a null displayRotation means unknown.
const capture = await client.screenshot();
if (capture?.artifact?.status === "failed")
console.warn("not saved to session artifacts", capture.artifact.error);
// Input is normalized to 0..1 of the screen, so it works for every device size.
client.sendTouch({ phase: "begin", x: 0.5, y: 0.5 });
client.sendTouch({ phase: "end", x: 0.5, y: 0.5 });
client.sendKey({ phase: "down", code: "KeyA", key: "a", repeat: false });
client.sendKey({ phase: "up", code: "KeyA", key: "a", repeat: false });
// Logs are off until you attach them.
client.attachLogs();
client.logs; // DeviceLog[]
client.detachLogs();
设备设置、相机种子、无障碍树、位置、,
和应用程序权限只在某些后端上可用. 检查 client.capabilities 在此之前
你显示他们的控制。 全数 DeviceClient 对每个字段都有评论的合约是:
内 src/types.ts。 。 。 .
远程连接
设定 options.baseUrl 打开 DeviceClientProvider 到远程流线服务器 :
- iOS: (英语)
https://hub.example.test/device-hub/vendor/serve-sim - 机器人:
https://hub.example.test/device-hub/vendor/serve-emu
iOS 嵌入
将 iOS 屏幕嵌入到另一个站点时, 通过服务对象的 公用 service- sim Mount
/api 财务报告和审定财务报表 /helper 作为 baseUrl,例如 https://sim.example.test/preview/session。 。 。 .
流和输入 URL 然后使用该服务器。 开始服务图像
--cors-origin <origin> 用于嵌入页的出处,例如
--cors-origin http://localhost:8081没有它,执行-ws套接字关闭,和日志,
事件、参数和用户界面请求停止,即使两个服务器运行 localhost。 。 。 .
前景应用图标不需要exec-ws. 中心客户端从服务器上读取
/api/apps/icon 路径,一个普通的获取,何时 /api 宣传路线。 旧服务器发送
图标.
开始一个服务图像 --require-token,例如EAS模拟预览会话,需要
它的会话在每次请求时都具有象征意义。 通过它作为 options.token数字 :
<DeviceClientProvider
key={udid}
platform="ios"
options={{
baseUrl: "https://sim.example.test/preview/session",
device: udid,
streamMode: "mjpeg",
token,
}}
>
<Screen />
</DeviceClientProvider>
符号不替换 --cors-origin。 。 。 .
许可证
麻省理工学院