mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-01 03:12:37 +08:00
[perf] adapt CLI preview rate and improve frame pacing
This commit is contained in:
@@ -78,7 +78,13 @@ crossdesk-cli 123456789 --renderer ansi --fps 8
|
||||
|
||||
## 预览与背压
|
||||
|
||||
`--fps 1..30` 是本地输出目标。未显式指定时,共享内存探测成功默认使用 30 FPS;ANSI、内联 Kitty 和 bridge 默认仍为 8 FPS。自动选择的共享内存路径运行中退回内联时,目标也恢复为 8;显式设置的 `--fps` 始终保留。对端声明 PreviewSubscription 时,目标不超过 15 映射到合法 1/8/15 FPS(15 需要对应能力);超过 15 则请求项目已有的交互模式:协议 `fps=60` 是模式标记,编码设置 `frame_rate=30` 才是帧率,不再把 `--fps 30` 限制为 15 FPS。分辨率上限使用协商到的 v2,视频预算只在 bandwidth 能力存在时发送。`--quality low|balanced|high|ultra` 对应如下请求:
|
||||
`--fps 1..30` 是本地输出目标。交互 CLI 的 ANSI、内联 Kitty 和共享内存 Kitty 默认均为 30 FPS;共享内存失败回退时保持该目标。bridge 仍按宿主协议默认使用 8 FPS。
|
||||
|
||||
未显式指定 `--fps` 时,远端预览从 30 FPS 开始,根据完整一帧从转换开始到输出完成的耗时自动选择 8/15/30 FPS 请求;共享内存的上传确认等待也计入,等待源帧的空闲时间不计入。使用平滑估计、不同升降档阈值和至少 8 个新帧样本,持续变慢 2 秒才降档、持续恢复 4 秒才升档,保留供帧余量。15 FPS 仅在对端声明支持时使用;预览未获接受时不追加自动调节。视口变化、显示器切换和共享内存回退会重置耗时估计。显式 `--fps` 固定本地目标和对应的远端预览请求,关闭自动调节;例如 `--fps 30` 可用于与自动策略对比。
|
||||
|
||||
本地调度只在有新帧或需要重绘时消耗刷新时隙;短暂迟到保持原刷新节奏,长时间阻塞后跳过旧时隙,不连续补画过时帧。目标帧率不代表终端实际显示帧率。
|
||||
|
||||
对端声明 PreviewSubscription 时,预览请求不超过 15 映射到合法 1/8/15 FPS(15 需要对应能力);超过 15 则请求项目已有的交互模式:协议 `fps=60` 是模式标记,编码设置 `frame_rate=30` 才是帧率。分辨率上限使用协商到的 v2,视频预算只在 bandwidth 能力存在时发送。`--quality low|balanced|high|ultra` 对应如下请求:
|
||||
|
||||
| 档位 | 分辨率上限 | 质量枚举 | 视频预算 |
|
||||
|---|---|---|---|
|
||||
@@ -95,7 +101,7 @@ crossdesk-cli 123456789 --renderer ansi --fps 8
|
||||
|
||||
测量像素渲染帧率时使用 release 构建;debug 的 `-O0` 面积采样开销明显更高。共享内存中的像素不经过终端字节预算,但仍受转换和终端消费速度限制;8MiB/s 是内联路径的输出上限,不是帧率保证。解码、像素转换、压缩、终端消费速度和帧大小都会影响结果。退出时,未完成画面最多继续写入 2s;取消图像传输及恢复屏幕的短写最多另重试 500ms。POSIX 立即恢复 termios 并清理输入,不等待输出排空;如果终端始终不消费输出,屏幕清理只能尽力发送。
|
||||
|
||||
统计包括收到帧数、被替换帧数、非法帧、等待帧数、实际完整提交次数、输出字节数、运行秒数、最近本地排队时间、网络码率/RTT/SRTP。通过计数差/时间差可得到区间 FPS;交互连接结束日志另给平均提交 FPS,以及实际 `renderer` 和 `output_bytes_per_second` 预算。`kitty_transport=shared_memory/inline/none` 标明生效路径,`target_fps` 是当前目标,`frame_processing_ms` 是平均本地布局/转换/传输准备耗时,`shared_ack_ms` 是平均上传命令写完到解析成功确认的耗时,`shared_fallbacks` 记录运行时回退次数。`client.log` 摘要的 `startup_ms` 记录从发起连接到连接成功(`connected`)、首个取出用于显示的视频帧入队(`first_frame`)、首帧显示命令完整写入终端(`first_output`)的毫秒数;未到达的阶段为 `null`。写入 TTY 完成不等于屏幕显示完成,本地排队时间不是端到端延迟。
|
||||
统计包括收到帧数、被替换帧数、非法帧、等待帧数、实际完整提交次数、输出字节数、运行秒数、最近本地排队时间、网络码率/RTT/SRTP。通过计数差/时间差可得到区间 FPS;交互连接结束日志另给平均提交 FPS,以及实际 `renderer` 和 `output_bytes_per_second` 预算。`kitty_transport=shared_memory/inline/none` 标明生效路径,`target_fps` 是本地目标,`preview_fps` 是最近请求的远端预览目标(并非已生效或实测帧率),`adaptive_preview` 表示是否开启自动调节,`preview_adjustments` 记录切档次数;每次切档另记录 `preview_fps` 事件。`frame_service_ms` 是最近完整帧处理与输出耗时的平滑估计,`frame_processing_ms` 是平均本地布局/转换/传输准备耗时,`shared_ack_ms` 是平均上传命令写完到解析成功确认的耗时,`shared_fallbacks` 记录运行时回退次数。`client.log` 摘要的 `startup_ms` 记录从发起连接到连接成功(`connected`)、首个取出用于显示的视频帧入队(`first_frame`)、首帧显示命令完整写入终端(`first_output`)的毫秒数;未到达的阶段为 `null`。写入 TTY 完成不等于屏幕显示完成,本地排队时间不是端到端延迟。
|
||||
|
||||
## 输入与退出
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
- ANSI 转义序列模拟器验证最终屏幕一致,覆盖全部四象限字形、8/24/256 色、纯背景及色板复用、首尾单元、局部修改、视口/偏移变化和可独立绘制的 bridge 字符行。未向工程新增测试文件或用例。
|
||||
- 本机 `-O2`、1920×1080 输入、240×100 字符视口、每项 24 次转换:平滑画面约 6.26 → 4.83 ms/帧,随机亮度噪声约 7.75 → 5.72 ms/帧。保留面积滤波、低对比合并及边界连续性约束。
|
||||
- 240×100 真彩色合成网格、每项 100 次输出生成:整帧约 2.03 → 1.43 ms;120×50 单元的局部修改约 1.02 → 0.36 ms,输出 461,743 → 231,026 字节;2×3 单元的光标修改约 0.072 → 0.013 ms,输出 27,763 → 262 字节。纯背景色块整帧输出 875,352 → 449,826 字节。
|
||||
- 这些结果衡量转换、编码及输出量,不包含实际终端绘制或网络传输。整帧高色彩变化图案仍需约 923 KB,仍受 1 MiB/s 输出预算限制;不能将转换耗时的倒数作为远程显示帧率。默认本地目标为 8 FPS,`--fps 15` 可请求更高目标,预览订阅还需要对端支持 15 FPS。
|
||||
- 这些结果衡量转换、编码及输出量,不包含实际终端绘制或网络传输。整帧高色彩变化图案仍需约 923 KB,仍受 1 MiB/s 输出预算限制;不能将转换耗时的倒数作为远程显示帧率。当时默认本地目标为 8 FPS,后续默认策略见“默认 30 FPS 与供帧自适应”。
|
||||
- 额外检查代码、模拟器、基准和产物均位于工程外临时目录;未进行跨平台终端或真实远端帧率验收。
|
||||
|
||||
## Kitty 帧间黑屏与本地吞吐修复(2026-10-30)
|
||||
@@ -104,7 +104,7 @@
|
||||
## 本地 Kitty 共享内存与 30 FPS 目标(2026-10-30)
|
||||
|
||||
- 本地 POSIX Kitty 增加共享内存能力探测、唯一名称、0600 权限、上传确认和 500ms 超时回退。单个待确认共享缓冲区不超过 1920×1080×3 字节;确认前保留旧画面,退出释放未确认对象。Windows 暂保留内联路径,SSH 不发共享内存探测。
|
||||
- 共享内存可用且未显式指定 `--fps` 时默认目标 30;自动回退内联恢复 8,显式帧率保持不变。目标大于 15 使用现有预览协议的交互模式 `fps=60, frame_rate=30`;没有新增协议取值或修改被控端。
|
||||
- 当时共享内存可用且未显式指定 `--fps` 时默认目标 30;自动回退内联恢复 8,显式帧率保持不变。后续默认策略见“默认 30 FPS 与供帧自适应”。目标大于 15 使用现有预览协议的交互模式 `fps=60, frame_rate=30`;没有新增协议取值或修改被控端。
|
||||
- 工程外 ASan/UBSan 检查通过:110 组随机帧/视口/宽高比/缩放/细节模式,整数面积滤波相对原浮点参考的每通道最大误差为 1/255;内联 Kitty 和 ANSI 保持原输出。覆盖共享内存权限、读取内容、命名、容量限制、双方 unlink、分片确认及非法/溢出/重复 ID 回应。
|
||||
- 使用实际 release CLI 对象代码、1920×1080 合成噪声 NV12 连续回调、1568×1088 报告视口的 PTY 测试:内联完整接收约 4.06 FPS,共享内存约 29.04 FPS;平均本地处理约 166.6 → 12.3 ms,终端字节流约 1,703,174 → 188 字节/帧。共享内存仍传输完整 RGB,并非总数据量只有 188 字节;数据复制、终端绘制和实际网络另有成本。
|
||||
- 显式 8 FPS、探测不支持/静默、第三帧上传报错/丢失确认、80ms 延迟确认、SSH 禁用探测均通过。回退前不显示未确认帧,运行时回退仅触发一次,并正确恢复自动目标和协议请求;所有共享对象均清理。第二帧上传后立即退出时,首帧仍是已提交画面,未确认对象及两个图像 ID 均清理。
|
||||
@@ -112,6 +112,16 @@
|
||||
- 用户在本机 Ghostty 运行新版 release 后反馈“明显流畅了”。额外测试源码、数据、构建夹具和产物均位于系统临时目录,未在工程或子模块新增测试。
|
||||
- 该次实机日志确认 `kitty_transport=shared_memory`、目标 30、预览请求已接受、运行时回退 0 次。会话约 13.61s 完整提交 273 帧,平均提交约 20.06 FPS(包含约 1.49s 首帧启动等待);平均本地处理约 10.87ms、共享内存确认约 0.42ms。该日志记录提交速率,不测量终端 GPU 呈现频率。
|
||||
|
||||
## 默认 30 FPS 与供帧自适应(2026-10-30)
|
||||
|
||||
- 交互 CLI 默认本地目标统一为 30 FPS,Kitty 共享内存回退不再改写该目标;远端请求独立在 8/15/30 FPS 档位间自适应,15 需要对端能力。显式 `--fps` 保留固定请求,bridge 默认仍为 8。
|
||||
- macOS arm64 debug/release 构建、release `doctor --json` 和帮助输出检查通过。工程外 ASan/UBSan 验证了升降档阈值、迟滞、恢复、15 FPS 能力缺失、显式帧率、源帧稀疏、请求拒绝以及估计重置。
|
||||
- 使用真实 release CLI 对象代码、支持预览确认和动态供帧的假传输、1920×1080 合成 NV12 与 1568×1088 像素视口进行 PTY 验证。固定 30 FPS 源的共享内存路径完整接收约 30 FPS,未触发自动降档;不再在没有新帧时提前消耗刷新时隙。
|
||||
- 高熵内联路径实际约 4 FPS 时,自动请求由 30 降为 8,本地目标保持 30;显式 `--fps 30` 在同条件下仍约 4 FPS,远端请求不变。上传报错/超时和探测静默均保持本地 30 目标,随后按完整帧耗时调节供帧。
|
||||
- 90ms 上传确认延迟使实际接收约 9 FPS,远端请求自动调整为 15;缺少 15 FPS 能力时不发送该档位。200ms 延迟后恢复正常确认,验证远端请求按 30 → 8 → 30 恢复。源仅提供 2 FPS、对端拒绝预览时均未误触发自动降档。
|
||||
- SSH 环境保持原有 1MiB/s 字节预算且不探测共享内存;慢速输出降远端请求但保留本地目标。ANSI 默认 30/显式 15 和 bridge 默认 8/帧 ACK 通过。所有 Kitty 场景继续验证不显示未确认帧、最多两个终端图像、共享对象释放及退出后终端模式恢复。
|
||||
- 以上数字是合成源和伪终端的完整输出接收率,不包含真实视频解码、网络或 Ghostty GPU 呈现。全部额外测试代码、数据、构建脚本和产物位于工程外系统临时目录;本次未新增工程内测试,尚未对新版进行 Ghostty 实窗验收。
|
||||
|
||||
## 实测边界
|
||||
|
||||
本地检查不涵盖 Linux / Windows 完整构建、跨平台远程控制、Kitty/SSH 实际终端显示、真实 Pi TUI 或长时间资源占用。五个平台 CI 配置与 npm 包名均不代表已发布或已验收。
|
||||
|
||||
+111
-12
@@ -84,6 +84,63 @@ private:
|
||||
double credit_ = 1048576;
|
||||
Clock::time_point last_;
|
||||
};
|
||||
// Measure occupied frame time, not the interval between incoming frames: a
|
||||
// slow source or a quiet desktop must not trap the next preview at a low rate.
|
||||
class AdaptivePreview {
|
||||
public:
|
||||
AdaptivePreview(int fps, bool enabled) : fps_(fps), enabled_(enabled) {}
|
||||
void Reset() {
|
||||
service_ms_ = 0;
|
||||
samples_ = 0;
|
||||
candidate_ = fps_;
|
||||
candidate_since_.reset();
|
||||
}
|
||||
std::optional<int> Observe(double ms, Clock::time_point now, bool fps15,
|
||||
bool accepted) {
|
||||
service_ms_ = samples_++ ? service_ms_ + .2 * (ms - service_ms_) : ms;
|
||||
if (!enabled_ || !accepted) {
|
||||
candidate_since_.reset();
|
||||
return {};
|
||||
}
|
||||
const double capacity = 1000 / std::max(.001, service_ms_);
|
||||
int next = fps_;
|
||||
// Request the next tier above sustainable output, leaving headroom for
|
||||
// fresh frames. Separate up/down thresholds prevent boundary oscillation.
|
||||
if (fps_ > 8 && capacity <= 6.4)
|
||||
next = 8;
|
||||
else if (fps_ == 30 && fps15 && capacity <= 12)
|
||||
next = 15;
|
||||
else if (fps_ < 30 && capacity >= 13.5)
|
||||
next = 30;
|
||||
else if (fps_ == 8 && capacity >= 7.2)
|
||||
next = fps15 ? 15 : 30;
|
||||
if (next == fps_) {
|
||||
candidate_since_.reset();
|
||||
return {};
|
||||
}
|
||||
if (next != candidate_ || !candidate_since_) {
|
||||
candidate_ = next;
|
||||
candidate_since_ = now;
|
||||
}
|
||||
const auto hold = std::chrono::seconds(next < fps_ ? 2 : 4);
|
||||
if (samples_ < 8 || now - *candidate_since_ < hold)
|
||||
return {};
|
||||
fps_ = next;
|
||||
++adjustments_;
|
||||
candidate_since_.reset();
|
||||
return fps_;
|
||||
}
|
||||
int Fps() const { return fps_; }
|
||||
double ServiceMs() const { return service_ms_; }
|
||||
uint64_t Adjustments() const { return adjustments_; }
|
||||
|
||||
private:
|
||||
int fps_, candidate_ = 0;
|
||||
bool enabled_;
|
||||
double service_ms_ = 0;
|
||||
uint64_t samples_ = 0, adjustments_ = 0;
|
||||
std::optional<Clock::time_point> candidate_since_;
|
||||
};
|
||||
struct Tty {
|
||||
int out;
|
||||
RawTerminal terminal;
|
||||
@@ -326,9 +383,11 @@ int View(SessionOptions options, const std::string &device,
|
||||
}
|
||||
shared_image.Reset();
|
||||
}
|
||||
bool automatic_fast_fps = shared_enabled && !fps_explicit;
|
||||
if (automatic_fast_fps)
|
||||
if (!fps_explicit)
|
||||
options.fps = 30;
|
||||
AdaptivePreview preview(options.fps, !fps_explicit);
|
||||
const auto frame_interval = std::chrono::duration_cast<Clock::duration>(
|
||||
std::chrono::duration<double>(1.0 / options.fps));
|
||||
ControllerSession s;
|
||||
Input input;
|
||||
input.control = control;
|
||||
@@ -339,6 +398,7 @@ int View(SessionOptions options, const std::string &device,
|
||||
s.SetControl(control);
|
||||
std::optional<Grid> displayed, pending;
|
||||
bool pending_frame = false;
|
||||
std::optional<Clock::time_point> frame_started;
|
||||
bool shared_upload = false;
|
||||
std::optional<Clock::time_point> shared_deadline;
|
||||
std::string pending_clear;
|
||||
@@ -362,11 +422,26 @@ int View(SessionOptions options, const std::string &device,
|
||||
shared_deadline.reset();
|
||||
shared_image.Reset();
|
||||
++shared_fallbacks;
|
||||
if (automatic_fast_fps) {
|
||||
automatic_fast_fps = false;
|
||||
options.fps = 8;
|
||||
s.SetPreview(options.fps, options.preview_width, options.preview_height,
|
||||
frame_started.reset();
|
||||
preview.Reset();
|
||||
};
|
||||
const auto complete_sample = [&] {
|
||||
if (!frame_started)
|
||||
return;
|
||||
const auto now = Clock::now();
|
||||
const double ms =
|
||||
std::chrono::duration<double, std::milli>(now - *frame_started).count();
|
||||
frame_started.reset();
|
||||
if (auto fps = preview.Observe(ms, now, s.SupportsPreviewFps15(),
|
||||
s.Connected() && s.PreviewAccepted())) {
|
||||
s.SetPreview(*fps, options.preview_width, options.preview_height,
|
||||
options.quality, options.max_bitrate_bps);
|
||||
std::cerr << json{{"event", "preview_fps"},
|
||||
{"target_fps", options.fps},
|
||||
{"requested_fps", *fps},
|
||||
{"frame_service_ms", preview.ServiceMs()}}
|
||||
.dump()
|
||||
<< "\n";
|
||||
}
|
||||
};
|
||||
const auto pump_output = [&] {
|
||||
@@ -379,6 +454,7 @@ int View(SessionOptions options, const std::string &device,
|
||||
pending.reset();
|
||||
pending_frame = false;
|
||||
++submitted;
|
||||
complete_sample();
|
||||
if (!first_output_at) {
|
||||
first_output_at = Clock::now();
|
||||
status_tick = *first_output_at;
|
||||
@@ -447,6 +523,7 @@ int View(SessionOptions options, const std::string &device,
|
||||
if (input.view_revision != view_revision) {
|
||||
view_revision = input.view_revision;
|
||||
redraw = true;
|
||||
preview.Reset();
|
||||
}
|
||||
const bool grid_resized =
|
||||
v.columns != last_view.columns || v.rows != last_view.rows;
|
||||
@@ -460,6 +537,7 @@ int View(SessionOptions options, const std::string &device,
|
||||
last_view = v;
|
||||
last_status_rows = status_rows;
|
||||
redraw = true;
|
||||
preview.Reset();
|
||||
status_tick = now;
|
||||
if (grid_resized)
|
||||
size_query_tick = now;
|
||||
@@ -535,10 +613,11 @@ int View(SessionOptions options, const std::string &device,
|
||||
status_tick = now + std::chrono::seconds(1);
|
||||
}
|
||||
if (output.Idle() && !shared_upload && now >= next) {
|
||||
next = now + std::chrono::milliseconds(1000 / options.fps);
|
||||
bool fresh = false;
|
||||
if (auto frame = s.TakeFrame(last_frame ? &*last_frame : nullptr);
|
||||
frame && frame->generation == s.Generation()) {
|
||||
if (!last_frame || last_frame->generation != frame->generation)
|
||||
preview.Reset();
|
||||
last_frame = std::move(frame);
|
||||
fresh = true;
|
||||
if (!first_frame_at)
|
||||
@@ -548,10 +627,17 @@ int View(SessionOptions options, const std::string &device,
|
||||
last_frame.reset();
|
||||
if (last_frame && (fresh || redraw)) {
|
||||
const auto render_started = Clock::now();
|
||||
if (fresh)
|
||||
// Keep cadence when only slightly late, skip missed slots after a
|
||||
// stall, and never consume a slot while waiting for a source frame.
|
||||
next = render_started > next + frame_interval
|
||||
? render_started + frame_interval
|
||||
: next + frame_interval;
|
||||
if (fresh) {
|
||||
frame_started = render_started;
|
||||
queue_ms = std::chrono::duration<double, std::milli>(
|
||||
now - last_frame->queued)
|
||||
.count();
|
||||
}
|
||||
pending = kitty ? Layout(*last_frame, v) : Convert(*last_frame, v);
|
||||
// Clear waiting text and Kitty margins without erasing the status bars.
|
||||
std::string clear;
|
||||
@@ -571,6 +657,8 @@ int View(SessionOptions options, const std::string &device,
|
||||
}
|
||||
}
|
||||
if (!shared_upload) {
|
||||
if (fresh && !frame_started)
|
||||
frame_started = Clock::now();
|
||||
output.Put(clear + (kitty ? Kitty(*last_frame, v, submitted % 2 != 0)
|
||||
: Difference(*pending, displayed)));
|
||||
waiting_visible = false;
|
||||
@@ -585,10 +673,16 @@ int View(SessionOptions options, const std::string &device,
|
||||
displayed = std::move(pending);
|
||||
pending.reset();
|
||||
pending_frame = false;
|
||||
complete_sample();
|
||||
}
|
||||
}
|
||||
}
|
||||
output.Wait(0);
|
||||
const auto wait_ms = std::chrono::ceil<std::chrono::milliseconds>(
|
||||
next - Clock::now())
|
||||
.count();
|
||||
output.Wait(0, output.Idle() && !shared_upload && wait_ms > 0
|
||||
? int(std::min<int64_t>(5, wait_ms))
|
||||
: 5);
|
||||
}
|
||||
auto final = s.Status();
|
||||
s.Disconnect();
|
||||
@@ -627,6 +721,10 @@ int View(SessionOptions options, const std::string &device,
|
||||
: shared_enabled ? "shared_memory"
|
||||
: "inline"},
|
||||
{"target_fps", options.fps},
|
||||
{"adaptive_preview", !fps_explicit},
|
||||
{"preview_fps", preview.Fps()},
|
||||
{"preview_adjustments", preview.Adjustments()},
|
||||
{"frame_service_ms", preview.ServiceMs()},
|
||||
{"frame_processing_ms", rendered ? render_ms / rendered : 0},
|
||||
{"shared_ack_ms", shared_acks ? shared_ack_ms / shared_acks : 0},
|
||||
{"shared_fallbacks", shared_fallbacks},
|
||||
@@ -917,9 +1015,10 @@ Ctrl+T and Ctrl+C pass through in control mode; another Ctrl+] leaves local mode
|
||||
Default ANSI detail: quadrant. Default quality: high (1920x1080); ultra requests up to 3840x2160.
|
||||
Cell aspect is character width/height: auto detects terminal geometry; use --cell-aspect to calibrate.
|
||||
Display index is zero-based. ANSI cannot reproduce full desktop text at fit scale.
|
||||
--fps controls local output. Local Kitty shared memory defaults to 30; other paths default to 8.
|
||||
Targets above 15 use the negotiated interactive 30 FPS stream; lower targets use 1/8/15 FPS previews.
|
||||
bridge: private NDJSON v1, credentials on a separate inherited descriptor.
|
||||
--fps controls local output (default 30). Without it, remote preview adapts to measured output speed.
|
||||
Explicit --fps keeps a fixed preview request. Kitty transport fallback keeps the local target unchanged.
|
||||
Preview requests above 15 use the negotiated interactive 30 FPS stream; lower requests use 1/8/15 FPS.
|
||||
bridge: private NDJSON v1, default 8 FPS, credentials on a separate inherited descriptor.
|
||||
No IP direct connection, audio playback, clipboard or file transfer support.
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
@@ -68,6 +68,8 @@ public:
|
||||
std::optional<DecodedFrame> TakeFrame(DecodedFrame *recycle = nullptr);
|
||||
nlohmann::json Status() const;
|
||||
bool Connected() const { return state_ == "connected"; }
|
||||
bool PreviewAccepted() const { return preview_accepted_; }
|
||||
bool SupportsPreviewFps15() const { return fps15_; }
|
||||
bool Finished() const { return state_ == "failed" || state_ == "closed"; }
|
||||
uint64_t Generation() const { return generation_; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user