Skip to content

iOS

VideoEditorCore is the iOS SDK, a single XCFramework built on AVFoundation and Core Image.

  • iOS 16+, Swift 5.9+ (Xcode 15+).
  • Links with any Swift 5.9 or later toolchain. The privacy manifest and all resources are inside the framework.

Swift Package Manager:

.package(url: "https://github.com/infinishow/VideoEditorCore", from: "1.0.0")

CocoaPods:

pod 'VideoEditorCore', '~> 1.0'
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo access is needed to pick videos to edit.</string>
<key>CFBundleLocalizations</key>
<array><string>en</string><string>ko</string></array>

Without CFBundleLocalizations, the editor shows English on every device.

The editor asks for full photo-library access right before it opens the picker. If the user denies or limits access, it shows a Settings button; with no clips, it closes through onCancel. initialSource doesn’t need the permission.

A host that saves results to Photos declares NSPhotoLibraryAddUsageDescription itself.

import VideoEditorCore
let editor = VideoEditorViewController(
initialSource: pickedURL, // optional; omit to open the picker
config: VideoEditorConfig(licenseKey: "ve_live_…", giphyApiKey: "…")
)
editor.onEditingComplete = { [weak editor] result in
editor?.dismiss(animated: true)
upload(result.videoPath)
}
editor.onCancel = { [weak editor] in editor?.dismiss(animated: true) }
editor.onEditingFailed = { failure in log(failure) }
present(editor, animated: true)

Note: the host dismisses the editor, as with UIImagePickerController. Call dismiss in both onEditingComplete and onCancel.

initialSource is a file URL to a local video or photo. A photo becomes a one-second image clip.

Field Value
videoPath Rendered MP4 in the app cache. Move, upload or delete it.
coverPath Cover JPEG. Empty if no cover could be extracted (.coverUnavailable arrives first).
durationSec Duration in seconds.
audioPaths Audio tracks used, in timeline order, as pickAudio or the document picker returned them.
draftId Draft holding this edit; nil if drafts are off or nothing was saved.
usage Usage stats. See “Read usage data”.
VideoEditorConfig
licenseKey Required.
giphyApiKey Sticker tool is disabled without it.
textFonts Replaces the bundled fonts. nil keeps them. Empty array rejected.
minDurationSeconds, maxDurationSeconds Exportable length. Checked on “Next”; editing isn’t restricted.
allowAddClip false hides the add button and keeps the last clip.
locale BCP 47 tag. Only needed if your app has its own language setting.
draftsEnabled See “Resume drafts”. Default false.
coverPickerEnabled See “Let the user pick a cover”. Default false.
detachedRenderEnabled See “Show render progress in your own UI”. Default false.

Invalid values trap at construction, in release builds too: empty licenseKey, minimum above maximum, empty font list, initialSource with draftId, draftId without draftsEnabled.

Pass renderOutput: OutputSpec(…) to the initializer. nil uses OutputSpec.default: 9:16, 7 Mbps cap, 30 fps cap, H.264. codec is codecH264 or codecHEVC.

OutputSpec.AspectRatio Size
.ratio9x16 (default) 1080×1920
.ratio1x1 1080×1080
.ratio4x5 1080×1350
.ratio3x4 1080×1440
.ratio16x9 1920×1080

Sources are fitted with black bars. Sources below the frame-rate cap keep their rate. Audio: AAC stereo, 48 kHz, 256 kbps.

editor.pickAudio = { [weak editor] done in
guard let editor else { return done(nil) }
let library = MySoundLibraryViewController()
library.onPick = { path in library.dismiss(animated: true); done(path) }
library.onCancel = { library.dismiss(animated: true); done(nil) }
editor.present(library, animated: true)
}

Call done with a local file path, or nil (or an empty string) to cancel, from any thread. Without pickAudio, the Files document picker opens.

TextFontSpec(displayName: "Brand", fontName: "Brand-Bold")
  • fontName is the PostScript name; family names don’t resolve.
  • Register the font in your app (UIAppFonts or CTFontManagerRegisterFontsForURL). Bundled fonts register themselves.
  • Give the bold face. Overlay text uses one semi-bold weight.
  • The name identifies the font in drafts. Change it and existing text falls back to the first font.

With draftsEnabled: true, the editor saves when the user leaves: close, or the app going to the background. A render keeps the draft and returns its id in result.draftId.

