feat(export): full-timeline video export orchestrator + export_video command#112
Merged
Conversation
…command Wire the two ready-made building blocks — the wgpu compositor (opentake-render) and the ffmpeg VideoEncoder (opentake-media) — into a connected pure-video export spine. The new src-tauri/src/export.rs walks every frame of the current timeline, composites each on the GPU at the full export resolution (export_render_size, not the preview cap), and pipes the RGBA frames into a streaming H.264 encoder to produce a real .mp4 on disk. Scope (first cut): H.264 / .mp4 only; no audio mix; no progress/cancel. H.265 (.mp4) and ProRes 422 (.mov) are accepted by the request type but rejected with a clear error until their container/preset branches are validated (TODO). The manifest/text projection + MediaResolver + GpuContext logic is a self-contained copy of the preview path so render.rs's composite_frame body is untouched. The orchestration is split into a Tauri-free run_export(timeline, manifest, project_dir, req) so it can be driven directly by an ffmpeg+GPU gated integration test (tests/export_integration.rs) that builds a 6-frame timeline, runs the whole plan->composite->encode chain, and asserts the output mp4's width/height/codec/frame-count via ffprobe. Unit tests cover preset resolution (codec/extension validation, unwired-codec rejection) and serde defaults (#[serde(default)] -> H.264/1080p). lib.rs: register export::export_video; `pub mod export` for the test. Refs SPEC §2.4 / §8.2.
appergb
approved these changes
Jun 23, 2026
appergb
left a comment
Owner
There was a problem hiding this comment.
审核通过 ✅(自动审核流程 · 主控亲审)
- 新增 src-tauri/src/export.rs 整条时间线逐帧导出编排:snapshot→resolve_preset→project text/media→build_render_plan→逐帧 compositor.render_to_rgba→encoder.push_frame→finish。H.264/.mp4 限定,H.265/ProRes 明确拒绝并校验扩展名。run_export 与 Tauri 解耦便于测试。
- 资源/路径解析正确(External/Project 相对 project_dir)、source_frame.max(0)、tolerance=0 精确取帧、render_size 偶数化。
- 自包含复制 preview 逻辑、未碰 composite_frame → 与 #47/#48/#77/#79/#91 无碰撞(仅动 export.rs 新文件 + lib.rs 加 2 行注册 + 新集成测试)。CI 双绿。
- 测试齐全:单测 preset/quality/serde 默认 + ffmpeg/GPU 门控集成测试(真导出→ffprobe 校验宽高/编码/帧数,无 GPU/ffmpeg 自动跳过)。
- follow-up(非阻塞):run_export ~77 行 > 50 规约建议抽出帧循环;MediaResolver/projection 与 preview 路径重复,稳定后 hoist 为 pub(crate);out_path 缺路径边界校验(桌面 saveDialog 场景风险有限);H.265/ProRes/音频/进度取消按设计后续接。
This was referenced Jun 23, 2026
appergb
added a commit
that referenced
this pull request
Jun 23, 2026
feat(export): linear audio mixdown for full-timeline export (refs Phase5/#112)
This was referenced Jun 23, 2026
H-Chris233
pushed a commit
to H-Chris233/OpenTake
that referenced
this pull request
Jun 24, 2026
…se5/appergb#112) Extend the appergb#112 video export spine with a linear audio mixdown, replacing the dead `push_audio` side-channel with a wired decode → mix → mux path. - opentake-media: new pure `encode::mix` module — lay each clip's mono f32 PCM at its frame-derived sample offset, apply a per-sample gain envelope, sum overlapping clips, hard-limit to [-1,1]. `mono_f32_to_s16le` for the mux wire format. Fully unit-tested, no ffmpeg/domain deps. - opentake-media encoder: `finish` now mux's a supplied mixed buffer via a second ffmpeg pass (`-c:v copy` + AAC/LPCM `-shortest`), restoring the video-only file if the mux fails. `mux_args` is pure and unit-tested. - export.rs orchestrator: decode every audio/video clip's source window with the reused `extract_pcm` (mix rate, mono f32), build the `volume_at` gain envelope, skip muted tracks and audio-less sources, mix and push to the encoder. No-audio timelines still produce the same video-only output. - export_integration.rs: ffmpeg+GPU-gated test builds a sine-audio fixture, exports, and asserts ffprobe reads back an AAC audio stream + cleaned temps; video-only test now asserts no audio stream leaks in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A clean, verifiable minimal slice of the export subsystem: a per-frame export orchestrator that connects two already-ready building blocks — the wgpu compositor (
opentake-render) and the ffmpegVideoEncoder(opentake-media) — into a pure-video (no audio).mp4export.src-tauri/src/export.rswalks every frame of the current timeline:plan.frame(f)→Compositor::render_to_rgba→DecodedFrame→RgbaFrame→VideoEncoder::push_frame, thenfinish(). Export renders at the full export resolution (export_render_size, not the preview cap).Scope (first cut)
TODO). The encoder already builds correct args for both; this command just hasn't opted them in.Rc-textured compositor requires it; export is one frame at a time anyway).Design / safety
MediaResolver+ GPU-context logic is a self-contained copy of the preview path (render.rs), socomposite_frame's body is untouched — zero collision with the in-flight playback/preview work (appergb 时间线合成预览 + 播放(wgpu composite_frame 接线 + 暂停态 canvas + 播放引擎) #47/时间线片段编辑收尾:选中/Delete 验证 + 切割 + 片段右键菜单(Copy/Swap Media/Save as Media/AI Edit) #48) or the media PRs (feat(media): folder browsing in media panel (#58) #77 / feat(media): extract audio track to local file (#39) #79).run_export(timeline, manifest, project_dir, req)so it's drivable from an integration test withoutAppCore/ TauriState.Err(String);opentake-domainstays zero-dependency; request DTO uses#[serde(default)]so a bare{ "outPath": "..." }exports H.264/1080p.Files
src-tauri/src/export.rs— orchestrator +export_video#[tauri::command].src-tauri/tests/export_integration.rs— ffmpeg+GPU gated end-to-end test.src-tauri/src/lib.rs— 2 lines:pub mod export;+ registerexport::export_videoininvoke_handler.Verification (all green on real hardware)
cargo fmt --all— cleancargo clippy -p opentake-tauri --tests -- -D warnings— cleancargo test -p opentake-tauri --lib export— 6 unit tests pass (preset/codec/extension validation, unwired-codec rejection, serde defaults)cargo test -p opentake-tauri --test export_integration— 1 integration test passes: generates a 6-frame fixture, runs the full plan→composite→encode chain, andffprobeconfirms the output is a real1280x720h264mp4 with exactly 6 frames. Auto-skips when ffmpeg/ffprobe or a GPU adapter are unavailable.Refs SPEC §2.4 / §8.2.
🤖 Generated with Claude Code