How to Handle Video Trim and Speed Adjustments in Swift: A Complete Guide
Palmier Pro stores video trim and speed data in a Clip model that converts timeline deltas into source frame adjustments using the formula Int((Double(deltaFrames) * speed).rounded()), enabling real-time preview and accurate export.
Handling video trim and speed adjustments in Swift requires a robust architecture that separates preview calculations from final model mutations. The palmier-pro repository demonstrates a production-ready approach using a speed-aware Clip model, pure-function editing engines, and SwiftUI drag gesture handling. This implementation ensures that when users trim clips or adjust playback speed, the underlying source frame calculations remain accurate for both real-time preview and final export.
Understanding the Clip Model Architecture
The foundation of Palmier Pro's trimming system lies in the Clip struct defined in Sources/PalmierPro/Models/Timeline.swift. This model holds both timeline positioning and trim-specific properties.
The Clip Struct and Speed-Aware Properties
Each Clip maintains temporal data through several key fields:
startFrame: The clip's position on the timelinedurationFrames: How long the clip occupies in the timelinetrimStartFrameandtrimEndFrame: The source media boundariesspeed: ADoublerepresenting playback velocity (1.0 = normal speed)
The model computes source frame consumption through a calculated property:
// In Sources/PalmierPro/Models/Timeline.swift
var sourceFramesConsumed: Int {
Int((Double(durationFrames) * speed).rounded())
}
This property tells the renderer exactly how many source frames are needed to fill the clip's duration at the given speed, ensuring that trim operations account for time-stretched media.
Real-Time Preview During Trim Operations
When users drag trim handles, Palmier Pro updates a preview clip immediately while deferring model mutations until the drag completes. This separation prevents unstable states during gesture recognition.
Handling Drag Gestures in TimelineView
The TimelineView in Sources/PalmierPro/Timeline/TimelineView.swift detects drag states through DragState.TrimDrag. During active drags, the view constructs a temporary preview clip that reflects the proposed trim:
// Inside TimelineView drag handling
guard let (drag, isLeft) = trimDrag,
clip.id == drag.clipId || trimPartnerIds.contains(clip.id) else { return }
var preview = clip
let sourceDelta = Int((Double(drag.deltaFrames) * clip.speed).rounded())
Calculating Source Frame Deltas
The critical calculation converts timeline frame deltas into source frame adjustments using the clip's speed factor:
// Left-hand trim (adjusting start)
if isLeft {
preview.startFrame = clip.startFrame + drag.deltaFrames
preview.trimStartFrame = clip.trimStartFrame + sourceDelta
preview.durationFrames = clip.durationFrames - drag.deltaFrames
} else {
// Right-hand trim (adjusting end)
preview.durationFrames = clip.durationFrames + drag.deltaFrames
preview.trimEndFrame = clip.trimEndFrame - sourceDelta
}
The ClipRenderer then draws using this preview data, allowing users to see exact source frames before committing changes.
Committing Trim Changes with OverwriteEngine
Once the user releases the drag gesture, EditorViewModel invokes the OverwriteEngine to compute final actions. This engine operates as a pure function that determines whether to trim, split, or remove clips based on the new region boundaries.
Pure Functions for Trim, Split, and Remove Actions
The OverwriteEngine.computeOverwrite method in Sources/PalmierPro/Editor/OverwriteEngine.swift analyzes clip overlaps and returns specific actions:
.trimStartor.trimEnd: For adjustments within clip boundaries.split: When the trim handle crosses into adjacent clips.remove: For clips completely covered by the operation region
Speed-Aware Split Logic
When splitting clips, the engine multiplies the region delta by the clip's speed to calculate the correct source frame boundary:
// In Sources/PalmierPro/Editor/OverwriteEngine.swift
if cs < regionStart && ce > regionEnd {
// Clip spans the region – split required
let rightTrimStart = clip.trimStartFrame +
Int((Double(regionEnd - cs) * clip.speed).rounded())
actions.append(.split(
clipId: clip.id,
leftDuration: regionStart - cs,
rightId: UUID().uuidString,
rightStartFrame: regionEnd,
rightTrimStart: rightTrimStart,
rightDuration: ce - regionEnd
))
}
The EditorViewModel+ClipMutations extension then applies these actions to the actual model objects, handling the creation of new clip IDs and boundary adjustments.
Exporting Speed and Trim Data
Palmier Pro persists speed adjustments during export through time-remap filters, ensuring compatibility with external editing software that cannot infer speed from duration alone.
XML Export with Time-Remap Filters
The XMLExporter in Sources/PalmierPro/Export/XMLExporter.swift converts the internal speed property into industry-standard time-remap values:
// Speed expressed as percentage for Premiere/FCXML compatibility
let timeRemapSpeed = clip.speed * 100 // e.g., 0.5 becomes 50%
This approach ensures that when projects import into professional editing software, the trim points and speed adjustments remain frame-accurate.
Summary
- Clip Model: Stores
trimStartFrame,trimEndFrame, andspeed, withsourceFramesConsumedcalculating required source frames usingInt((Double(durationFrames) * speed).rounded()). - Preview System:
TimelineViewcreates temporary preview clips during drag gestures, applying speed-adjusted deltas before rendering. - Commit Logic:
OverwriteEngineuses pure functions to determine trim, split, or remove actions, with speed-aware calculations for split boundaries. - Split Implementation:
EditorViewModel+ClipMutationsconverts timeline offsets to source frames using the speed multiplier when dividing clips. - Export Compatibility:
XMLExporterexpresses speed as time-remap percentages (speed × 100) for external editor compatibility.
Frequently Asked Questions
How does Palmier Pro handle speed adjustments when trimming video clips?
Palmier Pro multiplies all timeline deltas by the clip's speed property to calculate source frame adjustments. When trimming in TimelineView.swift, the code computes sourceDelta = Int((Double(drag.deltaFrames) * clip.speed).rounded()), ensuring that the trim boundaries align with the correct source media frames even when the clip plays at 2× or 0.5× speed.
What is the purpose of the OverwriteEngine in Palmier Pro?
The OverwriteEngine in Sources/PalmierPro/Editor/OverwriteEngine.swift functions as a pure-function decision engine that determines how to modify the timeline when clips overlap or undergo trim operations. It returns specific actions like .trimStart, .trimEnd, .split, or .remove based on region calculations, keeping the business logic testable and separate from the UI layer.
How are trim operations converted to source frame adjustments?
The conversion occurs through a consistent formula applied across multiple files: Int((Double(timelineDelta) * speed).rounded()). This calculation appears in TimelineView.swift for preview generation, OverwriteEngine.swift for split operations, and EditorViewModel+ClipMutations.swift for final clip mutations, ensuring that speed adjustments always factor into source frame boundaries.
How does Palmier Pro export speed data to external editors?
The XMLExporter translates the internal speed property (stored as a Double such as 0.5 or 2.0) into time-remap filter percentages by multiplying by 100. This produces values like 50% for half-speed or 200% for double-speed, which professional editing software like Adobe Premiere can interpret correctly when importing the exported XML files.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →