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 { opacity: 0.3; cursor: not-allowed; }
|
||||||
.toolbar-btn:disabled:hover { background: transparent; }
|
.toolbar-btn:disabled:hover { background: transparent; }
|
||||||
.toolbar-btn:disabled:active { transform: none; }
|
.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 {
|
.toolbar-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
/* 同 .floating-toolbar:8px 基础 + 安全区 inset 避开刘海/灵动岛 */
|
/* 同 .floating-toolbar:8px 基础 + 安全区 inset 避开刘海/灵动岛 */
|
||||||
@@ -1205,10 +1216,12 @@
|
|||||||
<div class="touch-indicator" id="touch-indicator"></div>
|
<div class="touch-indicator" id="touch-indicator"></div>
|
||||||
<button class="toolbar-toggle" id="toolbar-toggle" onclick="toggleFloatingToolbar()">•••</button>
|
<button class="toolbar-toggle" id="toolbar-toggle" onclick="toggleFloatingToolbar()">•••</button>
|
||||||
<div class="floating-toolbar" id="floating-toolbar">
|
<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" 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-mouse" onclick="toggleControl()" title="Mouse Control">🖱</button>
|
||||||
<button class="toolbar-btn" id="btn-keyboard" onclick="toggleKeyboard()" title="Keyboard" disabled>⌨</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="disconnect()" title="Disconnect">✕</button>
|
||||||
|
<button class="toolbar-btn" onclick="toggleFloatingToolbar()" title="Collapse">▴</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="zoom-indicator" id="zoom-indicator">100%</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">
|
<input type="text" id="mobile-keyboard" style="position:fixed;left:-9999px;opacity:0;" autocomplete="off" autocorrect="off" autocapitalize="off">
|
||||||
@@ -1291,6 +1304,12 @@
|
|||||||
<script>
|
<script>
|
||||||
let ws = null, token = null, decoder = null, devices = [], currentDevice = null;
|
let ws = null, token = null, decoder = null, devices = [], currentDevice = null;
|
||||||
let frameCount = 0, lastFrameTime = 0, fps = 0, pingInterval = 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 canvas = document.getElementById('screen-canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
@@ -1678,8 +1697,12 @@
|
|||||||
// Set up vertical flip transform once (BMP is bottom-up)
|
// Set up vertical flip transform once (BMP is bottom-up)
|
||||||
ctx.setTransform(1, 0, 0, -1, 0, height);
|
ctx.setTransform(1, 0, 0, -1, 0, height);
|
||||||
if (decoder) { try { decoder.close(); } catch(e) {} }
|
if (decoder) { try { decoder.close(); } catch(e) {} }
|
||||||
frameCount = 0;
|
// Reset FPS sliding window on decoder (re)init so a resolution
|
||||||
lastFrameTime = performance.now();
|
// 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({
|
decoder = new VideoDecoder({
|
||||||
output: (frame) => {
|
output: (frame) => {
|
||||||
// Check if frame dimensions match canvas
|
// Check if frame dimensions match canvas
|
||||||
@@ -1688,13 +1711,13 @@
|
|||||||
}
|
}
|
||||||
ctx.drawImage(frame, 0, 0);
|
ctx.drawImage(frame, 0, 0);
|
||||||
frame.close();
|
frame.close();
|
||||||
|
// 原始风格的 FPS 计数:1 秒采样窗口
|
||||||
frameCount++;
|
frameCount++;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
if (now - lastFrameTime >= 1000) {
|
if (now - lastFrameTime >= 1000) {
|
||||||
fps = Math.round(frameCount * 1000 / (now - lastFrameTime));
|
fps = Math.round(frameCount * 1000 / (now - lastFrameTime));
|
||||||
frameCount = 0;
|
frameCount = 0;
|
||||||
lastFrameTime = now;
|
lastFrameTime = now;
|
||||||
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
|
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
|
||||||
@@ -1994,6 +2017,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBinaryFrame(data) {
|
function handleBinaryFrame(data) {
|
||||||
|
// 全部进入的二进制都计入带宽统计:视频帧 + 音频帧 + 终端帧
|
||||||
|
bwBytesAccum += data.byteLength;
|
||||||
// 终端输出帧:4 字节 magic 'TRM1' (0x54 0x52 0x4D 0x31) → 转发到 xterm。
|
// 终端输出帧:4 字节 magic 'TRM1' (0x54 0x52 0x4D 0x31) → 转发到 xterm。
|
||||||
const u8 = new Uint8Array(data);
|
const u8 = new Uint8Array(data);
|
||||||
if (u8.length >= 4 &&
|
if (u8.length >= 4 &&
|
||||||
@@ -2475,6 +2500,12 @@
|
|||||||
currentDevice = dev;
|
currentDevice = dev;
|
||||||
document.getElementById('device-name').textContent = currentDevice.name;
|
document.getElementById('device-name').textContent = currentDevice.name;
|
||||||
document.getElementById('frame-info').textContent = '';
|
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');
|
updateScreenStatus('connecting');
|
||||||
// Default the audio button to "on" optimistically; server will
|
// Default the audio button to "on" optimistically; server will
|
||||||
// correct via connect_result.audio_enabled or audio_state event.
|
// 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() {
|
function isLandscape() {
|
||||||
return window.innerWidth > window.innerHeight;
|
return window.innerWidth > window.innerHeight;
|
||||||
}
|
}
|
||||||
@@ -3816,6 +3880,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function disconnect() {
|
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
|
// Reset control mode
|
||||||
controlEnabled = false;
|
controlEnabled = false;
|
||||||
// Reset keyboard state (blur event will update button state)
|
// Reset keyboard state (blur event will update button state)
|
||||||
|
|||||||
Reference in New Issue
Block a user