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 timeline
  • durationFrames: How long the clip occupies in the timeline
  • trimStartFrame and trimEndFrame: The source media boundaries
  • speed: A Double representing 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:

  • .trimStart or .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, and speed, with sourceFramesConsumed calculating required source frames using Int((Double(durationFrames) * speed).rounded()).
  • Preview System: TimelineView creates temporary preview clips during drag gestures, applying speed-adjusted deltas before rendering.
  • Commit Logic: OverwriteEngine uses pure functions to determine trim, split, or remove actions, with speed-aware calculations for split boundaries.
  • Split Implementation: EditorViewModel+ClipMutations converts timeline offsets to source frames using the speed multiplier when dividing clips.
  • Export Compatibility: XMLExporter expresses 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →