Skip to content

Android

show.infini:videoeditor is the Android SDK, a single AAR built on Media3 and Compose.

  • minSdk 24, compileSdk 36, AGP 8.x.
  • Kotlin 2.1+ (the SDK is compiled with Kotlin 2.2.20). Java-only hosts work too.
  • Compose runtime and Media3 1.10 come in as transitive dependencies. The host doesn’t need to use Compose. If the host also uses Media3, Gradle picks the higher version.
repositories {
maven("https://dl.infinive.dev/maven")
}
dependencies {
implementation("show.infini:videoeditor:1.2.4")
}

No manifest changes. The editor activity and the INTERNET permission (GIPHY) are merged from the SDK manifest; video selection uses the photo picker.

import show.infini.videoeditor.VideoEditorActivity
import show.infini.videoeditor.editor.EditorConfig
val editor = registerForActivityResult(VideoEditorActivity.Contract) { result ->
if (result != null) upload(result.videoPath) // null: cancelled
}
editor.launch(
VideoEditorActivity.intent(
context = this,
initialSource = pickedUri, // optional; omit to open the picker
config = EditorConfig(licenseKey = "ve_live_…", giphyApiKey = "…"),
onFailure = { failure -> Log.w(TAG, "editor failed: $failure") },
),
)

The activity finishes itself. With startActivityForResult, unpack the result with VideoEditorActivity.result(resultCode, data).

initialSource is a content:// URI or a file path to a local video or photo. A photo becomes a one-second image clip; a photo path needs its file extension.

Field Value
videoPath Rendered MP4 in the app cache. Move, upload or delete it.
coverPath Cover JPEG. Empty if no cover could be extracted (COVER_UNAVAILABLE arrives first).
durationSec Duration in seconds.
audioPaths Audio URIs or paths used, in timeline order, as audioPicker or the system picker returned them.
draftId Draft holding this edit; null if drafts are off or nothing was saved.
usage Usage stats. See “Read usage data”.
EditorConfig
licenseKey Required.
giphyApiKey Sticker tool is disabled without it.
textFonts Replaces the bundled fonts. Omit to keep them. Empty list 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. null follows the app language, including Android 13 per-app language.
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 throw IllegalArgumentException at construction: empty licenseKey, minimum above maximum, empty font list, unknown OutputSpec.codec, initialSource with draftId, draftId without draftsEnabled.

Pass output = OutputSpec(…) to intent(). null uses OutputSpec.DEFAULT: 9:16, 7 Mbps cap, 30 fps cap, H.264. codec is CODEC_H264 or CODEC_HEVC.

OutputSpec.AspectRatio Size
RATIO_9_16 (default) 1080×1920
RATIO_1_1 1080×1080
RATIO_4_5 1080×1350
RATIO_3_4 1080×1440
RATIO_16_9 1920×1080

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

VideoEditorActivity.intent(
context = this,
config = …,
audioPicker = Intent(this, MySoundLibraryActivity::class.java),
)
// In MySoundLibraryActivity
setResult(RESULT_OK, Intent().setData(uri).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION))
finish()

The editor starts audioPicker for a result when the user adds or replaces audio. Return a content:// or file:// URI as data; RESULT_CANCELED or no data cancels. URIs from your own ContentProvider need FLAG_GRANT_READ_URI_PERMISSION. Without audioPicker, the system document picker opens.

TextFontSpec(displayName = "Brand", asset = "fonts/Brand-Bold.ttf")
  • asset is a path in your app’s assets/, as accepted by AssetManager.open.
  • Give the bold face. Overlay text uses one semi-bold weight; synthetic bold isn’t applied.
  • The path 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, back, or onStop. A render keeps the draft and returns its id in result.draftId.

import show.infini.videoeditor.editor.EditorDrafts
val drafts = EditorDrafts.list(context) // suspend; newest first
editor.launch(
VideoEditorActivity.intent(
context = this,
draftId = drafts[0].id,
config = EditorConfig(licenseKey = "ve_live_…", draftsEnabled = true),
),
)
EditorDrafts.delete(context, drafts[0].id)
EditorDrafts.deleteAll(context)
  • A save arrives as RESULT_CANCELED. List again to see it.
  • Unchanged edits and empty timelines aren’t saved. Undo history isn’t saved.
  • EditorDraft: id, createdAt, updatedAt (epoch ms), durationSec, clipCount, coverPath (first-frame JPEG; empty until extracted).
  • Videos and audio are referenced, not copied. If a source is gone, opening fails with SOURCE_UNREADABLE followed by RESULT_CANCELED, and the draft stays in the list.
  • Drafts are deleted only through delete and deleteAll, which work with draftsEnabled off too. All draft calls are suspending and safe from any thread.

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 an EditorRenderJob that reports progress, the result and failures. Launch with RenderContract.

val editor = registerForActivityResult(VideoEditorActivity.RenderContract) { job ->
if (job != null) uploadBanner.show(job.progress) // null: cancelled
}
editor.launch(
VideoEditorActivity.intent(
this,
config = EditorConfig(licenseKey = "ve_live_…", draftsEnabled = true, detachedRenderEnabled = true),
),
)
Application.onCreate
appScope.launch {
EditorRenderJob.current.filterNotNull().collect { job ->
when (val end = job.state.first { it != EditorRenderState.Rendering }) {
is EditorRenderState.Completed -> upload(end.result.videoPath, end.result.coverPath)
is EditorRenderState.Failed -> showRetry(job.draftId)
else -> Unit
}
}
}

Note: collect the outcome in an application-lifetime scope.

EditorRenderJob
current The running job (StateFlow). null once it ends.
draftId Draft holding the rendered state. null when draftsEnabled is off.
progress StateFlow<Double>, 0 to 1.
state Changes once from Rendering to Completed(result), Failed(failure) or Cancelled. An empty coverPath means no cover could be extracted.
cancel() Stops the render. Call on the main thread.
  • The only failure is RENDER_FAILED, delivered through state.
  • With startActivityForResult, get the job with VideoEditorActivity.renderJob(resultCode, data).
  • 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 (EditorUsage) 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; null 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

intent(onFailure=) reports failures on the main thread; the editor shows its own message. Closing or cancelling arrives as RESULT_CANCELED.

reason When Then
SOURCE_UNREADABLE A source or draft couldn’t be opened With no clips left, finishes with RESULT_CANCELED
RENDER_FAILED Render failed; nativeCode is Media3 ExportException.errorCode Back to editing; “Next” can be tapped again
COVER_UNAVAILABLE Render succeeded, cover extraction failed Result follows with an empty coverPath
LICENSE_INVALID No valid license On open: finishes with RESULT_CANCELED. 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. If your app has its own language setting, pass it as EditorConfig.locale.

To change a string or add a language, define the same name in your strings.xml. Resource merging gives your value precedence.

res/values-ko/strings.xml
<string name="videoeditor_next">계속</string>

Names start with videoeditor_; the full list is res/values/public.xml in the AAR. The two strings with numbers are <plurals>.

SDK logs are on in debug builds of the host. To turn them on in a release build, run this before launching the app:

adb shell setprop log.tag.VideoEditor DEBUG

For a crash inside the SDK, send the stack trace with the SDK version.