- The clip menu's Speed/Duration… applied 50% at once, and Cmd+R / Clip ▸
Speed/Duration… ran it at 100% with no dialog. It now opens Clip Speed /
Duration (speed, the resulting duration, Reverse Speed, Ripple Edit, Time
Interpolation), seeded from the selected clip.
- effects.setParam took one undo step per frame of a drag, so Undo after
dragging Time Remapping's speed (or any Effect Controls value) crept back in
small steps. It takes `merge` / `begin` like the mixer and masks, and Effect
Controls sends them while the mouse button is down: a drag is one step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mxf: read_pcm bounds a request by the sample frames the file actually stores (plus 16M
samples of zero padding) instead of a fixed 16,777,216-sample cap, so whole-clip reads of
long stereo MXF (transcription) work again; PCM/AES3 chunk buffers are clamped to the source
length instead of a 256 MiB cap; decode_pcm's output is bounded by its input, so its
fixed cap goes (the AES3 >8-channel refusal stays; a request is also bounded by the source
length divided by the channel count). read_sample keeps only the source-length bound.
- mxf: index_entries no longer fails open() past 1,048,576 entries (the sparse BTreeMap
already bounds memory by real entries); long recordings open.
- project/format: new Sequence/Track/CaptionTrack::check_bounds() hold only the hostile-value
bounds (settings, marks, overflowing time ranges); project load and export ranges use it, so
projects that break only structural rules (overlap, zero duration) still open and export.
check() = check_bounds() + the structural rules, as before.
- project: frame-size cap raised to 268,435,456 pixels, 32768 per side (16K, 16384x8192).
- engine: checked_u32_p accepts integer-valued floats (1920.0); renderable_sequence returns
an EngineError (new Session::try_render_program_at, used by automation render) instead of
eprintln.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With clips selected under the playhead, Ripple Trim Previous/Next Edit to
Playhead (and the plain trims) now trim only their tracks and linked partners,
like Add Edit; with none selected they use the targeted tracks as before. All
tracks are targeted by default, so W on a V1 clip also cut the music on A2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A refused ripple delete / ripple trim now names the sync-locked track whose
clip is in the way and how to proceed, instead of a generic "would break sync"
(the edit is refused the same way Premiere refuses it).
- Dragging the playhead no longer snaps onto its own position (it stuck and moved
in SNAP_PX jumps), snaps on the initial press too, and "Snap playhead in
Timeline when Snap is enabled" is on by default, as in Premiere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge origin/main into the NVENC branch. #183 already provides the
HardwareEncoding enum, ExportSettings.hardware_encoding, the
hardwareEncoding export param and the Export > Video > Hardware Encoding
checkbox, so the duplicates from this branch are dropped and NVENC uses
them. filmcraft-export is now an all-platform dependency of
filmcraft-platform (VideoToolbox and NVENC both register encoders).
Never-crash fixes in the NVENC backend:
- submit() rejects a driver input pitch narrower than a row (and unlocks
the buffer); fill_nv12 uses get_mut instead of slicing rows.
- composition offsets use saturating math and i32::try_from; frame-rate,
size and timestamp casts are checked; sizes above u16::MAX decline.
- Annex B scanning and NAL type checks use get()/first().
- A failed nvEncOpenEncodeSessionEx that returns a handle destroys it.
ffi.rs carries NVIDIA's MIT notice for nvEncodeAPI.h, listed in
ATTRIBUTION.md under a new "Third-party source code" section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refresh() cleared the Missing badge, but the media pool kept the
MEDIA NOT FOUND slate cached under the item, so the monitors kept
showing it. The regression test now checks the rendered frame too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graphics.newFromFile resolved the Motion anchor with the sequence frame size
instead of the imported picture's size, so a picture smaller than the frame
got its anchor at the frame centre (e.g. 960,540 for a 4x2 image). Setting
Motion ▸ Position then moved the wrong point and the picture landed far from
where it was asked to go. place_video_clip now resolves auto points with the
source's own size (Project::source_size), falling back to the frame size.
The new-layer-from-file test checks the anchor is the picture's centre; it
fails before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause: the only Select All was `edit.selectAll`, which fills the
timeline clip selection (`state.selection`). Nothing filled the Project
panel's selection (`state.project_selection`), so with 161 imported
items the user had to click each one.
Fix:
- `project_panel::commands()` gets `project.selectAll` (selects every
item in the project) and `project.deselectAll` (clears the Project
panel selection). They are regular journalled commands with no menu
entry and no shortcut of their own, so they add no shortcut conflicts.
- `handle_shortcuts` (ui-egui) remaps a fired `edit.selectAll` /
`edit.deselectAll` to the `project.*` command when the Project panel
has focus. Cmd+A / Cmd+Shift+A keep acting on timeline clips
everywhere else, and `edit.selectAll` behaves exactly as before.
Out of scope: no menu entry. The Edit ▸ Select All menu item still
calls `edit.selectAll` directly, because menu clicks don't go through
the focus remap. The regression test checks the engine commands but not
the UI keyboard remap.
Regression test: project_panel_tests::select_all_selects_every_project_item
(fails without the fix: unknown command).
Co-Authored-By: ninso112 <ninso112@pm.me>
Co-Authored-By: Luna161 <Rosenkranzrene@gmail.com>
Adds `Format::Hevc` (Export > Format > H.265 (HEVC); `"format": "hevc"` in
`file.exportMedia`): HEVC Main, 8-bit 4:2:0, in MP4 (`hvc1`) or QuickTime, with AAC audio,
encoded by the macOS hardware encoder. There is no built-in H.265 encoder (one in pure Rust
is a project of its own, and x265 is GPL), so the format exists only where the OS has one.
- `videotoolbox_encode`: the session wrapper creates HEVC sessions too (`VtProfile::HevcMain`;
the profile also says which codec). Keyframes are the HEVC random access points (NAL types
16 to 21); the parameter sets are VPS / SPS / PPS; the `hvcC` record in the MP4 sample
entry is the one VideoToolbox writes (sample description extension atoms), so profile,
level and flags are the encoder's own. `VtEncoder::uses_hardware()` asks VideoToolbox
whether the session runs on a hardware encoder (a test checks H.264 and HEVC), so a
requirement dropped by accident could never become a silent software encode.
- `hardware_encode`: one adapter for both codecs. Choosing the format is the opt-in (the
`hardware_encoding` setting is for H.264). `hevc_available()` (one small hardware session,
created once) is registered with the new `filmcraft_export::register_format_probe`, so
`available(Format::Hevc)`, the format list of Export mode and `exportFormats` follow the
machine. `Format::has_builtin_encoder` says which formats need such a probe.
- `export`: `Format::is_h26x` where H.264 and H.265 behave alike (MP4 or QuickTime, AAC, size
estimate, summary line). Two-pass VBR is refused up front ("H.265 export has no two-pass
mode") and HDR sequences export as SDR, because there is no encoder to fall back to.
- Export mode: H.265 shows H.264's multiplexer, audio, metadata and bitrate controls, without
profile, level, two-pass or the hardware checkbox, and says "Encoder: Hardware".
Apple M1 (8 cores), 1080p25, a 9:29 timeline (camera clip, ProRes 4444 overlays, AAC,
loudness): hardware H.265 at 10 Mb/s 195 s, hardware H.264 at 20 Mb/s 204 s (CPU 1029 s and
1042 s; the same H.264 export took 193 s and 200 s on other runs). It is not faster: the codec
that compresses further is the more expensive one (raw encoders: 166 and 189 frames/s), and
the export is bound by the CPU compositor, not by the media engine.
Quality per bit, 30 s of the same timeline against a ProRes 422 HQ export (average PSNR over
the three planes, SSIM): 20 Mb/s 46.88 dB / 0.9902 (H.264) and 46.91 dB / 0.9903 (H.265);
10 Mb/s 44.96 / 0.9863 and 45.09 / 0.9863; 6 Mb/s 43.72 / 0.9833 and 44.31 / 0.9843; 4 Mb/s
42.91 / 0.9807 and 43.64 / 0.9825; 3 Mb/s 42.37 / 0.9786 and 43.05 / 0.9807. The same picture
at 10 Mb/s and above; at 3 to 6 Mb/s H.265 needs 22 to 31 % less bitrate for the same PSNR.
Both formats therefore keep the same default bitrate. Details in docs/performance.md (HW3).
Tests: tests/hardware_encode.rs gets the HEVC counterparts of the H.264 ones (what is taken
and declined, a round trip through our HEVC decoder with luma PSNR above 30 dB, keyframes, MP4
and QuickTime exports with AAC, ffmpeg / ffprobe reading `hevc` / `Main` / `hvc1` /
`yuv420p` / BT.709, exact sizes, hostile configurations, encoders dropped at any point, the
format list agreeing with the probe, sessions on the hardware encoder); settings_tests.rs
checks names, ids, defaults, the summary, two-pass refusal and the probe; export_ui.rs the
controls H.265 shows and hides.
Stacked on the hardware H.264 encoder (the first commit of this branch).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Export mode, the status bar and the Progress panel showed "Encoding 37%" and nothing about how
long the rest would take. They now say "37% · 2:05 left".
- `Progress::eta()` (the new `pace` module) is the time left at the job's speed over the last
15 seconds. Each call is also a reading, so the UI polling on every frame it draws the job
feeds it, and none of the places that advance `done` had to change. A trailing window follows
the real speed changes of an export (an intro, a banner, plain camera footage) where an
average since the start lags; a stall makes the estimate grow instead of freezing it; a long
idle start (the loudness pass) is not counted as the job's speed; nothing is shown until a
second of readings has progress, and nothing once the job is done or finished.
- `format_eta`: `45 s`, `2:05`, `1:02:05`, rounded up.
- Shown in the status bar ("Exporting... 37% · 2:05 left", and the `status.job.progress`
automation label), in the Export queue (on its own line: the column is narrow) and in the
Progress panel. The engine adds `etaSeconds` (null while unknown) to every job of `jobs.list`
and to the encoding items of `export.queue.list`; docs/agents.md says so.
Measured on a whole 9:29 export (193 s, Apple M1), asking every 3 s: the first estimate comes
after 6 s; from 30 s on, the predicted end of the export is on average 1.1 s early and never
more than 14.5 s off (7.5 % of the run). The status bar read "9% · 3:00 left" at 21 s and
"97% · 5 s left" at 184 s.
Tests: `pace` (steady job, speed change, stall, idle start, count going back, bounded history,
extreme values, a whole `Progress`), `format_eta`, the engine (`etaSeconds` of jobs and queue
items), and export_ui (status bar label and queue item).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Export encodes H.264 on the CPU with the built-in encoder, which is most of the time
of an export (about two thirds of the busy CPU in a profile of a 20 s export). On
macOS the media engine can do it: a VideoToolbox compression session behind
`filmcraft_export::VideoEncoder`, registered by `filmcraft_platform::register()`.
- `ExportSettings::hardware_encoding` (`HardwareEncoding::Off` | `Auto`; Export > Video >
Hardware Encoding, `"hardwareEncoding": "off|auto"` in `file.exportMedia`, a boolean is
understood too), off by default: the built-in encoder's output is byte-identical on
every machine, a hardware encoder's is not.
- `videotoolbox_encode` (FFI, same containment as the decoder: SAFETY comments, no
panic across the callback, safe API) and `hardware_encode` (safe adapter and factory).
- The factory declines, so the built-in encoder is used, for other formats, MXF, two-pass
VBR, HDR, odd sizes, non-square pixels and any configuration VideoToolbox cannot create a
hardware session for. An encoder that fails mid-export stops it with an error: unlike a
decoder, it cannot hand a half-written stream to another one.
- No B-frames: on real 1080p footage the quality is the same with and without them
(+-0.3 dB at equal bitrate) and the bitrate lands closer to the target, and without
them every frame is presented at its decode time (no composition offsets, no edit list).
- `export::register_encoder` ignores a factory registered twice; `encoder_registered`.
Apple M1, 8 cores, single runs: a 9:29 timeline (camera clip, ProRes 4444 overlays with
alpha, AAC, loudness) exports in 311 s instead of 793 s (2.5x faster); its densest 134 s
in 84 s instead of 395 s (4.7x). The outputs compare at SSIM 0.990 / PSNR 46.5 dB. The encoder
alone runs 200 fps at 1080p (docs/performance.md).
Tests (tests/hardware_encode.rs): what is taken and declined, a round trip through our
software decoder, an export that decodes in our decoder and in ffmpeg, the fallback for
everything hardware declines, exact output sizes, an empty range, hostile configurations
and encoders dropped at any point; the engine's `hardwareEncoding` parameter.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
NVENC through nvEncodeAPI64.dll (API 12.1, FFI layout checked against the C header by generated
tests), registered as an Export encoder in front of the software one; Export > Hardware encoding
(off by default). Declines two-pass VBR, HDR, MXF, unsupported sizes; B-frames, IDR/GOP, VUI, avcC.
Tests: decode with our decoder (PSNR), pipeline export vs software, decline paths.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp
file.import created new items but never rebuilt Session::offline.missing.
The UI reads that list to drive the Program slate and the Project
panel 'Media missing' badge, so any re-imported item the project
already listed as missing stayed listed as missing until on_open ran
again (project reopen) or the caller manually relinked.
Add a single crate::relink::refresh(s) call at the end of file.import,
matching the call on_open already makes after a project opens. Closes
the slate/badge symptom without requiring reopen or per-item relink.
The deeper SharedSource cache eviction issue the reporter observed
with media.autoRelink is out of scope here; a follow-up should call
MediaPool::remove for items whose path now resolves before re-insert.
Co-Authored-By: ninso112 <ninso112@pm.me>
Co-Authored-By: Luna161 <Rosenkranzrene@gmail.com>
Add Edit always cut every targeted track, so with one clip selected it cut
all tracks. As in Premiere, selected clips under the playhead are now cut
and nothing else; with none there, the targeted tracks are, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Looking at a folder probes each visible file for its properties. A format
no streaming reader takes (AVI, which isn't supported at all, and WAV,
MP3 and the other byte-opened formats) fell through open_reader's
whole-file path: a 290 MB AVI took 518 MB of memory and a full read just
to report that it isn't supported, a 247 MB WAV 500 MB.
- filmcraft-media: open_reader_within(.., max_whole) refuses, after the
sniffing head, a file no reader opener takes when it is larger than
max_whole. open_reader is open_reader_within with no limit.
- engine: MediaPool::probe_file uses it with PROBE_WHOLE_FILE_MAX (64 MiB);
media_browser::probe calls probe_file. Import and playback still use
open_file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- transcript.generate is enabled only with a recogniser: one installed in
Session::transcriber, or the build's own (feature `whisper`). Otherwise
describe, command_list enabled_only and the menu report it disabled with
"speech-to-text is not available in this build" as the reason (#97).
Transcribe Sequence, which runs it, follows.
- transcript.downloadModel is enabled only with `speech-download` (#98).
- file.import with Automatically transcribe clips on no longer skips
transcription silently when it can't run: the disabled command's reason
lands in the import's errors as "transcription: ..." (#89).
- docs/transcripts.md describes the disabled state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- transcript.generate is enabled only with an installed transcriber or the whisper feature; it
reported enabled and then always failed with "speech-to-text is not available in this build".
- transcript.downloadModel is enabled only with the speech-download feature.
- An import with Automatically transcribe clips (all imported) on reports a
"transcription: speech-to-text is not available in this build …" error instead of importing
silently untranscribed.
Fixes#97Fixes#98Fixes#89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- File ▸ New ▸ Color Matte… (and Project ▸ New Item) opens a New Color Matte
dialog: color picker, name, width, height and duration. Called with params
(CLI, control, MCP) it still creates the matte directly.
- project.matteColor {item?, color}: change a Color Matte's color (the
selected one by default); every clip of it follows and it undoes.
Double-clicking a Color Matte in the Project panel opens its color picker,
as in Premiere.
- media pool: a generator's cache key now holds its parameters, so a matte
whose color changes (or comes back with undo) is generated again instead of
the pool serving the first one forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both tools are annotated readOnlyHint, but a call with `seconds` moved the active sequence's
playhead (and, with Selection Follows Playhead on, the selection) to that time, in headless and
bridge mode alike.
- headless: Session::render_program_at renders the frame at `seconds` (frame-snapped, as the
playhead would be) without touching the session.
- bridge: the Program monitor screenshot needs the live playhead there, so the tool records the
playhead and selection, takes the screenshot and puts both back (the selection only if it changed,
since selecting also leaves trim mode).
Regression test: rendering at 5 s and 8 s with Selection Follows Playhead on leaves playhead and
selection unchanged and still renders the requested frame (it moved the playhead to 8 s before).
Fixes#114
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text layer is now one of two types, each with Premiere Pro 26's handles:
- Point text (Type tool click): every handle scales the layer about its
anchor point, both axes alike; the font size is untouched.
- Paragraph text (Type tool drag): the dragged rectangle is a box the text
wraps in. Handles move that side or corner of the box, the opposite side
stays, the text re-wraps at the same size. Lines below the box are not
shown and the bottom-right handle turns into a red plus.
- The anchor point can be dragged with the Selection tool; the layer stays.
It is picked before anything under it, so a shape dragged by its exact
centre moves its anchor.
- Properties > Text > wrench opens Text Properties, where Text Layer Type
switches a layer between the two (graphics.setTextType); the text stays
where it is.
Shapes keep stretching away from their opposite side.
New: text layer parameter box_height, graphics.newText `box`, and
textType / box / overflow / anchor / scale in graphics.list. A layer saved
before a parameter existed can now be given it with graphics.set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- codecs: a resampled MP4 audio read scales the edit-list offset to the source rate in i128 and
saturates the window arithmetic; a hostile media_time overflowed i64.
- engine: media_duration follows a subclip's parent chain in a loop bounded at 16 hops instead of
recursing; a cyclic or very deep chain in a damaged project overflowed the stack.
Each has a regression test that failed before (overflow panic / stack overflow).
Fixes#66
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New backend in crates/platform (media_foundation/): a Direct3D-aware decoder MFT is driven at the
level of single access units (Annex B), given the process's D3D11 video device through an
IMFDXGIDeviceManager so it decodes with DXVA, and its NV12 / P010 textures are read back through a
staging texture into planar Yuv8 / Yuv16. H.264 Baseline/Main/High and HEVC Main / Main 10, 4:2:0.
- Hardware is verified: non-D3D-aware or async MFTs, streams the GPU's DXVA decoder does not list,
and system-memory output (Microsoft's MFT software fallback) are declined or fail the stream so
HybridDecoder continues with our decoder.
- mfplat.dll is loaded at run time so Windows N editions still start.
- register() / registered() / hardware_decoder_for() cover Windows; perf.stats reports the backend.
- Tests (Windows): bit-exact parity with the software decoders at 640x360, 1080p and 2160p
(resets, seeks, flush), forced mid-stream failure, damaged input, concurrency, repeated init.
- Bench: runs on Windows (ffmpeg on PATH, process/thread CPU clocks), HEVC Main 10 2160p row,
hardware counters per row.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp