Skip to content

Add fast stream-copy export via Ctrl+Shift+S - #8

Open
JDay-IT wants to merge 1 commit into
omacom:masterfrom
JDay-IT:fast-copy
Open

JDay-IT wants to merge 1 commit into
omacom:masterfrom
JDay-IT:fast-copy

Conversation

@JDay-IT

@JDay-IT JDay-IT commented Sep 5, 2026

Copy link
Copy Markdown

When cutting video, i'm usually just looking to trim to the right spots of a video not re-encode. This option allows for instant cutting without waiting for encoding

--- AI Summary
Exports can now remux the video/audio streams without re-encoding via a Mode (Re-encode / Copy) choice in the save dialog, preselectable with Ctrl+Shift+S for a near-instant cut that snaps to the nearest keyframe.

Force original size for stream-copy exports

A copy cut never decodes the streams, so a scaleHeight passed alongside copy would silently be ignored. Zero it in exportClip to make the no-scaling contract self-enforcing, and cover it with a test that asks for a downscale in copy mode.

Exports can now remux the video/audio streams without re-encoding via a
Mode (Re-encode / Copy) choice in the save dialog, preselectable with
Ctrl+Shift+S for a near-instant cut that snaps to the nearest keyframe.

Force original size for stream-copy exports

A copy cut never decodes the streams, so a scaleHeight passed alongside
copy would silently be ignored. Zero it in exportClip to make the
no-scaling contract self-enforcing, and cover it with a test that asks
for a downscale in copy mode.
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed by Claude Opus 5 in Claude Code, with an independent second opinion from Codex at xhigh reasoning. The declared suite ran on a disposable Omarchy VM (QT_QPA_PLATFORM=offscreen ./bin/test): 29 passed, 0 failed, 0 skipped. Everything below was measured on that VM against ffmpeg 9.0.1 using the exact argument list trimArgs produces, rather than reasoned about from the diff.

The copy export keeps the footage it trimmed away, and it is recoverable

-ss before -i seeks to the keyframe at or before the trim point, and -c copy cannot discard the packets between there and where the user actually cut — so they go into the output and are hidden behind an MP4 edit list. The visible cut is better than the README claims: on a 20s source with keyframes every 5s, exporting 7.000 → 12.000 in copy mode gives a first displayed frame whose md5 matches the source frame at t=7.000 exactly, and a 0.1s beep placed at source t=8.0 lands at 1.000021 in the output against 1.000021 for the re-encode control. But the file physically holds 210 video packets (7.0s of content) with the first at pts -2.000, and one command gets the removed part back out:

ffmpeg -ignore_editlist 1 -i clip_trimmed.mp4 -t 2.008 -c copy recovered.mp4

That produced a 2.03s playable file whose first frame is the source at t=5.000 — the two seconds the user cut off. For a trimmer that is the finding that matters: someone who trims the head off a screen recording because of what is in it still ships it. The same material is what any reader that does not apply edit lists shows, and such a reader reports the clip as 7.012s rather than 5.000s.

The size cost is bounded by the source's keyframe interval, which omacut never looks at. A 60s source with a single keyframe, trimmed 45.000 → 48.000, produced a 12,087,123-byte file containing 1440 video packets (48s of content, first packet at pts -45.000), where re-encoding the same 3 seconds is 458,921 bytes. A more ordinary 1080p60 source with a 2s keyframe interval, trimmed 12.4 → 16.4, gave 15.3 MB copied against 6.1 MB re-encoded. "Near-instant, always original size" is accurate; it is the third number that is missing.

So README.md:33 and the comments at src/ffmpeg.cpp:140-145 and src/ffmpeg.h:34-36 describe the wrong failure. The cut does not snap to the nearest keyframe — the playback start is exact, and the file carries up to a full keyframe interval of hidden pre-roll. Which way to resolve that is a design call rather than something to patch in review: snapping start down to the real previous keyframe and saying so makes the file honest and small at the cost of moving the visible cut; warning after the export, or refusing copy when the start is off a keyframe, are the other two shapes. Whatever is chosen, the text should describe what the file contains.

The end is not exact either

Codex raised this and it reproduces. -t cannot split an encoded packet, and with B-frames the retained frames run past it. A 10fps source encoded with -bf 2, selection 25.000 → 30.000, produced a copy whose video stream is 5.300s (52 decoded frames, last at 5.200) and whose audio is 5.000181s, against 5.000/5.000 for the re-encode control. Two extra frames on the end, and the two streams no longer end together.

Copy mode is offered for inputs that cannot be remuxed into MP4

The open filter advertises *.webm and *.mkv (src/portalfilepicker.cpp:126-130) and the output is always MP4. Copying into MP4, measured:

  • VP8/Vorbis WebM fails outright: exit 234, Could not find tag for codec vp8 in stream #0, codec not currently supported in container. The failure path itself is sound — ffmpeg only ever writes <target>.omacut-part.mp4, and Backend::failExport (src/backend.cpp:414-419) removes it, so an existing target is untouched — but what reaches the user is raw ffmpeg text in a single elided status Label.
  • H.264/Vorbis muxes successfully with the Vorbis track written under an mp4a sample entry, and H.264/PCM under ipcm. Codex found these and I confirmed them. Both decode back out of ffmpeg, so the export looks like a success, but neither is a codec an MP4 recipient can be assumed to play. The re-encode path always produces H.264/AAC.

A codec check before offering Copy, or a fallback to re-encode when the muxer refuses, would turn both into something other than a surprise.

Checked and clean

-ss before -i is the right placement for both branches; after -i would be worse for copy specifically, since it discards packets without being able to produce a decodable first frame. No A/V desync and no head gap: the beep measurement above matches the re-encode control to the sample, and both streams carry their pre-roll together. The two export paths cannot collide — backend.busy at src/Main.qml:61, m_pendingAction at src/portalfilepicker.cpp:230 and m_busy at src/backend.cpp:319 each refuse a second export, there is one exportClip, one temp path derived from the target, and the rename happens only on exit code 0. The re-encode argument list is unchanged and its existing tests pass. The -progress arithmetic is fine: codex expected out_time_us to go negative during the pre-roll and stall the display at 0%, but I could not reproduce it — ffmpeg emitted a single positive progress block in both the 2s and the 45s pre-roll cases (out_time_us=3042086 for the latter) — so that one is not being reported against this branch. The default argument on the override at tests/backend_tests.cpp:37 is legal and matches the base declaration, and the portal choices restructuring is correct.

The new tests cannot catch any of the above

The fixture is testsrc=size=32x32:rate=1:duration=1 (tests/backend_tests.cpp:236) — one frame, no audio — and exportClipCopiesStreams copies from 0.0, the one start that has no pre-roll. It proves ffmpeg exits 0 and writes an MP4. A fixture with a known keyframe interval, trimmed off a keyframe, would make the pre-roll assertable.

Overlap with #4

PR #4 extends exactly the same signatures with a different extra parameter — trimArgs, Backend::exportDialog, Backend::exportClip, FilePicker::exportVideo, the exportSelected signal, PortalFilePicker::exportVideo and the QML caller — so whichever lands second needs a rebase across ffmpeg.cpp/h, backend.cpp/h, filepicker.h, portalfilepicker.cpp/h, Main.qml and backend_tests.cpp. They also conflict in substance: a crop is a -vf crop= filter and cannot be applied without decoding, so a copy export would silently drop a crop the user drew, the way it silently zeroes scaleHeight today. If both are wanted, Copy needs to be disabled or refused while a crop is set. Which goes first is the maintainer's call.

Nothing was pushed to this branch: each finding above needs a product decision rather than a patch. Waiting on you and the maintainer.

@JDay-IT

JDay-IT commented Sep 7, 2026

Copy link
Copy Markdown
Author

Speaking to the overlap with number four, I did try merging that code into a later test branch and got it rebased, a decision I made to keep both functionalities is, if cropping, to disallow the fast copy. If a user crops and hits the fast copy shortcut it just continues to the normal export and re-encodes like normal

@JDay-IT

JDay-IT commented Sep 7, 2026

Copy link
Copy Markdown
Author

This is the rebase of my fast-copy and #4 as well as with added multifile/drag video. i didn't want to iterate to much on it until something was merged into main though

https://github.com/JDay-IT/omacut/tree/multifile-cropping-fastcopy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants