Search Docs
Loading...
Skip to content

Programmatic Editing

Edit video scenes with CE.SDK Engine APIs when your app needs automation, custom controls, or template-driven video output.

8 mins
estimated time
GitHub

Programmatic editing works directly on the Engine scene graph rather than the built-in timeline UI for iOS. The Timeline Editor guide covers timeline-focused UI and Engine concepts. Use the programmatic approach when your app needs to create or modify video timelines from code, for example when generating variants, applying a known edit recipe, or exporting a scene without opening the editor.

This guide builds a compact two-clip montage, trims the first clip, splits the second clip, adds a timed graphic overlay, and exports the edited page as MP4.

Understand the Video Timeline#

A video scene contains one or more pages. A page owns tracks and timed blocks, and each track arranges its child clips in sequence. In a typical video timeline:

  • The scene is created in video mode with engine.scene.createVideo().
  • A page defines the canvas size and the exported playback duration.
  • A track contains graphic clip blocks with video fills.
  • Audio blocks or overlay blocks can sit on the page timeline beside the track.
  • Audio-only media uses DesignBlockType.audio; video fills and audio blocks can both be muted or mixed with volume controls.

Timeline values are measured in seconds. setDuration(_:duration:) controls how long a block is active, setTimeOffset(_:offset:) controls when a block starts within its parent timeline, and trim values control which source-media segment plays.

Create a Video Scene#

Create a video scene, add a page, set the page size, and set the page duration. The page is the block exported later.

let scene = try engine.scene.createVideo()
let page = try engine.block.create(.page)
try engine.block.appendChild(to: scene, child: page)
try engine.block.setWidth(page, value: 1280)
try engine.block.setHeight(page, value: 720)
try engine.block.setDuration(page, duration: 4.0)

createVideo() enables timeline behavior. The page duration here is 4.0 seconds, so the export renders the first four seconds of the edited page.

Add and Arrange Clips#

Create a track, attach it to the page, and append two graphic blocks with video fills. The track uses the child order to play clips sequentially.

let track = try engine.block.create(.track)
try engine.block.appendChild(to: page, child: track)
try engine.block.fillParent(track)
let firstClip = try engine.block.create(.graphic)
try engine.block.setShape(firstClip, shape: engine.block.createShape(.rect))
let firstVideoFill = try engine.block.createFill(.video)
try engine.block.setURL(
firstVideoFill,
property: "fill/video/fileURI",
value: baseURL.appendingPathComponent(
"ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4",
),
)
try engine.block.setFill(firstClip, fill: firstVideoFill)
try engine.block.setContentFillMode(firstClip, mode: .cover)
let secondClipURL = baseURL.appendingPathComponent(
"ly.img.video/videos/pexels-kampus-production-8154913.mp4",
)
let secondClip = try engine.block.create(.graphic)
try engine.block.setShape(secondClip, shape: engine.block.createShape(.rect))
let secondVideoFill = try engine.block.createFill(.video)
try engine.block.setURL(secondVideoFill, property: "fill/video/fileURI", value: secondClipURL)
try engine.block.setFill(secondClip, fill: secondVideoFill)
try engine.block.setContentFillMode(secondClip, mode: .cover)
try engine.block.appendChild(to: track, child: firstClip)
try engine.block.appendChild(to: track, child: secondClip)

The sample sets .cover so each clip fills the page frame. Pick the mode that matches how the source video should fit into the graphic block:

ModeEffect
ContentFillMode.cropUses manual crop positioning.
ContentFillMode.coverScales content to cover the full block frame and can crop the edges.
ContentFillMode.containScales content to fit inside the block frame and can leave empty space.

Change Timing and Trim#

Load AV metadata before changing trim values or reading duration metadata. forceLoadAVResource(_:) is async and must be awaited. Then set each clip’s timeline duration and trim the first fill to start one second into its source media.

try await engine.block.forceLoadAVResource(firstVideoFill)
try await engine.block.forceLoadAVResource(secondVideoFill)
try engine.block.setDuration(firstClip, duration: 2.0)
try engine.block.setDuration(secondClip, duration: 2.0)
try engine.block.setTrimOffset(firstVideoFill, offset: 1.0)
try engine.block.setTrimLength(firstVideoFill, length: 2.0)

Use these APIs for different timing layers:

APIEffect
engine.block.setDuration(_:duration:)Sets how long the block is active on the timeline.
engine.block.setTimeOffset(_:offset:)Sets when the block starts within its parent timeline.
engine.block.setTrimOffset(_:offset:)Sets where source media playback starts.
engine.block.setTrimLength(_:length:)Sets how much source media is used for playback.

Split a Clip#

Split the second clip at one second. The original block becomes the first segment, and split(_:atTime:options:) returns the new second segment.

let secondSegment = try engine.block.split(
secondClip,
atTime: 1.0,
options: SplitOptions(selectNewBlock: false),
)

The sample passes SplitOptions(selectNewBlock: false) because a headless workflow does not need to change editor selection after the split.

SplitOptions controls how CE.SDK attaches and selects the new segment:

FieldDefaultEffect
attachToParenttrueAttaches the returned segment to the same parent as the original block.
createParentTrackIfNeededfalseCreates a parent track when the split block needs one and attachToParent is enabled.
selectNewBlocktrueSelects the returned segment after splitting.

Apply a Timed Overlay#

Add a short graphic overlay directly to the page timeline. Its timeOffset and duration make it appear only for part of the exported video.

let overlay = try engine.block.create(.graphic)
try engine.block.setShape(overlay, shape: engine.block.createShape(.rect))
let overlayFill = try engine.block.createFill(.color)
try engine.block.setFill(overlay, fill: overlayFill)
try engine.block.setColor(
overlayFill,
property: "fill/color/value",
color: .rgba(r: 1.0, g: 0.82, b: 0.1, a: 0.85),
)
try engine.block.setWidth(overlay, value: 1280)
try engine.block.setHeight(overlay, value: 72)
try engine.block.setPositionY(overlay, value: 648)
try engine.block.setTimeOffset(overlay, offset: 1.25)
try engine.block.setDuration(overlay, duration: 1.5)
try engine.block.appendChild(to: page, child: overlay)

This same pattern works for other programmatic edits: create or find the target block, set its timing, then apply the specific block or fill properties your workflow needs.

Export the Edited Video#

Export the page as MP4 with exportVideo(_:mimeType:options:). The call returns an AsyncThrowingStream that emits .progress(...) cases while rendering and a single .finished(video:) case with the encoded bytes. VideoExportOptions controls output size, frame rate, and bitrate.

let mimeType: MIMEType = .mp4
let options = VideoExportOptions(
videoBitrate: 8_000_000,
audioBitrate: 128_000,
framerate: 30,
targetWidth: 1280,
targetHeight: 720,
)
let exportTask = Task {
for try await export in try await engine.block.exportVideo(page, mimeType: mimeType, options: options) {
switch export {
case let .progress(renderedFrames, _, totalFrames):
print("Rendered", renderedFrames, "of", totalFrames, "frames")
case let .finished(video: videoData):
return videoData
}
}
return Blob()
}
let editedVideo = try await exportTask.value
precondition(editedVideo.count > 0)

The sample asserts that the returned Blob is non-empty so the automated check verifies a real export result.

VideoExportOptions exposes these public fields:

FieldPurpose
h264ProfileSelects the H.264 encoder profile.
h264LevelSelects the H.264 encoder level — for example 52 for level 5.2.
videoBitrateSets video bitrate in bits per second, or 0 for automatic selection.
audioBitrateSets audio bitrate in bits per second, or 0 for automatic selection.
timeOffsetTime offset in seconds into the scene timeline where the export starts.
durationLength of the exported video in seconds; 0 uses the page duration.
framerateSets the target export frame rate in Hz.
targetWidthSets the target output width when used with targetHeight.
targetHeightSets the target output height when used with targetWidth.
allowTextOverhangIncludes glyph overhang bounds to avoid clipping text during export.

API Reference#

Swift APIPurpose
engine.scene.createVideo()Create a scene in video mode.
engine.block.create(_:)Create pages, tracks, graphics, audio-only timeline blocks, and other blocks.
engine.block.createFill(_:)Create video or color fills.
engine.block.createShape(_:)Create a shape for a graphic block.
engine.block.appendChild(to:child:)Add pages, tracks, clips, and overlays to the hierarchy.
engine.block.setString(_:property:value:)Set the source URI on a video fill via "fill/video/fileURI".
engine.block.setContentFillMode(_:mode:)Control how video content fits inside the graphic block.
engine.block.setDuration(_:duration:)Set page or block playback duration in seconds.
engine.block.setTimeOffset(_:offset:)Set when a block starts in its parent timeline.
engine.block.forceLoadAVResource(_:)Load audio or video metadata before trim and duration queries.
engine.block.getAVResourceTotalDuration(_:)Read the loaded media duration.
engine.block.setTrimOffset(_:offset:)Set the source media start offset.
engine.block.setTrimLength(_:length:)Set the source media playback length.
engine.block.split(_:atTime:options:)Split a timed block and return the second segment.
engine.block.setMuted(_:muted:)Mute audio on a video fill or audio block.
engine.block.setVolume(_:volume:)Set audio volume from 0 to 1 on a video fill or audio block.
engine.block.exportVideo(_:mimeType:options:)Export one edited page as a stream of progress events and final video bytes.

Next Steps#

  • Control Audio and Video — Learn to play, pause, seek, and preview audio and video content in CE.SDK using playback controls and solo mode.