Skip to content

Commit fa8f273

Browse files
committed
merge(v0.4.1): nf-recorder 质量审查 + 4 fix · 零回归
session: 0912b6e2-1f27-49c4-8c92-a0d6005cb314 6 ally gpt 并发审查 → 5 P0 + 12 P1 → 修 4 P0(Fix-1 stdout / Fix-2 seek tests / Fix-3 orchestrator tests / Fix-4 v1.x 注释) · 留 1 P0(bounded pool)到 v0.5+ ADR. - 18 文件 · 305+ / 112- - cargo test 10 passed 0 failed(新 seek 6 失败路径 + orchestrator 4 range tests) - 4K HEVC 回归 · 3200211 bytes 跟 v0.4 字节级一致 · 零回归 Co-Authored-By: Claude Opus 4.7
2 parents 40097d2 + 39d4bbe commit fa8f273

18 files changed

Lines changed: 305 additions & 112 deletions

crates/nf-recorder/src/cli.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
//! CLI parser for `nf-recorder` · v1.14 T-09 / T-18 subcommand refactor.
1+
//! CLI parser for `nf-recorder` · clap subcommands for record / snapshot / verify.
22
//!
3-
//! Contract source: `spec/versions/v1.14/spec/interfaces-delta.json`
3+
//! Historical: v1.14 T-09 / T-18 subcommand refactor.
4+
//! Historical: contract source `spec/versions/v1.14/spec/interfaces-delta.json`
45
//! → `additions.modules[nf-recorder].subprocess_protocol`.
56
//!
67
//! ## Invocation shapes (backward-compatible)
@@ -26,7 +27,7 @@ use std::path::PathBuf;
2627
use crate::pipeline::VideoCodec;
2728
use crate::record_loop::RecordConfig;
2829