let drafts = await VideoEditorDrafts.list() // newest first
let editor = VideoEditorViewController(
draftId: drafts[0].id,
config: VideoEditorConfig(licenseKey: "ve_live_…", draftsEnabled: true)
)
await VideoEditorDrafts.delete(id: drafts[0].id)
await VideoEditorDrafts.deleteAll()
  • A save arrives as onCancel. List again to see it.
  • Unchanged edits and empty timelines aren’t saved. Undo history isn’t saved.
  • VideoEditorDraft: id, createdAt, updatedAt, durationSec, clipCount, coverPath (first-frame JPEG; empty until extracted).
  • Videos are referenced, not copied. If a source is gone, photo access is revoked, or an app update moves an initialSource file, opening fails with .sourceUnreadable followed by onCancel, and the draft stays in the list.
  • Drafts are deleted only through delete and deleteAll, which work with draftsEnabled off too.

With coverPickerEnabled: true, “Next” opens a cover screen: the paused preview on top, a thumbnail strip of the whole video below. The user slides the frame to a time or picks a gallery photo.

  • A time gives that frame of the rendered video. A photo is center-cropped to the output aspect ratio. Either way: JPEG, 480 px on the short side.
  • With no selection, the cover is the first frame.
  • Back returns to editing. A cancelled or failed render returns to the cover screen.
  • Duration limits are checked before the cover screen. The selection isn’t saved in drafts.
  • Strings: videoeditor_cover_title, videoeditor_cover_choose_from_gallery.

Set detachedRenderEnabled: true to draw render progress in your own UI. The editor closes when the render starts, and the render continues as a VideoEditorRenderJob that reports progress, the result and failures.

editor.onRenderStarted = { [weak editor] job in
editor?.dismiss(animated: true)
job.onProgress = { value in uploadBanner.progress = Float(value) }
job.onComplete = { result in upload(result.videoPath, result.coverPath) }
job.onFailure = { failure in uploadBanner.showRetry(draftId: job.draftId) }
}
VideoEditorRenderJob
current The running job (static). nil once it ends. Use it from other screens.
draftId Draft holding the rendered state. nil when draftsEnabled is off.
progress, onProgress 0 to 1.
onComplete The result. An empty coverPath means no cover could be extracted.
onFailure renderFailed.
onCancel Ended by cancel().
cancel() Stops the render.
  • Results, failures and cancellation arrive only on the job: exactly one of onComplete, onFailure or onCancel.
  • One job runs at a time. Tapping “Next” in any editor meanwhile keeps the user on the editing screen and shows videoeditor_render_busy.
  • To render again, open the editor with draftId.

result.usage (VideoEditorUsage) counts what’s in the rendered video. Items removed during editing aren’t counted.

Field Value
videoClipCount, imageClipCount Clips on the timeline; each piece of a split counts
distinctSourceCount Distinct source paths
sourceDurationSec Total length of distinct video sources; images excluded
adjustedVolumeClipCount, mutedClipCount Video clips with changed volume (muted included); muted video clips
audioCount, adjustedAudioCount Audio tracks; tracks whose section within the song was moved
stickerCount Stickers
texts TextUsage: count, plus fonts, styles, colors (font displayName, style id, #AARRGGBB → count)
filters, effects, transitions Preset id → count
cover CoverUsage: source (.frame or .photo), moved; nil when the cover picker is off
editingSec Time on the editing screen; excludes license check, cover screen, render, background
renderSec Duration of the render that produced this result
renderAttemptCount Renders started in this session, this one included; failed and cancelled count
fullscreenOpenCount Fullscreen preview opens
undoCount, redoCount Undo and redo actions that changed something

onEditingFailed reports failures; the editor shows its own message. Closing or cancelling arrives as onCancel.

reason When Then
sourceUnreadable A source or draft couldn’t be opened With no clips left, onCancel follows
renderFailed Render failed; nativeCode is NSError.code Back to editing; “Next” can be tapped again
coverUnavailable Render succeeded, cover extraction failed Result follows with an empty coverPath
licenseInvalid No valid license On open: alert, then onCancel. On render: stays on editing

Strings are SDK resources in English and Korean. Other languages fall back to English.

The editor follows the app language, resolved by iOS from CFBundleLocalizations and the user’s preferences. If your app has its own language setting, pass it as VideoEditorConfig.locale.

To change a string or add a language, define the same key in your Localizable.strings. Your value wins.

ko.lproj/Localizable.strings
"videoeditor_next" = "계속";

Keys start with videoeditor_; the full list is Resources/en.lproj/Localizable.strings in the framework. The two strings with numbers are .stringsdict entries.