iOS
VideoEditorCore is the iOS SDK, a single XCFramework built on AVFoundation and Core Image.
Requirements
Section titled “Requirements”- 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.
Install
Section titled “Install”Swift Package Manager:
.package(url: "https://github.com/infinishow/VideoEditorCore", from: "1.0.0")CocoaPods:
pod 'VideoEditorCore', '~> 1.0'Info.plist
Section titled “Info.plist”<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.
Present the editor
Section titled “Present the editor”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.
Result: VideoEditorResult
Section titled “Result: VideoEditorResult”| 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”. |
Configure
Section titled “Configure”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.
Output
Section titled “Output”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.
Supply an audio picker
Section titled “Supply an audio picker”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.
Add fonts
Section titled “Add fonts”TextFontSpec(displayName: "Brand", fontName: "Brand-Bold")fontNameis the PostScript name; family names don’t resolve.- Register the font in your app (
UIAppFontsorCTFontManagerRegisterFontsForURL). 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.
Resume drafts
Section titled “Resume drafts”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
initialSourcefile, opening fails with.sourceUnreadablefollowed byonCancel, and the draft stays in the list. - Drafts are deleted only through
deleteanddeleteAll, which work withdraftsEnabledoff too.
Let the user pick a cover
Section titled “Let the user pick a cover”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.
Show render progress in your own UI
Section titled “Show render progress in your own UI”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,onFailureoronCancel. - 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.
Read usage data
Section titled “Read usage data”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 |
Handle failures
Section titled “Handle failures”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 |
Localize
Section titled “Localize”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.
"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.