29-
/// v1.14 `nf-recorder` command-line interface.
30+
/// `nf-recorder` command-line interface.
3031
///
3132
/// The top-level struct doubles as the legacy record invocation (no
3233
/// subcommand); `command` is an optional subcommand that takes precedence
@@ -35,7 +36,7 @@ use crate::record_loop::RecordConfig;
3536
#[command(
3637
name = "nf-recorder",
3738
version,
38-
about = "Record bundle.html to MP4 · snapshot single frame to PNG (v1.14)"
39+
about = "Record bundle.html to MP4 · snapshot single frame to PNG"
3940
)]
4041
pub struct Cli {
4142
/// Optional subcommand (snapshot / future verify). Absent = legacy record.
@@ -66,14 +67,16 @@ pub struct Cli {
6667
#[arg(long = "max-duration", default_value_t = 60)]
6768
pub max_duration: u32,
6869

69-
/// v1.15 / v1.56 · 时间切片并行录制 · 父进程启 N 个子进程各录 1/N 段 ·
70+
/// 时间切片并行录制 · 父进程启 N 个子进程各录 1/N 段 ·
7071
/// ffmpeg concat 合并。未指定时按分辨率取默认值:1080p=1 · 4k=4。
7172
/// 显式 `--parallel 1` 可强制串行基线;N≥2 但视频 duration<6s 自动退化单进程。
73+
/// Historical: v1.15 / v1.56 parallel export.
7274
#[arg(long = "parallel")]
7375
pub parallel: Option<usize>,
7476

75-
/// v1.15 · 子进程内部参数 · 父 orchestrator 调子进程时传 · 用户不用设(hidden)。
77+
/// 子进程内部参数 · 父 orchestrator 调子进程时传 · 用户不用设(hidden)。
7678
/// 格式 `<start_frame>,<end_frame>` · 半开区间 [start, end) · 0-indexed。
79+
/// Historical: v1.15 frame-range worker protocol.
7780
#[arg(long = "frame-range", hide = true)]
7881
pub frame_range: Option<String>,
7982
}

crates/nf-recorder/src/events.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1-
//! Stdout JSON-Line event emitter · v1.14 T-09 minimal implementation.
1+
//! Stdout JSON-Line event emitter for recorder progress and verification.
22
//!
3-
//! Contract source: `spec/versions/v1.14/spec/interfaces-delta.json`
3+
//! Historical: v1.14 T-09 minimal implementation.
4+
//! Historical: contract source `spec/versions/v1.14/spec/interfaces-delta.json`
45
//! → `additions.modules[nf-recorder].subprocess_protocol.stdout_events`.
56
//!
67
//! Emits one JSON object per line to stdout, flushed on every call so
78
//! downstream (nf-cli · verify script · test harness) can read events
89
//! incrementally without waiting for buffer flushes.
910
//!
1011
//! **Why sync stdout (not tokio::io)**: recorder pipeline is single-threaded
11-
//! (worker_count = 1 in v1.14) · sync stdout is simpler · avoids tokio
12+
//! (single recorder worker) · sync stdout is simpler · avoids tokio
1213
//! runtime contention with the NSRunLoop pump inside `call_async`.
14+
//! Historical: v1.14 used `worker_count = 1`.
1315
1416
use serde::Serialize;
1517
use std::io::Write;
1618
use std::path::PathBuf;
1719

18-
/// v1.14 recorder stdout events · tagged by `event` field.
20+
/// Recorder stdout events · tagged by `event` field.
21+
/// Historical: v1.14 recorder stdout event schema.
1922
#[derive(Debug, Serialize)]
2023
#[serde(tag = "event")]
2124
pub enum Event {
@@ -30,18 +33,20 @@ pub enum Event {
3033
},
3134
/// Emitted per frame after successful encode.
3235
///
33-
/// `t_ms` is the legacy integer-ms value (向后兼容 · v1.14.0/1 字段 · 下游脚本仍用).
36+
/// `t_ms` is the legacy integer-ms value (向后兼容 · 下游脚本仍用).
3437
/// `t_exact_ms` is the precise f64 time the recorder sent to `window.__nf.seek`
35-
/// (v1.14.2 FM-T-QUANTIZATION 新增 · 精确 `seq * 1000 / fps` · VP-3 用它做
38+
/// (精确 `seq * 1000 / fps` · VP-3 用它做
3639
/// "帧间 t 序列严格等距" 断言 · spread < 1e-6).
40+
/// Historical: v1.14.0/1 integer-ms field; v1.14.2 added FM-T-QUANTIZATION.
3741
#[serde(rename = "record.frame")]
3842
RecordFrame {
3943
t_ms: u64,
4044
t_exact_ms: f64,
4145
seq: u64,
4246
encode_ms: f64,
4347
},
44-
/// Emitted every N frames (recorder chooses cadence, v1.14 = 30).
48+
/// Emitted every N frames (recorder chooses cadence).
49+
/// Historical: v1.14 cadence = 30.
4550
#[serde(rename = "record.encode_progress")]
4651
RecordEncodeProgress {
4752
frames_encoded: u64,
@@ -84,7 +89,8 @@ pub enum Event {
8489
duration_ms: u64,
8590
asserts: Vec<serde_json::Value>,
8691
},
87-
/// v1.15 · 并行录制开始 · orchestrator 父进程 probe duration 后 emit。
92+
/// 并行录制开始 · orchestrator 父进程 probe duration 后 emit。
93+
/// Historical: v1.15 parallel recording event.
8894
#[serde(rename = "record.parallel.start")]
8995
RecordParallelStart {
9096
parallel: usize,

crates/nf-recorder/src/export_api.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
//! v1.44 · High-level export API · 从 source.json 直接产 MP4。
1+
//! High-level export API · 从 source.json 直接产 MP4。
2+
//!
3+
//! Historical: v1.44 high-level export API.
24
//!
35
//! 架构(ADR-064):
46
//! - 输入:source.json 路径 + 输出 MP4 路径 + 持续时长(秒)
@@ -18,11 +20,13 @@ use crate::pipeline::VideoCodec;
1820
use crate::record_loop::{self, RecordConfig, RecordError};
1921
use crate::OutputStats;
2022

21-
/// v1.44 · nf-runtime 浏览器端 IIFE 产物 · 编译时 inline · 跟 nf-shell preview 同源。
23+
/// nf-runtime 浏览器端 IIFE 产物 · 编译时 inline · 跟 nf-shell preview 同源。
24+
/// Historical: v1.44 runtime IIFE export source.
2225
const RUNTIME_IIFE: &str = include_str!("../../nf-runtime/dist/runtime-iife.js");
2326

24-
/// v1.44 · 7 个官方 track 的 JS 源 · 编译时 inline · 喂给 runtime 解析
27+
/// 7 个官方 track 的 JS 源 · 编译时 inline · 喂给 runtime 解析
2528
/// `__NF_SOURCE__.tracks[].kind` 定位到对应代码。跟 nf-shell preview 同源。
29+
/// Historical: v1.44 official track source embedding.
2630
const TRACK_BG: &str = include_str!("../../nf-tracks/official/bg.js");
2731
const TRACK_SCENE: &str = include_str!("../../nf-tracks/official/scene.js");
2832
const TRACK_VIDEO: &str = include_str!("../../nf-tracks/official/video.js");
@@ -76,11 +80,13 @@ pub struct ExportOpts {
7680
pub fps: u32,
7781
/// VideoToolbox 目标比特率(bps)· 默认 20Mbps。
7882
pub bitrate_bps: u32,
79-
/// v1.44.1 / v1.56 · 并行切片 N(ADR-061) · `None` = 按分辨率取默认
83+
/// 并行切片 N(ADR-061) · `None` = 按分辨率取默认
8084
/// (1080p=1 / 4k=4) · `Some(1)` 可显式强制串行。
8185
/// duration < 6s 时 orchestrator 自动降级单进程(segment boot 开销吃掉收益)。
86+
/// Historical: v1.44.1 / v1.56 parallel slicing.
8287
pub parallel: Option<usize>,
83-
/// v1.55 · CLI `--resolution` 覆盖 `source.json meta.export.resolution`。
88+
/// CLI `--resolution` 覆盖 `source.json meta.export.resolution`。
89+
/// Historical: v1.55 resolution override.
8490
pub resolution_override: Option<ExportResolution>,
8591
}
8692

@@ -149,7 +155,9 @@ struct ResolvedExportPreset {
149155
codec: VideoCodec,
150156
}
151157

152-
/// v1.44 · 高层 lib API · 输入 source.json 路径 · 输出 MP4。
158+
/// 高层 lib API · 输入 source.json 路径 · 输出 MP4。
159+
///
160+
/// Historical: v1.44 high-level lib export API.
153161
///
154162
/// 主调方(nf-shell):
155163
/// ```ignore
@@ -237,7 +245,7 @@ pub async fn run_export_from_source(
237245
preset.viewport.1,
238246
)?;
239247

240-
// v1.44.1 / v1.56 · parallel >= 2 走 orchestrator (spawn N 子进程 +
248+
// Historical: v1.44.1 / v1.56 parallel >= 2 走 orchestrator (spawn N 子进程 +
241249
// ffmpeg concat) · 4k 未显式指定时默认 parallel=4。
242250
// 短视频(<6s) orchestrator 内部自动降级单进程 · duration 够长走真并行。
243251
// 单进程路径用 record_loop::run 拿 OutputStats · 并行路径 orchestrator 返 ()
@@ -382,7 +390,7 @@ window.__NF_TRACKS__ = {tracks_map_json};
382390
var REQUESTED_DURATION_MS = {requested_duration_ms};
383391
window.__nf = window.__nf || {{}};
384392
window.__nf_handle = handle;
385-
// recorder 合约 (v1.14 FrameReadyContract):
393+
// Historical: recorder 合约 (v1.14 FrameReadyContract):
386394
// - seek(t_ms) 返 {{t, frameReady:true, seq}} · t 在 0.01ms 容差内等于 t_ms · seq 单调递增
387395
// - 外部 t 纯驱动 (ADR-045) · 不依赖 RAF
388396
var _seq = 0;

crates/nf-recorder/src/frame_pool.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
//! `FramePool` · v1.14 T-09 placeholder.
1+
//! `FramePool` · Capacity + submission counter · NOT an SPSC pool.
22
//!
3-
//! v1.14 runs with `worker_count = 1` (scope.json) · the recorder thread is
3+
//! True bounded in-flight control 留 v0.5 ADR.
4+
//! Historical: v1.14 T-09 placeholder.
5+
//!
6+
//! Historical: v1.14 ran with `worker_count = 1` (scope.json) · the recorder thread is
47
//! the sole driver of the VT encoder which already owns a tiny internal
58
//! reorder queue inside VideoToolbox. A classic multi-producer SPSC pool
69
//! (crossbeam ArrayQueue) is over-engineered for this case: we never hold
710
//! more than one outstanding `IOSurface` at a time because `push_frame`
811
//! drains the compressor's output queue synchronously each iteration.
912
//!
1013
//! This struct is kept as a named entry point so future versions can swap
11-
//! in a real pool (v1.19 multi-worker) without touching `record_loop.rs`.
14+
//! in a real pool without touching `record_loop.rs`.
1215
//! Today it only records statistics.
16+
//! Historical: v1.19 multi-worker pool was the planned expansion point.
1317
1418
/// Simplified pool · counts submissions for telemetry.
1519
#[derive(Debug, Default)]
@@ -19,7 +23,8 @@ pub struct FramePool {
1923
}
2024

2125
impl FramePool {
22-
/// Construct an empty pool with a nominal `capacity` (v1.14 ignores it).
26+
/// Construct an empty pool with a nominal `capacity`.
27+
/// Historical: v1.14 ignored capacity.
2328
#[must_use]
2429
pub fn new(capacity: usize) -> Self {
2530
Self {
@@ -40,7 +45,8 @@ impl FramePool {
4045
self.submitted
4146
}
4247

43-
/// Nominal capacity (unused in v1.14 · reserved for v1.19).
48+
/// Nominal capacity.
49+
/// Historical: unused in v1.14 · reserved for v1.19.
4450
#[must_use]
4551
pub fn capacity(&self) -> usize {
4652
self.capacity

crates/nf-recorder/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ pub mod cli; // T-10 填
44
pub mod events; // T-10 填
55
pub mod export_api;
66
pub mod frame_pool; // T-09 填
7-
pub mod orchestrator; // v1.15 · 并行录制父进程 · spawn N 子 + ffmpeg concat
7+
pub mod orchestrator; // Historical: v1.15 · 并行录制父进程 · spawn N 子 + ffmpeg concat
88
pub mod pipeline;
99
pub mod record_loop; // T-09 填
1010
pub mod snapshot; // T-18 · product-internal single-frame PNG
11-
pub mod verify_mp4; // T-17 · product-internal MP4 atom verifier // v1.44 · high-level lib API · 从 source.json 直接导出 MP4
11+
pub mod verify_mp4; // T-17 · product-internal MP4 atom verifier // Historical: v1.44 · high-level lib API · 从 source.json 直接导出 MP4
1212

1313
pub use export_api::{run_export_from_source, ExportOpts, ExportResolution};
1414
pub use pipeline::{ColorSpec, OutputStats, PipelineError, RecordOpts, RecordPipeline, VideoCodec};

crates/nf-recorder/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
//! `nf-recorder` binary entry point · v1.14 T-09 / T-17 / T-18.
1+
//! `nf-recorder` binary entry point · record / snapshot / verify dispatch.
2+
//!
3+
//! Historical: v1.14 T-09 / T-17 / T-18 entry point.
24
//!
35
//! Single-threaded tokio runtime so the inner `call_async` future can pump
46
//! the macOS main run loop on the calling thread (WKWebView / CARenderer

0 commit comments

Comments
 (0)