Shoplive Shortform Editor SDK Integration Guide
This SDK lets your users create and upload shorts from inside your app.
Pick a video, edit it (trim, crop, filter, speed, volume), choose a cover, upload — the SDK provides every screen.
Switch the code samples with the iOS / Android tabs at the top of each block.
Overview & Requirements
The Shortform Editor SDK is for UGC — shorts your users make. The parts that take a long time to build yourself — trimming, cropping, filters, encoding, upload — are all handled by the SDK. Your app decides when to open it and what to do once it finishes.
Two ways to use it
Mission map — what do you want to build?
| What you want | Mission |
|---|---|
| An "upload a short" button opens the editor and finishes with an upload | Mission 1 · Open the editor |
| Match duration and aspect ratio to your policy, and hide tools you don't use | Mission 2 · Customize the screens |
| Keep your own gallery and borrow only the editing | Appendix · Video editor alone |
| Let users re-pick just the thumbnail | Appendix · Cover picker |
Delivery Forms (Platform Mapping)
| Form | iOS | Android |
|---|---|---|
| Full flow | ShopLiveShortformEditor.shared.start(vc) | ShopLiveShortformEditor(activity).start() |
| Editor alone | ShopliveVideoEditor.shared.build(data:completion:) | ShopLiveVideoEditor(activity).start(data) |
| Cover picker | ShopLiveCoverPicker.shared.build(data:completion:) | ShopLiveCoverPicker(activity).start(data) |
| Image → video | — | ShopLiveImageEditor(activity).start(data) |
| Receiving the result | ShopLiveShortformEditorDelegate | ShopLiveShortformEditorHandler |
Requirements
| Item | iOS | Android |
|---|---|---|
| Minimum version | iOS 13.0+ | minSdk 23+ with shoplive-media3 (the default engine) · 19+ with shoplive-exoplayer |
| SDK version | 1.8.12 (ShopLiveShortformEditorSDK) | 1.8.14 (shoplive-video-editor) |
| Required companion modules | ShopliveFilterSDK · ShopliveSDKCommon | shoplive-common · shoplive-network |
| Runtime host | UIViewController | ComponentActivity (AppCompatActivity included) |
| App size | FFmpeg-based encoding is bundled, so your app grows noticeably | |
.so files for
armeabi-v7a, arm64-v8a, x86 and x86_64, so if size matters,
enable ABI splitting (App Bundle).Installation
// ① Swift Package Manager
dependencies: [
.package(url: "https://github.com/shoplive/shortform-editor-ios", .upToNextMajor(from: "1.8.12")),
.package(url: "https://github.com/shoplive/common-ios", .upToNextMajor(from: "1.8.12")),
]
// Add both ShopLiveShortformEditorSDK and ShopliveFilterSDK to your target.
// ② CocoaPods — Podfile
// pod 'ShopLiveShortformEditorSDK', '1.8.12' # includes ShopliveFilterSDK
// pod 'ShopliveSDKCommon', '1.8.12'
// ③ Import
import ShopliveSDKCommon
import ShopLiveShortformEditorSDK
dependencies {
implementation("cloud.shoplive:shoplive-video-editor:1.8.14") // editor
implementation("cloud.shoplive:shoplive-common:1.8.14") // shared (access key, auth)
implementation("cloud.shoplive:shoplive-network:1.8.14") // networking
implementation("cloud.shoplive:shoplive-filter:1.8.14") // Filters (required by the editor)
// Playback engine — declare <b>only one</b> of these (engine versions are numbered separately from the SDK)
implementation("cloud.shoplive:shoplive-media3:1.10.1.3") // media3 (default) — minSdk 23
// implementation("cloud.shoplive:shoplive-exoplayer:2.19.1.20") // ExoPlayer (legacy) — minSdk 19
}
android {
defaultConfig { multiDexEnabled = true }
// Native encoding libraries are bundled — split by ABI if size matters
bundle { abi { enableSplit = true } }
}
Info.plist and permissions
<!-- Info.plist — the app crashes without these -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo access so you can pick a video for your short.</string>
<key>NSCameraUsageDescription</key>
<string>We need camera access so you can record a short.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access to record sound with your video.</string>
// Gallery selection uses PickVisualMedia (the photo picker), so no storage permission is needed.
// Only add these to AndroidManifest.xml if you also wire up recording:
// <uses-permission android:name="android.permission.CAMERA" />
// <uses-permission android:name="android.permission.RECORD_AUDIO" />
ShopLivePermissionHandler to setPermissionHandler and your app receives the
photo, camera and microphone permission results, so you can present your own "open Settings" guidance
(appendix).Quick Start
accessKey and a user identity.
An uploaded short belongs to its author, so without a signed-in user nobody can find their own uploads again.// ① Once at app startup
ShopLiveCommon.setAccessKey(accessKey: "YOUR_ACCESS_KEY")
ShopLiveCommon.setAuthToken(authToken: myJwtFromServer)
// ② From your "upload a short" button — pick, edit, cover and upload in one go
ShopLiveShortformEditor.shared
.setDelegate(delegate: self)
.start(self)
// ① Once at app startup (Application.onCreate)
ShopLiveCommon.setAccessKey("YOUR_ACCESS_KEY")
ShopLiveCommon.setAuthToken(myJwtFromServer)
// ② From your "upload a short" button — pick, edit, cover and upload in one go
ShopLiveShortformEditor(this) // this = ComponentActivity
.setHandler(myHandler)
.start()
ComponentActivity
The editor exchanges screen results through activityResultRegistry.
Pass an Application/Service context or an old-style Activity and you get
9050 REQUIRED_COMPONENT_ACTIVITY — or simply nothing at all.Next stepScreen Terminology Guide · Start with Mission 1 →
Screen Terminology Guide
The full flow is four screens in sequence. The missions and options below use the same names.
The flow
| Step | Screen | What the user does | What you can control |
|---|---|---|---|
| ① | Media picker | Picks a video from the gallery | Allowed duration range and the rejection message (Mission 2) |
| ② | Video editor | Trim, crop, filter, speed, volume | Which tools appear, the ratio, the duration limits (Mission 2) |
| ③ | Cover picker | Picks the thumbnail frame | Ratio and whether the crop button shows (iOS · Android) |
| ④ | Upload | Enters title, description, hashtags and products, then publishes | Which inputs appear, initial values (tags, products), visibility (Mission 2) |
Editing tools (screen ②)
| Tool | What it does | How to turn it off |
|---|---|---|
| Trim | Drag the start and end points to set the length | Always shown (you set the range only) |
| Crop | Crop to the target aspect ratio | iOS editOptions · Android isUsedCropButton |
| Filter | Apply a color preset registered in Shoplive Admin | iOS editOptions · Android isUsedFilterButton |
| Playback speed | Change the speed | iOS editOptions · Android isUsedPlaybackSpeedButton |
| Volume | Adjust the original audio level | iOS editOptions · Android isUsedVolumeButton |
Upload screen (screen ④)
| Input | Default | Control |
|---|---|---|
| Description (body) | Shown | iOS isDescriptionVisible |
| Hashtags | Shown | iOS isTagsVisible · Android ui.hashTag |
| Replace video | Hidden | Android ui.videoChange |
| Rating | Hidden | Android ui.rating |
| Linked products | None | Android products sets the initial values |
| Visibility | Public (OPENED) | iOS shortsStatus · Android shortformStatus |
Previous ← Quick StartNext → Try It in the Demo App
Try It in the Demo App
The demo app has an editor options popup, so you can see how Mission 2's settings land on screen without touching any code.
- Full flow — the default path the SDK provides, from picking to uploading (Mission 1).
- Gallery + editor — a video picked by the app's own gallery fed into the editor (appendix).
- Editor alone — opening the editor with a file path or URL you already have.
- Change ratio, min/max duration, description and tag visibility, editing tools in the options popup and compare.
Previous ← Screen Terminology GuideNext → Mission 1 · Open the editor
Mission 1 · Open the editor
- Prepare the delegate or handler that will receive the result.
- Start the editor from your button's action.
- Handle success, cancellation and errors separately.
Step 1 · Start it
@IBAction func createShorts() {
ShopLiveShortformEditor.shared
.setPermissionHandler(self) // optional — see the appendix
.setDelegate(delegate: self)
.start(self) // presented over the current view controller
}
// When your app has to close it
ShopLiveShortformEditor.shared.close()
fun createShorts() {
ShopLiveShortformEditor(this) // this = ComponentActivity
.setHandler(myHandler)
.start()
}
Step 2 · Receive the result
extension MyViewController: ShopLiveShortformEditorDelegate {
func onShopLiveShortformEditorUploadSuccess(result: ShopliveEditorResultData?) {
// Uploaded — result.shortsId can play the short you just made
guard let shortsId = result?.shortsId else { return }
myFeed.insertNewShorts(shortsId)
}
func onShopLiveShortformEditorVideoConvertSuccess(videoPath: String) {
// The encoded local file path (before upload)
}
func onShopLiveShortformEditorCoverImageSuccess(image: UIImage?) {
// The cover image the user picked
}
func onShopLiveShortformEditorClosed() {
// Closed midway — offer a "save draft" prompt if that fits your app
}
func onShopLiveShortformEditorError(error: ShopLiveCommonError) {
log("editor error \(error.codes) \(error.message ?? "")")
}
func onShopLiveShortformEditorOnEvent(name: String, payload: [String : Any]?) {
analytics.log(name) // per-button behavior logging
}
}
val myHandler = object : ShopLiveShortformEditorHandler() {
override fun onSuccess(activity: ComponentActivity) {
// Uploaded — refresh the list or go to the "my shorts" tab
activity.finish()
myFeed.refresh()
}
override fun onCancel() {
// Closed midway
}
override fun onError(context: Context, error: ShopLiveCommonError) {
log("editor error ${error.code} ${error.message}")
}
override fun onEvent(context: Context, command: String, payload: Map<String, Any?>) {
analytics.log(command) // per-button behavior logging
}
}
ShopliveEditorResultData (including shortsId), while Android's full-flow
onSuccess hands you only the activity.
If you need shortsId on Android, use ShopLiveEditorResultData.shortsId from the
editor-alone path, or re-query the list after upload.Step 3 · Pre-fill the upload screen (Android)
For a flow like "make a short from this product", you can pre-fill the products and tags.
ShopLiveShortformEditor(this)
.setShortformUploaderData(
ShopLiveShortformUploaderData(
tags = listOf("spring"),
products = listOf(
ShopLiveShortformUploaderProductData(
customerProductId = "P-1001",
sku = "SKU-1001"
)
),
ui = ShopLiveShortformUploaderUiData(
hashTag = true,
videoChange = false,
rating = false
)
)
)
.setHandler(myHandler)
.start()
// On iOS you open the uploader as a standalone component and pre-fill it there.
// (The full flow, ShopLiveShortformEditor, has no injection point.)
ShopLiveShortformUploader.shared
.setDelegate(self)
.setUploaderData(ShopLiveShortformUploaderData(
ui: ShopLiveUgcUploaderUiData(hashTag: true, videoChange: false, rating: false),
tags: ["spring"],
products: [ShopLiveConversionProductData(sku: "SKU-1001", customProductId: "P-1001")]
))
.build { uploaderVC in
uploaderVC.modalPresentationStyle = .fullScreen
self.present(uploaderVC, animated: true)
}
let visible = ShopLiveShortFormEditorVisibleContent(
isDescriptionVisible: true,
isTagsVisible: true
)
Completion Checklist
- The tap opens the media picker
- Picking a video moves on to the editing screen
- Finishing the upload fires the success callback
- Closing midway fires the cancel callback and your screen comes back cleanly
- The uploaded short shows up in the Shortform SDK list
Related: PermissionsNext → Mission 2 · Customize the screens
Mission 2 · Customize the screens
Option ① · Duration and aspect ratio
let config = ShopLiveShortformEditorConfiguration(
videoCropOption: ShopLiveShortFormEditorAspectRatio(width: 9, height: 16, isFixed: true),
visibleContents: nil,
videoOutputOption: ShopLiveShortformEditorVideoOuputOption(
videoOutputQuality: .high,
videoOutputResoltuion: ._720
),
mediaPickerVideoDurationOption: ShopLiveMediaPickerVideoDurationOption(
minVideoDuration: 3, // seconds
maxVideoDuration: 60 * 5,
invalidDurationToastMessage: "Videos must be between 3 seconds and 5 minutes"
),
videoUploadOption: nil,
minVideoDuration: 3, // length of the edited result (seconds)
maxVideoDuration: 60
)
ShopLiveShortformEditor.shared
.setConfiguration(config)
.setDelegate(delegate: self)
.start(self)
ShopLiveShortformEditor(this)
.setVideoEditorData(
ShopLiveVideoEditorData(
minVideoDuration = 3_000, // milliseconds
maxVideoDuration = 60_000,
aspectRatio = ShopLiveShortformEditorAspectRatio(
width = 9, height = 16, isFixed = true
),
outputVideoQuality = ShopLiveVideoEditorVideoQuality.HIGH,
outputResolution = ShopLiveVideoEditorResolution.RESOLUTION_720
)
)
.setHandler(myHandler)
.start()
60 on iOS and 60_000 on Android.
If you keep shared constants, define them in one unit and convert. Being off by 1000×
shows up as "every video is too short / too long".mediaPickerVideoDurationOption is not applied
ShopLiveShortformEditorConfiguration's initializer accepts the argument but never assigns it,
so the picker duration limit always runs at its default (3 seconds to 15 minutes). If you must constrain
the picking step, use the standalone ShopLiveMediaPicker path or ask for an SDK fix. The note below
describes the intended design; the edited-result limits (min/maxVideoDuration) do apply correctly.mediaPickerVideoDurationOption is the length of the source you can pick from the gallery;
min/maxVideoDuration is the length of the edited result.
Set them differently to let someone pick a 5-minute source and trim it to 60 seconds.isFixed means
true stops the user from changing the ratio (locked at 9:16). For a vertical-only feed, prefer true.
The default is true on iOS and false on Android — they differ, so set both explicitly.Option ② · Hide editing tools
// Keep filter and crop; hide speed and volume
let visible = ShopLiveShortFormEditorVisibleContent(
isDescriptionVisible: true,
isTagsVisible: true,
editOptions: [.filter, .crop] // default: [.filter, .playBackSpeed, .volume, .crop]
)
ShopLiveVideoEditorData(
visibleActionButton = ShopLiveShortformEditorVisibleActionButton(
isUsedFilterButton = true,
isUsedCropButton = true,
isUsedPlaybackSpeedButton = false,
isUsedVolumeButton = false
)
)
Option ③ · Upload screen inputs
// Description and hashtag visibility, plus the upload policy
let visible = ShopLiveShortFormEditorVisibleContent(
isDescriptionVisible: true,
isTagsVisible: false // hide the hashtag input
)
let upload = ShopLiveShortFormEditorVideoUploadOption(
shortsStatus: .CLOSED, // private on upload → publish after review
isCreatedShortform: true
)
ShopLiveShortformEditor(this)
.setShortformUploaderData(
ShopLiveShortformUploaderData(
ui = ShopLiveShortformUploaderUiData(hashTag = false)
)
)
.setVideoEditorData(
ShopLiveVideoEditorData(
shortformStatus = ShopLiveShortformStatus.CLOSED // upload as private
)
)
.setHandler(myHandler)
.start()
ShopLiveShortformEditor (the full flow) overwrites isCreatedShortform = true and
shortformStatus = CLOSED internally. The setting below is therefore ignored in the full flow and
everything uploads as private. To control it yourself, use the editor-alone path.CLOSED if you moderate UGC
Uploading as CLOSED lets you review in Shoplive Admin before publishing.
For any service where showing user content immediately is uncomfortable, this should be your default.Option ④ · Colors and icons (Android)
On Android you can replace the buttons, sliders and trim colors on the editor screens.
ShopLiveShortformEditor(this)
.setVideoEditorCustomize(
ShopLiveVideoEditorCustomizeData(
mainData = ShopLiveVideoEditorCustomizeMainData(
titleTextSize = 16f,
backButtonBackgroundRes = R.drawable.ic_my_back,
closeButtonBackgroundRes = R.drawable.ic_my_close,
confirmButtonTextColor = Color.WHITE,
confirmButtonBackgroundRes = R.drawable.bg_my_button,
trimColor = Color.parseColor("#245EFF"),
trimHandleColor = Color.WHITE
)
// volumeData · cropData · filterData · playbackSpeedData work the same way
)
)
.setHandler(myHandler)
.start()
// On iOS you replace icons and colors through per-screen global config singletons.
// Set them once before presenting the editor.
ShopLiveShortformEditor.MediaPickerConfig.global.cellCornerRadius = 8
ShopLiveShortformEditor.EditorMainConfig.global.confirmButtonBackgroundColor = .systemPink
ShopLiveShortformEditor.EditorMainConfig.global.confirmButtonTextColor = .white
ShopLiveShortformEditor.EditorCropConfig.global.cropColor = .systemPink
// EditorVolumeConfig · EditorSpeedConfig · EditorFilterConfig
// · EditorCoverPickerConfig expose .global the same way.
*Config.global
singletons on iOS, setVideoEditorCustomize(…) on Android. Don't make pixel-identical screens a requirement;
agreeing on which features are exposed is the realistic goal.Completion Checklist
- Picking a video shorter or longer than your policy shows the guidance message
- Only the tools you enabled appear on the editing screen
- Inputs you hid are absent from the upload screen
- The uploaded short has the visibility you intended (public or private)
Previous ← Mission 1Next → Appendix · Using the video editor alone
Using the video editor alone
For when your app already has a gallery or camera screen and you only need the editing. You hand it the video you picked, only the editor opens, and your app gets the result back.
// iOS — it returns the editor "screen", so your app presents it
ShopliveVideoEditor.shared
.setDelegate(self)
.setConfiguration(ShopliveVideoEditorConfiguration(
videoCropOption: ShopliveVideoEditorAspectRatio(width: 9, height: 16, isFixed: true),
videoOutputOption: nil,
videoTrimOption: ShopliveVideoEditorTrimOption(minVideoDuration: 3, maxVideoDuration: 60),
visibleContents: nil,
videoUploadOption: nil
))
.build(data: ShopLiveVideoEditorData(videoUrl: pickedFileURL, isCreatedShortform: true)) { editorVC in
editorVC.modalPresentationStyle = .fullScreen
self.present(editorVC, animated: true)
}
// Free memory after closing the editor
ShopliveVideoEditor.shared.cleanUpMemory()
// Android — the SDK opens an Activity and returns the result through the handler
ShopLiveVideoEditor(this)
.setData(ShopLiveVideoEditorData(
minVideoDuration = 3_000,
maxVideoDuration = 60_000,
isCreatedShortform = true
))
.setHandler(object : ShopLiveVideoEditorHandler() {
override fun onSuccess(
videoEditorActivity: ComponentActivity,
result: ShopLiveEditorResultData
) {
// result.shortsId · localVideoUri · localCoverImageUri · duration …
ShopLiveVideoEditor.close()
uploadToMyServer(result.localVideoUri)
}
override fun onCancel() { /* the user closed it */ }
override fun onError(context: Context, error: ShopLiveCommonError) { log(error) }
})
.start(ShopLiveEditorLocalData(pickedUri)) // ShopLiveEditorCameraData for camera results
Input sources
| Source | iOS | Android |
|---|---|---|
| Local file | ShopLiveVideoEditorData(videoUrl:) | ShopLiveEditorLocalData(uri) |
| Camera capture | ShopLiveVideoEditorData(videoUrl:) | ShopLiveEditorCameraData(uri) |
| Remote URL | ShopLiveVideoEditorData(videoRemoteUrl:) | — |
| Absolute + relative pair | ShopLiveVideoEditorData(videoAbsoluteUrl:videoRelativeUrl:) | — |
isCreatedShortform
true means the edited result is registered as a Shoplive short (which is why you get a shortsId).
If your app only wants the output file and no registration, set it to false.9501
The editor checks the input codec first. If arbitrary files can reach it from your own gallery,
always build a path that catches UNSUPPORTED_MEDIA and tells the user.Cover picker & image editor
Standalone components for re-picking a thumbnail, or turning a still image into a short video. The cover picker is on both platforms; the image editor is Android only.
Cover picker
ShopLiveCoverPicker(this)
.setData(ShopLiveCoverPickerData(
shortsId = "SHORTS_ID",
aspectRatio = ShopLiveShortformEditorAspectRatio(9, 16, isFixed = true),
visibleActionButton = ShopLiveCoverPickerVisibleActionButton(isUsedCropButton = true)
))
.setHandler(object : ShopLiveCoverPickerHandler() {
override fun onSuccess(
coverPickerActivity: ComponentActivity,
result: ShopLiveEditorResultData
) {
ShopLiveCoverPicker.close()
applyCover(result.localCoverImageUri)
}
override fun onCancel() { /* … */ }
})
.start(ShopLiveCoverPickerLocalData(localVideoUri)) // or ShopLiveCoverPickerUrlData(remoteUrl)
// iOS can open the cover picker on its own too (it returns a screen, so your app presents it)
ShopLiveCoverPicker.shared
.setDelegate(self)
.setConfiguration(ShopLiveCoverPickerConfiguration(
cropOption: ShopLiveShortFormEditorAspectRatio(width: 9, height: 16, isFixed: true),
visibleActionButton: ShopLiveCoverPickerVisibleActionButton(editOptions: [.crop])
))
.build(data: ShopLiveCoverPickerData(videoUrl: localVideoURL, shortsId: shortsId)) { pickerVC in
self.present(pickerVC, animated: true)
}
// Free memory after closing it
ShopLiveCoverPicker.shared.cleanUpMemory()
Image → short video
ShopLiveImageEditor(this)
.setData(ShopLiveImageEditorData(
isCreatedShortform = true,
resolution = ShopLiveVideoEditorResolution.RESOLUTION_720,
videoDuration = 10_000 // milliseconds — a 10-second video from the image
))
.setHandler(object : ShopLiveImageEditorHandler() {
override fun onSuccess(result: ShopLiveEditorResultData) { /* … */ }
override fun onError(error: ShopLiveCommonError) { log(error) }
})
.start(ShopLiveEditorLocalData(pickedImageUri))
// iOS exposes no image-to-video API.
Permissions
Use this when you want your own guidance instead of the SDK's default behavior after a photo, camera or microphone denial.
ShopLiveShortformEditor.shared
.setPermissionHandler(self)
.setDelegate(delegate: self)
.start(self)
extension MyViewController: ShopLivePermissionHandler {
func handlePhotoLibraryUsagePermission(status: PermissionStatus) {
// On denial, show your own "please allow photo access in Settings" prompt
}
func handleCameraPermission(status: PermissionStatus) { /* … */ }
func handleMicroPhonePermission(status: PermissionStatus) { /* … */ }
func handleMediaLibraryUsagePermission(status: PermissionStatus) { /* … */ }
}
// Android uses the photo picker (PickVisualMedia), so there is no storage permission to handle.
// Only when you add recording does your app request CAMERA and RECORD_AUDIO itself, then
// start the editor with ShopLiveEditorCameraData once granted.
NSPhotoLibraryUsageDescription and friends don't match the real purpose, app review can reject you.
Be concrete — "to create a short", not "to access your photos" (install).Data Field Dictionary
Configuration — where each setting lives
| What you want to set | iOS | Android |
|---|---|---|
| Aspect ratio | videoCropOption | ShopLiveVideoEditorData.aspectRatio |
| Result duration | minVideoDuration · maxVideoDuration (seconds) | minVideoDuration · maxVideoDuration (ms) |
| Selectable source duration | mediaPickerVideoDurationOption | — |
| Which editing tools show | visibleContents.editOptions | visibleActionButton |
| Description / tag inputs | visibleContents.isDescriptionVisible/isTagsVisible | ShopLiveShortformUploaderData.ui |
| Output quality and resolution | videoOutputOption | outputVideoQuality · outputResolution |
| Upload visibility | videoUploadOption.shortsStatus | shortformStatus |
| Register as a short | videoUploadOption.isCreatedShortform | isCreatedShortform |
| Colors and icons | ShopLiveShortformEditor.*Config.global | setVideoEditorCustomize(…) |
| Upload pre-fill (products, tags) | ShopLiveShortformUploader.setUploaderData(…) | setShortformUploaderData(…) |
Ratio & duration
| Field | Default (iOS) | Default (Android) | Description |
|---|---|---|---|
width : height | 9 : 16 | 9 : 16 | Crop ratio |
isFixed | true | false | Can the user change the ratio — the platform defaults differ |
minVideoDuration | 1 (s) | 1000 (ms) | Minimum length of the result |
maxVideoDuration | 60 (s) | 60000 (ms) | Maximum length of the result |
| Media picker minimum | 3 (s) | — | Shortest source selectable from the gallery |
| Media picker maximum | 900 (s = 15 min) | — | Longest source selectable from the gallery |
maxVideoDuration ≤ minVideoDuration.
Android only replaces values below 1 with its defaults (1000 ms / 60000 ms) and does not validate max ≤ min at all.
Neither raises an error, so you may not notice the setting was ignored. If you compute these values, validate the order yourself.Result data
| Field | iOS ShopliveEditorResultData | Android ShopLiveEditorResultData |
|---|---|---|
| Shorts ID | shortsId | shortsId |
| Local video | localVideoUrl (String) | localVideoUri (Uri) |
| Local cover | localCoverImage (UIImage) | localCoverImageUri (Uri) |
| Uploaded video | remoteOriginVideoUrl | remoteOriginVideoUrl |
| Uploaded cover | remoteCoverImageUrl | remoteCoverImageUrl |
| Resolution | width · height | width · height |
| Duration | duration (Double) | duration (Long) |
| Capture time | videoCreatedAt | — |
Delegate & handler
| When | iOS (full flow) | Android (full flow) |
|---|---|---|
| Upload succeeded | onShopLiveShortformEditorUploadSuccess(result:) | onSuccess(activity) |
| Encoding finished | onShopLiveShortformEditorVideoConvertSuccess(videoPath:) | — |
| Cover confirmed | onShopLiveShortformEditorCoverImageSuccess(image:) | — |
| Closed / cancelled | onShopLiveShortformEditorClosed() | onCancel() |
| Error | onShopLiveShortformEditorError(error:) | onError(context, error) |
| Behavior event | onShopLiveShortformEditorOnEvent(name:payload:) | onEvent(context, command, payload) |
| When | iOS (editor alone) | Android (editor alone) |
|---|---|---|
| Success | onShopLiveVideoEditorUploadSuccess(editor:result:) | onSuccess(activity, result) |
| Encoding finished | onShopLiveVideoEditorVideoConvertSuccess(editor:videoPath:) | — |
| Cancelled | onShopLiveVideoEditorCancelled(editor:) | onCancel() |
| Error | onShopLiveVideoEditorError(editor:error:) | onError(context, error) |
editor view controller
The editor-alone delegate tells you which editor an event came from.
Use that reference to dismiss the editor or present an alert over it.Types & States
Editing tools (iOS SLEditOptions)
| Value | Tool | Android equivalent |
|---|---|---|
.filter | Filter | isUsedFilterButton |
.playBackSpeed | Playback speed | isUsedPlaybackSpeedButton |
.volume | Volume | isUsedVolumeButton |
.crop | Crop | isUsedCropButton |
[.filter, .playBackSpeed, .volume, .crop]).Output quality & resolution
| Aspect | iOS | Android |
|---|---|---|
| Quality | .normal · .high · .max (default .high) | NORMAL · HIGH · MAX (default NORMAL) |
| Resolution | ._360 · ._480 · ._720 · ._1080 (default ._720) | RESOLUTION_360/480/720/960/1080 (default 720) |
high vs Android's NORMAL).
Set both explicitly if upload size and time have to match.
Note that RESOLUTION_960 exists only on Android.Visibility
| Value | Meaning |
|---|---|
OPENED | Public immediately after upload (default) |
CLOSED | Uploaded as private — publish after review in Shoplive Admin |
Behavior events
What arrives in onEvent. These are for analytics, and the list can grow, so ignore values you don't know.
| Command | When |
|---|---|
VIDEO_EDITOR_CLICK_CLOSE / …CONFIRM | Editor closed / confirmed |
VIDEO_EDITOR_CLICK_CROP / …CROP_CONFIRM / …CROP_CANCEL | Crop opened / confirmed / cancelled |
VIDEO_EDITOR_CLICK_FILTER / …FILTER_CONFIRM / …FILTER_CANCEL | Filter opened / confirmed / cancelled |
VIDEO_EDITOR_CLICK_VOLUME / …VOLUME_CONFIRM / …VOLUME_CANCEL | Volume opened / confirmed / cancelled |
VIDEO_EDITOR_CLICK_PLAYBACK_SPEED / …CONFIRM / …CANCEL | Speed opened / confirmed / cancelled |
COVER_PICKER_CLICK_CAMERA_ROLL / …CLOSE / …CONFIRM | Cover picker (Android) |
Error Handling
Errors arrive through the delegate's or handler's onError. It's the same code scheme as the
Shortform SDK; the editor mostly meets the 9300s (encoding) and 9500s (unsupported media).
| Code | Name | Cause · fix |
|---|---|---|
9000 | NOT_INITIALIZED_ACCESS_KEY | The editor started before setAccessKey |
9002 | NOT_INITIALIZED_CONFIG | Editor configuration not received from the server → check the network and retry |
9050 | REQUIRED_COMPONENT_ACTIVITY | (Android) A non-ComponentActivity context was passed |
9300 | FAILED_ENCODING | Encoding failed → check free storage and the source file |
9301 | FAILED_VIDEO_ENCODING | Video encoding failed |
9302 | FAILED_COVER_ENCODING | Cover image generation failed |
9320 | WITHOUT_THUMBNAIL | No thumbnail could be produced → ask the user to pick a cover again |
9501 | UNSUPPORTED_MEDIA | Unsupported codec → tell the user this video can't be used |
9900 | FAILED_NETWORK | Network failure during upload → offer a retry |
10000 | UNEXPECTED_ERROR | Unclassified → contact us with the logs |
onError.Client-Configured Items
| Item | Where | Why it matters |
|---|---|---|
| accessKey | Issued by Shoplive | Required by every call |
| User identity (JWT) | Your backend | Ownership of the uploaded short. Without it nobody finds their own uploads |
| UGC enabled | Shoplive Admin | Uploads only succeed if UGC is open for your account |
| Filter presets | Shoplive Admin | With none registered, the filter list shows up empty |
| Moderation policy | Your operations | Upload as CLOSED or publish immediately (Mission 2) |
| Info.plist strings | Your app | Reviewed by Apple (install) |
Troubleshooting & FAQ
Tapping the button does nothing (Android)
Most likely a non-ComponentActivity context. The editor exchanges screen results through
activityResultRegistry, so applicationContext or an old-style Activity
won't work (quick start).
My duration limits aren't applied
Check the units — seconds on iOS, milliseconds on Android.
Also, if maxVideoDuration is at or below minVideoDuration, the SDK
silently reverts to the defaults (ratio & duration).
The ratio seems to lock only on iOS
isFixed defaults to true on iOS and false on Android.
Set it explicitly on both (Mission 2).
I enabled the filter button but the list is empty
No filter presets are registered in Shoplive Admin (your configuration).
The upload succeeded but the short doesn't appear in the list
① It was uploaded as CLOSED (private) — publish it in Shoplive Admin.
② With isCreatedShortform = false it is not registered as a short at all.
③ Check that the query (tags, collection) matches what was entered at upload time.
My app got a lot bigger
Native encoding libraries ship for four ABIs. Turning on ABI splitting in your Android App Bundle shrinks the per-device download (install).
Can I make both platforms look identical?
Not exactly — color and icon customization is Android only. Aligning which features are exposed (tools, inputs) is the realistic goal (Mission 2).
How do I play the uploaded short in my app?
See the Shortform SDK guide. Pass the result's shortsId
straight through to play it.