修复了一些bug
This commit is contained in:
+91
-28
@@ -73,6 +73,7 @@ class GalleryCard {
|
||||
this.framePending = false;
|
||||
this.frameTimer = null;
|
||||
this.frameTimeout = null;
|
||||
this.frameTimeoutCount = 0;
|
||||
this.latestPixelBuffer = null;
|
||||
this.transportTimes = [];
|
||||
this.presentationTimes = [];
|
||||
@@ -87,7 +88,8 @@ class GalleryCard {
|
||||
this.reconnectTimer = null;
|
||||
this.disposed = false;
|
||||
this.ready = false;
|
||||
this.visible = true;
|
||||
this.intersecting = false;
|
||||
this.backendActive = null;
|
||||
this.lastFrameRequest = 0;
|
||||
this.lastObserveRequest = 0;
|
||||
this.node = elements.cardTemplate.content.firstElementChild.cloneNode(true);
|
||||
@@ -122,18 +124,44 @@ class GalleryCard {
|
||||
this.resizeObserver = new ResizeObserver(() => this.resize());
|
||||
this.resizeObserver.observe(this.shell);
|
||||
this.intersectionObserver = new IntersectionObserver(entries => {
|
||||
const wasVisible = this.visible;
|
||||
this.visible = entries[0]?.isIntersecting !== false;
|
||||
if (this.ready && wasVisible !== this.visible) this.send(this.visible ? "show" : "hide");
|
||||
if (this.visible) this.scheduleFramePump();
|
||||
const intersecting = entries[0]?.isIntersecting === true;
|
||||
if (this.intersecting === intersecting) return;
|
||||
this.intersecting = intersecting;
|
||||
this.syncActivity();
|
||||
}, {rootMargin: "160px"});
|
||||
this.intersectionObserver.observe(this.node);
|
||||
this.connect();
|
||||
}
|
||||
setSocketState(state, text) {
|
||||
this.socketState.dataset.state = state;
|
||||
this.socketState.querySelector("span").textContent = text;
|
||||
}
|
||||
categoryVisible() {
|
||||
return activeCategory === "全部" || this.definition.category === activeCategory;
|
||||
}
|
||||
displayVisible() {
|
||||
return !document.hidden && this.mode.id === activeMode && this.categoryVisible() && this.intersecting;
|
||||
}
|
||||
streamActive() {
|
||||
return !streamsPaused && this.displayVisible();
|
||||
}
|
||||
stopFramePump() {
|
||||
if (this.frameTimer !== null) clearTimeout(this.frameTimer);
|
||||
this.frameTimer = null;
|
||||
}
|
||||
syncActivity(force = false) {
|
||||
const visible = this.displayVisible();
|
||||
if (visible && ![WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) {
|
||||
this.connect();
|
||||
return;
|
||||
}
|
||||
const active = !streamsPaused && visible;
|
||||
if (!active) this.stopFramePump();
|
||||
if (this.ready && (force || this.backendActive !== active)) {
|
||||
this.send(active ? "show" : "hide");
|
||||
this.backendActive = active;
|
||||
}
|
||||
if (active) this.scheduleFramePump(true);
|
||||
}
|
||||
connect() {
|
||||
if (this.disposed || [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) return;
|
||||
clearTimeout(this.reconnectTimer);
|
||||
@@ -153,12 +181,13 @@ class GalleryCard {
|
||||
socket.addEventListener("close", () => {
|
||||
if (this.socket !== socket) return;
|
||||
this.ready = false;
|
||||
this.backendActive = null;
|
||||
this.framePending = false;
|
||||
clearTimeout(this.frameTimer); this.frameTimer = null;
|
||||
this.stopFramePump();
|
||||
clearTimeout(this.frameTimeout); this.frameTimeout = null;
|
||||
this.setSocketState("error", "连接关闭 · 自动重连");
|
||||
this.setSocketState("error", this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见");
|
||||
this.updateMotionStatus();
|
||||
if (!this.disposed) this.reconnectTimer = setTimeout(() => this.connect(), 1000);
|
||||
if (!this.disposed && this.displayVisible()) this.reconnectTimer = setTimeout(() => this.connect(), 1000);
|
||||
});
|
||||
socket.addEventListener("error", () => {
|
||||
if (this.socket === socket) this.setSocketState("error", "连接错误 · 自动重连");
|
||||
@@ -187,13 +216,13 @@ class GalleryCard {
|
||||
this.actions = data.actions?.data || [];
|
||||
this.telemetry = data.telemetry || {};
|
||||
this.ready = true;
|
||||
this.backendActive = null;
|
||||
this.node.dataset.ready = "true";
|
||||
this.send(this.visible ? "show" : "hide");
|
||||
this.syncActivity();
|
||||
this.node.querySelector(".card-controls").textContent = this.controls.length;
|
||||
this.node.querySelector(".card-actions").textContent = this.actions.length;
|
||||
this.setSocketState("ready", `${data.frame_mode?.strategy || "Core2"} 在线`);
|
||||
this.updatePerformance();
|
||||
this.scheduleFramePump(true);
|
||||
if (data.notice && !data.notice.includes("已创建")) toast(`${this.definition.title}:${data.notice}`);
|
||||
if (activeCard === this) { elements.menuStatus.textContent = data.notice || "后端状态已回读"; renderMenuBody(); }
|
||||
}
|
||||
@@ -214,6 +243,8 @@ class GalleryCard {
|
||||
this.node.querySelector(".perf-points").textContent = `${Number(dataShape.input_points || 0).toLocaleString()}→${Number(dataShape.rendered_elements || 0).toLocaleString()}`;
|
||||
this.node.querySelector(".perf-limit").textContent = limitNames[observer.limit_state] || observer.limit_state || "N/A";
|
||||
this.node.querySelector(".perf-event").textContent = observer.last_event || "none";
|
||||
this.node.querySelector(".perf-timeouts").textContent = this.frameTimeoutCount.toLocaleString();
|
||||
this.node.querySelector(".perf-pixel-age").textContent = this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—";
|
||||
this.updateObserverDashboard(observer);
|
||||
this.updateMotionStatus();
|
||||
const setLimitFlag = (selector, active, duration) => {
|
||||
@@ -244,14 +275,20 @@ class GalleryCard {
|
||||
pixelSignature(buffer, width, height, stride) {
|
||||
const bytes = new Uint8Array(buffer, 16, stride * height);
|
||||
let hash = (2166136261 ^ width ^ (height << 16)) >>> 0;
|
||||
for (let offset = 0; offset < bytes.length; offset += 64)
|
||||
const sampleCount = Math.min(4096, bytes.length);
|
||||
if (sampleCount <= 1) return Math.imul(hash ^ (bytes[0] || 0), 16777619) >>> 0;
|
||||
for (let index = 0; index < sampleCount; index++) {
|
||||
const offset = Math.floor(index * (bytes.length - 1) / (sampleCount - 1));
|
||||
hash = Math.imul(hash ^ bytes[offset], 16777619) >>> 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
updateMotionStatus(now = performance.now()) {
|
||||
let state = "waiting", label = "等待动态帧";
|
||||
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) {
|
||||
state = "stalled"; label = "像素流断开";
|
||||
} else if (!this.streamActive()) {
|
||||
state = "waiting"; label = streamsPaused && this.displayVisible() ? "像素流已暂停" : "非活动视图";
|
||||
} else if (this.lastPixelChangeAt && now - this.lastPixelChangeAt < 1500) {
|
||||
state = "moving"; label = `画面变化 ${this.changedPixelFrames.toLocaleString()}`;
|
||||
} else if (this.lastPixelReceivedAt && now - this.lastPixelReceivedAt < 1500) {
|
||||
@@ -268,11 +305,19 @@ class GalleryCard {
|
||||
receivePixels(buffer) {
|
||||
this.framePending = false;
|
||||
if (this.frameTimeout !== null) { clearTimeout(this.frameTimeout); this.frameTimeout = null; }
|
||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16) return;
|
||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16) {
|
||||
this.setSocketState("error", "像素帧无效 · 自动重连");
|
||||
this.socket?.close(1003, "invalid pixel frame");
|
||||
return;
|
||||
}
|
||||
const header = new DataView(buffer, 0, 16);
|
||||
const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
|
||||
const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true);
|
||||
if (magic !== "RVP1" || !width || !height || stride < width * 4 || buffer.byteLength < 16 + stride * height) return;
|
||||
if (magic !== "RVP1" || !width || !height || stride < width * 4 || buffer.byteLength < 16 + stride * height) {
|
||||
this.setSocketState("error", "像素帧协议错误 · 自动重连");
|
||||
this.socket?.close(1003, "invalid pixel frame");
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const signature = this.pixelSignature(buffer, width, height, stride);
|
||||
if (this.lastPixelSignature === null || signature !== this.lastPixelSignature) {
|
||||
@@ -282,6 +327,7 @@ class GalleryCard {
|
||||
this.lastPixelSignature = signature;
|
||||
this.lastPixelReceivedAt = now;
|
||||
this.latestPixelBuffer = buffer;
|
||||
this.setSocketState("ready", `${this.mode.strategy || "Core2"} 在线`);
|
||||
this.transportFps = this.recordRate(this.transportTimes, now);
|
||||
this.frameCount++;
|
||||
this.frameLabel.textContent = this.frameCount.toLocaleString();
|
||||
@@ -304,54 +350,63 @@ class GalleryCard {
|
||||
}
|
||||
this.presentationFps = this.recordRate(this.presentationTimes, time);
|
||||
}
|
||||
recordRate(history, now) {
|
||||
history.push(now);
|
||||
while (history.length > 2 && history[0] < now - 1000) history.shift();
|
||||
currentRate(history, now) {
|
||||
while (history.length && history[0] < now - 1000) history.shift();
|
||||
if (history.length < 2) return 0;
|
||||
return (history.length - 1) * 1000 / Math.max(1, history[history.length - 1] - history[0]);
|
||||
}
|
||||
recordRate(history, now) {
|
||||
history.push(now);
|
||||
return this.currentRate(history, now);
|
||||
}
|
||||
resize() {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
const rect = this.shell.getBoundingClientRect();
|
||||
this.send("resize", {width: Math.max(240, Math.round(rect.width)), height: Math.max(180, Math.round(rect.height))});
|
||||
}
|
||||
requestFrame(time, explicit = false) {
|
||||
if (streamsPaused && !explicit || !this.visible || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
if ((!explicit && !this.streamActive()) || (explicit && !this.displayVisible()) || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
if (!explicit && this.mode.id === "manual") return;
|
||||
const streamControl = this.controls.find(item => item.id === "pixel_stream_fps");
|
||||
const interval = this.mode.id === "low_latency" ? 1000 / Math.max(1, Math.min(60, Number(streamControl?.value || 30))) : 66;
|
||||
if (!explicit && time - this.lastFrameRequest < interval) return;
|
||||
if (!explicit && this.mode.id !== "low_latency" && time - this.lastFrameRequest < interval) return;
|
||||
this.lastFrameRequest = time;
|
||||
this.framePending = true;
|
||||
this.send("frame");
|
||||
this.frameTimeout = setTimeout(() => {
|
||||
this.framePending = false;
|
||||
this.frameTimeout = null;
|
||||
this.scheduleFramePump();
|
||||
this.frameTimeoutCount++;
|
||||
if (!this.streamActive() || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
this.setSocketState("error", "像素响应超时 · 正在恢复");
|
||||
this.syncActivity(true);
|
||||
}, 1500);
|
||||
}
|
||||
scheduleFramePump(reset = false) {
|
||||
if (this.mode.id !== "low_latency") return;
|
||||
if (reset && this.frameTimer !== null) { clearTimeout(this.frameTimer); this.frameTimer = null; }
|
||||
if (this.frameTimer !== null || this.framePending || streamsPaused || !this.visible ||
|
||||
!this.ready || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
if (reset) this.stopFramePump();
|
||||
if (this.frameTimer !== null || this.framePending || !this.streamActive() || !this.ready || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||
const streamControl = this.controls.find(item => item.id === "pixel_stream_fps");
|
||||
const interval = 1000 / Math.max(1, Math.min(60, Number(streamControl?.value || 30)));
|
||||
const delay = Math.max(0, this.lastFrameRequest + interval - performance.now());
|
||||
const delay = Math.max(0, Math.ceil(this.lastFrameRequest + interval - performance.now()));
|
||||
this.frameTimer = setTimeout(() => {
|
||||
this.frameTimer = null;
|
||||
this.requestFrame(performance.now());
|
||||
}, delay);
|
||||
}
|
||||
observe(time) {
|
||||
if (!this.ready || !this.visible || this.socket?.readyState !== WebSocket.OPEN || time - this.lastObserveRequest < 650) return;
|
||||
if (!this.ready || !this.displayVisible() || this.socket?.readyState !== WebSocket.OPEN || time - this.lastObserveRequest < 650) return;
|
||||
this.lastObserveRequest = time;
|
||||
this.transportFps = this.currentRate(this.transportTimes, time);
|
||||
this.presentationFps = this.currentRate(this.presentationTimes, time);
|
||||
this.send("gallery_observe", {client_metrics: {
|
||||
transport_fps: this.transportFps,
|
||||
presentation_fps: this.presentationFps,
|
||||
websocket_buffered_bytes: this.socket.bufferedAmount || 0,
|
||||
changed_pixel_frames: this.changedPixelFrames,
|
||||
duplicate_pixel_frames: this.duplicatePixelFrames,
|
||||
frame_request_timeout_count: this.frameTimeoutCount,
|
||||
last_pixel_receive_age_ms: this.lastPixelReceivedAt ? Math.max(0, time - this.lastPixelReceivedAt) : 0,
|
||||
last_pixel_change_age_ms: this.lastPixelChangeAt ? Math.max(0, time - this.lastPixelChangeAt) : 0
|
||||
}});
|
||||
}
|
||||
@@ -385,6 +440,9 @@ function createPage(mode) {
|
||||
pages.set(mode.id, {mode, page, cards});
|
||||
return pages.get(mode.id);
|
||||
}
|
||||
function syncCardActivity() {
|
||||
for (const page of pages.values()) for (const card of page.cards) card.syncActivity();
|
||||
}
|
||||
function selectMode(id) {
|
||||
activeMode = id;
|
||||
let selected = pages.get(id);
|
||||
@@ -393,12 +451,16 @@ function selectMode(id) {
|
||||
[...elements.modeTabs.children].forEach(button => button.classList.toggle("active", button.dataset.mode === id));
|
||||
elements.modeDescription.textContent = selected.mode.description;
|
||||
applyCategory();
|
||||
syncCardActivity();
|
||||
closeMenu();
|
||||
}
|
||||
function applyCategory() {
|
||||
const page = pages.get(activeMode);
|
||||
if (!page) return;
|
||||
for (const card of page.cards) card.node.hidden = activeCategory !== "全部" && card.definition.category !== activeCategory;
|
||||
for (const card of page.cards) {
|
||||
card.node.hidden = !card.categoryVisible();
|
||||
card.syncActivity();
|
||||
}
|
||||
}
|
||||
function installNavigation() {
|
||||
elements.modeTabs.replaceChildren(...modes.map(mode => {
|
||||
@@ -542,13 +604,14 @@ function loop(time) {
|
||||
elements.streamToggle.addEventListener("click", () => {
|
||||
streamsPaused = !streamsPaused;
|
||||
elements.streamToggle.textContent = streamsPaused ? "恢复自动像素流" : "暂停自动像素流";
|
||||
if (!streamsPaused) for (const page of pages.values()) for (const card of page.cards) card.scheduleFramePump(true);
|
||||
syncCardActivity();
|
||||
});
|
||||
elements.menuClose.addEventListener("click", closeMenu);
|
||||
elements.menuTabs.forEach(button => button.addEventListener("click", () => { activeTab = button.dataset.tab; elements.menuTabs.forEach(item => item.classList.toggle("active", item === button)); renderMenuBody(); }));
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && !elements.menu.hidden) closeMenu(); });
|
||||
document.addEventListener("pointerdown", event => { if (!elements.menu.hidden && !elements.menu.contains(event.target) && !event.target.closest(".open-menu")) closeMenu(); });
|
||||
window.addEventListener("beforeunload", () => { for (const page of pages.values()) for (const card of page.cards) { card.disposed = true; clearTimeout(card.reconnectTimer); card.socket?.close(); } });
|
||||
document.addEventListener("visibilitychange", syncCardActivity);
|
||||
window.addEventListener("beforeunload", () => { for (const page of pages.values()) for (const card of page.cards) { card.disposed = true; card.stopFramePump(); clearTimeout(card.frameTimeout); clearTimeout(card.reconnectTimer); card.send("hide"); card.socket?.close(); } });
|
||||
|
||||
connectCatalog();
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
@@ -78,17 +78,19 @@
|
||||
<div class="canvas-loading"><span class="loader"></span><small>创建 Kernel Scene</small></div>
|
||||
</div>
|
||||
<dl class="performance-strip">
|
||||
<div><dt class="perf-fps">0.0</dt><dd>后台 FPS</dd></div>
|
||||
<div><dt class="perf-transport-fps">0.0</dt><dd>WS 像素 FPS</dd></div>
|
||||
<div><dt class="perf-present-fps">0.0</dt><dd>页面 FPS</dd></div>
|
||||
<div><dt class="perf-fps">0.0</dt><dd>后端渲染 FPS</dd></div>
|
||||
<div><dt class="perf-transport-fps">0.0</dt><dd>像素响应 FPS</dd></div>
|
||||
<div><dt class="perf-present-fps">0.0</dt><dd>浏览器呈现 FPS</dd></div>
|
||||
<div><dt class="perf-render">0.00</dt><dd>Core 渲染 ms</dd></div>
|
||||
<div><dt class="perf-encode">0.00</dt><dd>像素编码 ms</dd></div>
|
||||
<div><dt class="perf-bandwidth">0.0</dt><dd>像素 MB/s</dd></div>
|
||||
<div><dt class="perf-pending">0</dt><dd>队列</dd></div>
|
||||
<div><dt class="perf-dropped">0</dt><dd>丢弃</dd></div>
|
||||
<div><dt class="perf-bandwidth">0.0</dt><dd>响应负载 MB/s</dd></div>
|
||||
<div><dt class="perf-pending">0</dt><dd>Kernel 待处理</dd></div>
|
||||
<div><dt class="perf-dropped">0</dt><dd>Kernel 丢弃</dd></div>
|
||||
<div><dt class="perf-points">0→0</dt><dd>输入→绘制</dd></div>
|
||||
<div><dt class="perf-limit">N/A</dt><dd>当前瓶颈</dd></div>
|
||||
<div><dt class="perf-event">none</dt><dd>观察事件</dd></div>
|
||||
<div><dt class="perf-timeouts">0</dt><dd>像素超时</dd></div>
|
||||
<div><dt class="perf-pixel-age">—</dt><dd>最近像素龄</dd></div>
|
||||
</dl>
|
||||
<div class="limit-flags" aria-label="低延迟限速来源">
|
||||
<strong>限速来源</strong>
|
||||
|
||||
Reference in New Issue
Block a user