- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、 温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。 - 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。 - 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增 延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。 - 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README; 新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。 - 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
81 lines
3.6 KiB
Python
81 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Summarize opt-in client marker reports using explicit, half-open row ranges."""
|
|
import argparse
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import statistics
|
|
|
|
|
|
def describe(values):
|
|
ordered = sorted(values)
|
|
if not ordered:
|
|
raise ValueError('No valid marker samples in the selected range')
|
|
return {
|
|
'count': len(ordered),
|
|
'mean': statistics.mean(ordered),
|
|
'p50': statistics.median(ordered),
|
|
'p95': ordered[int((len(ordered)-1)*.95)],
|
|
'p99': ordered[int((len(ordered)-1)*.99)],
|
|
'minimum': ordered[0],
|
|
'maximum': ordered[-1],
|
|
}
|
|
|
|
|
|
def summarize(rows, selection):
|
|
start, end = selection['start_row'], selection['end_row']
|
|
if not 0 <= start < end <= len(rows):
|
|
raise ValueError('Range must select complete rows from the report')
|
|
samples = rows[start:end]
|
|
seen = set()
|
|
for sample in samples:
|
|
if not all(isinstance(v, (int, float)) and math.isfinite(v) for v in sample.values()):
|
|
raise ValueError('Sample contains a nonfinite value')
|
|
if not 0 <= sample['lowerMs'] <= sample['upperMs'] <= 5000:
|
|
raise ValueError('Invalid refresh bracket')
|
|
key = (sample['counter'], sample['sourceDrawMs'])
|
|
if key in seen:
|
|
raise ValueError('A source marker was counted more than once')
|
|
seen.add(key)
|
|
return {
|
|
**selection,
|
|
'samples': len(samples),
|
|
'sample_span_seconds': (samples[-1]['drawMs']-samples[0]['drawMs'])/1000,
|
|
'marker_event_to_paint_ms': describe([(s['lowerMs']+s['upperMs'])/2 for s in samples]),
|
|
'lower_bound_ms': describe([s['lowerMs'] for s in samples]),
|
|
'upper_bound_ms': describe([s['upperMs'] for s in samples]),
|
|
'refresh_bracket_width_ms': describe([s['upperMs']-s['lowerMs'] for s in samples]),
|
|
'decoded_to_draw_ms': describe([s['decodeToDrawMs'] for s in samples]),
|
|
'marker_analysis_ms': describe([s['analysisMs'] for s in samples]),
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--report', type=Path, required=True)
|
|
parser.add_argument('--range', type=Path, action='append', required=True, dest='ranges')
|
|
parser.add_argument('--out', type=Path, required=True)
|
|
args = parser.parse_args()
|
|
rows = [json.loads(line) for line in args.report.read_text().splitlines() if line.strip()]
|
|
trials = [summarize(rows, json.loads(path.read_text())) for path in args.ranges]
|
|
result = {
|
|
'method': '50 ms source marker events; first decodable echo on production VideoSurface; midpoint of adjacent-rAF brackets',
|
|
'scope': 'Source-screen marker to camera exposure/capture, Wi-Fi/UDP, native reassembly, local WebSocket, JPEG decode and canvas draw',
|
|
'limitations': [
|
|
'rAF brackets estimate display scheduling; panel scanout and pixel response are not measured',
|
|
'Mixed-exposure or unrecognized markers are excluded; first decodable echo can follow an earlier ambiguous image',
|
|
'Not every displayed frame is a sample; accepted samples describe this calibration scene and network',
|
|
'decodeToDrawMs starts after JPEG decoding, not at UDP or WebSocket arrival',
|
|
],
|
|
'physical_panel_response_measured': False,
|
|
'trials': trials,
|
|
}
|
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
args.out.write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
|
for trial in trials:
|
|
print(json.dumps({k: trial[k] for k in ('fb_count', 'samples', 'marker_event_to_paint_ms')}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|