Feature(web): bandwidth read-out + collapsible fullscreen toolbar
This commit is contained in:
@@ -829,6 +829,17 @@
|
||||
.toolbar-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
.toolbar-btn:disabled:hover { background: transparent; }
|
||||
.toolbar-btn:disabled:active { transform: none; }
|
||||
/* Throughput read-out inside the floating toolbar (fullscreen mode).
|
||||
Hidden automatically while empty so the toolbar collapses cleanly. */
|
||||
.fb-stats {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 12px;
|
||||
padding: 0 10px 0 6px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
.fb-stats:empty { display: none; }
|
||||
.toolbar-toggle {
|
||||
position: fixed;
|
||||
/* 同 .floating-toolbar:8px 基础 + 安全区 inset 避开刘海/灵动岛 */
|
||||
@@ -1205,10 +1216,12 @@
|
||||
<div class="touch-indicator" id="touch-indicator"></div>
|
||||
<button class="toolbar-toggle" id="toolbar-toggle" onclick="toggleFloatingToolbar()">•••</button>
|
||||
<div class="floating-toolbar" id="floating-toolbar">
|
||||
<span class="fb-stats" id="fb-stats"></span>
|
||||
<button class="toolbar-btn" onclick="sendRdpReset()" title="RDP Reset">↻</button>
|
||||
<button class="toolbar-btn" id="btn-mouse" onclick="toggleControl()" title="Mouse Control">🖱</button>
|
||||
<button class="toolbar-btn" id="btn-keyboard" onclick="toggleKeyboard()" title="Keyboard" disabled>⌨</button>
|
||||
<button class="toolbar-btn" onclick="disconnect()" title="Disconnect">✕</button>
|
||||
<button class="toolbar-btn" onclick="toggleFloatingToolbar()" title="Collapse">▴</button>
|
||||
</div>
|
||||
<div class="zoom-indicator" id="zoom-indicator">100%</div>
|
||||
<input type="text" id="mobile-keyboard" style="position:fixed;left:-9999px;opacity:0;" autocomplete="off" autocorrect="off" autocapitalize="off">
|
||||
@@ -1291,6 +1304,12 @@
|
||||
<script>
|
||||
let ws = null, token = null, decoder = null, devices = [], currentDevice = null;
|
||||
let frameCount = 0, lastFrameTime = 0, fps = 0, pingInterval = null;
|
||||
// FPS 计数:原始风格——decoder.onOutput 里 ++,每经过 1 秒采样一次。
|
||||
// 简单直接,与本次会话改动前一致。
|
||||
// 网络流量统计:handleBinaryFrame 累加,每 1 秒钟 renderStats 读出
|
||||
let bwBytesAccum = 0; // current-second byte accumulator
|
||||
let bwBytesPerSec = 0; // last second's throughput (bytes/sec)
|
||||
let currentWidth = 0, currentHeight = 0; // captured at frame decode time
|
||||
const canvas = document.getElementById('screen-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
@@ -1678,8 +1697,12 @@
|
||||
// Set up vertical flip transform once (BMP is bottom-up)
|
||||
ctx.setTransform(1, 0, 0, -1, 0, height);
|
||||
if (decoder) { try { decoder.close(); } catch(e) {} }
|
||||
frameCount = 0;
|
||||
lastFrameTime = performance.now();
|
||||
// Reset FPS sliding window on decoder (re)init so a resolution
|
||||
// change or codec switch doesn't carry over stale counts.
|
||||
frameCount = 0; lastFrameTime = performance.now(); fps = 0;
|
||||
// 记录当前分辨率供 renderStats 重组 frame-info 文案
|
||||
currentWidth = width;
|
||||
currentHeight = height;
|
||||
decoder = new VideoDecoder({
|
||||
output: (frame) => {
|
||||
// Check if frame dimensions match canvas
|
||||
@@ -1688,13 +1711,13 @@
|
||||
}
|
||||
ctx.drawImage(frame, 0, 0);
|
||||
frame.close();
|
||||
// 原始风格的 FPS 计数:1 秒采样窗口
|
||||
frameCount++;
|
||||
const now = performance.now();
|
||||
if (now - lastFrameTime >= 1000) {
|
||||
fps = Math.round(frameCount * 1000 / (now - lastFrameTime));
|
||||
frameCount = 0;
|
||||
lastFrameTime = now;
|
||||
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
|
||||
}
|
||||
},
|
||||
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
|
||||
@@ -1994,6 +2017,8 @@
|
||||
}
|
||||
|
||||
function handleBinaryFrame(data) {
|
||||
// 全部进入的二进制都计入带宽统计:视频帧 + 音频帧 + 终端帧
|
||||
bwBytesAccum += data.byteLength;
|
||||
// 终端输出帧:4 字节 magic 'TRM1' (0x54 0x52 0x4D 0x31) → 转发到 xterm。
|
||||
const u8 = new Uint8Array(data);
|
||||
if (u8.length >= 4 &&
|
||||
@@ -2475,6 +2500,12 @@
|
||||
currentDevice = dev;
|
||||
document.getElementById('device-name').textContent = currentDevice.name;
|
||||
document.getElementById('frame-info').textContent = '';
|
||||
// Reset throughput / resolution read-outs for the new session
|
||||
currentWidth = 0; currentHeight = 0;
|
||||
bwBytesAccum = 0; bwBytesPerSec = 0;
|
||||
frameCount = 0; lastFrameTime = performance.now(); fps = 0;
|
||||
const fbs = document.getElementById('fb-stats');
|
||||
if (fbs) fbs.textContent = '';
|
||||
updateScreenStatus('connecting');
|
||||
// Default the audio button to "on" optimistically; server will
|
||||
// correct via connect_result.audio_enabled or audio_state event.
|
||||
@@ -2826,6 +2857,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Pretty-print bytes/sec with a compact 1-byte / 1K / 1M scale.
|
||||
function formatBandwidth(bytesPerSec) {
|
||||
if (bytesPerSec < 1024) return bytesPerSec + 'B/s';
|
||||
if (bytesPerSec < 1024 * 1024) return Math.round(bytesPerSec / 1024) + 'K/s';
|
||||
return (bytesPerSec / 1024 / 1024).toFixed(1) + 'M/s';
|
||||
}
|
||||
|
||||
// 1-second ticker that pulls the FPS/bandwidth counters and refreshes
|
||||
// the two read-outs: #frame-info (always visible above the canvas) and
|
||||
// #fb-stats (a chip inside the floating toolbar, only visible while
|
||||
// the toolbar is expanded — that's how the fullscreen mode shows
|
||||
// throughput without cluttering the screen).
|
||||
function renderStats() {
|
||||
// FPS 已经在 decoder.onOutput 里就地更新,renderStats 只负责
|
||||
// 把最新的 fps、bandwidth 组装成显示串写到 DOM。
|
||||
|
||||
bwBytesPerSec = bwBytesAccum;
|
||||
bwBytesAccum = 0;
|
||||
|
||||
const fi = document.getElementById('frame-info');
|
||||
const fs = document.getElementById('fb-stats');
|
||||
if (currentWidth > 0 && currentHeight > 0) {
|
||||
const bw = formatBandwidth(bwBytesPerSec);
|
||||
if (fi) fi.textContent = currentWidth + 'x' + currentHeight +
|
||||
' @ ' + fps + ' fps · ' + bw;
|
||||
if (fs) fs.textContent = bw;
|
||||
} else {
|
||||
if (fi) fi.textContent = '';
|
||||
if (fs) fs.textContent = '';
|
||||
}
|
||||
}
|
||||
setInterval(renderStats, 1000);
|
||||
|
||||
function isLandscape() {
|
||||
return window.innerWidth > window.innerHeight;
|
||||
}
|
||||
@@ -3816,6 +3880,10 @@
|
||||
});
|
||||
|
||||
function disconnect() {
|
||||
// Reset throughput / resolution read-outs
|
||||
currentWidth = 0; currentHeight = 0;
|
||||
bwBytesAccum = 0; bwBytesPerSec = 0;
|
||||
frameCount = 0; lastFrameTime = performance.now(); fps = 0;
|
||||
// Reset control mode
|
||||
controlEnabled = false;
|
||||
// Reset keyboard state (blur event will update button state)
|
||||
|
||||
Reference in New Issue
Block a user