Shoplive Developers
한국어KOEnglishEN日本語JA

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.

Getting started

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.

Start now →

Two ways to use it

The viewing side is a separate SDK Playing and listing the uploaded shorts in your app is covered by the Shortform SDK guide. This document covers the creating side only.

Mission map — what do you want to build?

What you wantMission
An "upload a short" button opens the editor and finishes with an uploadMission 1 · Open the editor
Match duration and aspect ratio to your policy, and hide tools you don't useMission 2 · Customize the screens
Keep your own gallery and borrow only the editingAppendix · Video editor alone
Let users re-pick just the thumbnailAppendix · Cover picker

Delivery Forms (Platform Mapping)

FormiOSAndroid
Full flowShopLiveShortformEditor.shared.start(vc)ShopLiveShortformEditor(activity).start()
Editor aloneShopliveVideoEditor.shared.build(data:completion:)ShopLiveVideoEditor(activity).start(data)
Cover pickerShopLiveCoverPicker.shared.build(data:completion:)ShopLiveCoverPicker(activity).start(data)
Image → video—ShopLiveImageEditor(activity).start(data)
Receiving the resultShopLiveShortformEditorDelegateShopLiveShortformEditorHandler
The platforms differ more here than in the Shortform SDK iOS hands you the editor screen (a ViewController) and your app presents it; Android opens an Activity itself and returns the result through a handler. The image editor is a standalone component on Android only (the cover picker exists on both). If you're integrating both platforms, absorb this difference in your own layer.

Requirements

ItemiOSAndroid
Minimum versioniOS 13.0+minSdk 23+ with shoplive-media3 (the default engine) · 19+ with shoplive-exoplayer
SDK version1.8.12 (ShopLiveShortformEditorSDK)1.8.14 (shoplive-video-editor)
Required companion modulesShopliveFilterSDK · ShopliveSDKCommonshoplive-common · shoplive-network
Runtime hostUIViewControllerComponentActivity (AppCompatActivity included)
App sizeFFmpeg-based encoding is bundled, so your app grows noticeably
Check size and ABIs up front The editor bundles native encoding libraries. On Android that's .so files for armeabi-v7a, arm64-v8a, x86 and x86_64, so if size matters, enable ABI splitting (App Bundle).
Getting started

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" />
Your app can handle denial itself (iOS) Pass a ShopLivePermissionHandler to setPermissionHandler and your app receives the photo, camera and microphone permission results, so you can present your own "open Settings" guidance (appendix).
Getting started

Quick Start

Prepare two things first You need both an 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()
Android — nothing works without a 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 →

Screens & demo

Screen Terminology Guide

The full flow is four screens in sequence. The missions and options below use the same names.

The flow

StepScreenWhat the user doesWhat you can control
①Media pickerPicks a video from the galleryAllowed duration range and the rejection message (Mission 2)
②Video editorTrim, crop, filter, speed, volumeWhich tools appear, the ratio, the duration limits (Mission 2)
③Cover pickerPicks the thumbnail frameRatio and whether the crop button shows (iOS · Android)
④UploadEnters title, description, hashtags and products, then publishesWhich inputs appear, initial values (tags, products), visibility (Mission 2)
Annotated captures coming The numbered screenshots used in the SDK v3 guide will land here once they are produced. Numbers ①–④ stay as they are when the images arrive.

Editing tools (screen ②)

ToolWhat it doesHow to turn it off
TrimDrag the start and end points to set the lengthAlways shown (you set the range only)
CropCrop to the target aspect ratioiOS editOptions · Android isUsedCropButton
FilterApply a color preset registered in Shoplive AdminiOS editOptions · Android isUsedFilterButton
Playback speedChange the speediOS editOptions · Android isUsedPlaybackSpeedButton
VolumeAdjust the original audio leveliOS editOptions · Android isUsedVolumeButton

Upload screen (screen ④)

InputDefaultControl
Description (body)ShowniOS isDescriptionVisible
HashtagsShowniOS isTagsVisible · Android ui.hashTag
Replace videoHiddenAndroid ui.videoChange
RatingHiddenAndroid ui.rating
Linked productsNoneAndroid products sets the initial values
VisibilityPublic (OPENED)iOS shortsStatus · Android shortformStatus

Previous ← Quick StartNext → Try It in the Demo App

Screens & demo

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.

  1. Full flow — the default path the SDK provides, from picking to uploading (Mission 1).
  2. Gallery + editor — a video picked by the app's own gallery fed into the editor (appendix).
  3. Editor alone — opening the editor with a file path or URL you already have.
  4. Change ratio, min/max duration, description and tag visibility, editing tools in the options popup and compare.
Demo distribution channels (TestFlight / APK) and the demo access key come with the SDK handover.

Previous ← Screen Terminology GuideNext → Mission 1 · Open the editor

Missions

Mission 1 · Open the editor

GoalTap "upload a short", finish picking, editing, covering and uploading, and receive the result
PrerequisitesInstallation · access key · a user identity (quick start)
  1. Prepare the delegate or handler that will receive the result.
  2. Start the editor from your button's action.
  3. 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
    }
}
The success callback has a different shape on each platform iOS returns 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

Missions

Mission 2 · Customize the screens

GoalMatch duration and ratio to your policy, and hide editing tools and inputs you don't use
PrerequisitesMission 1 done

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()
The units differ — seconds on iOS, milliseconds on Android The same "60 seconds" is 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".
iOS 1.8.12 — 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.
The picker limit and the editing limit are separate (iOS) 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.
What 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
    )
)
Only filters registered in Shoplive Admin appear Turning the filter button on with no presets in Shoplive Admin shows an empty list (your configuration).

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()
The Android full flow forces the publication status 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.
Upload as 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.
The two platforms will not look identical Colors and icons are customizable on both, but through different surfaces — per-screen *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

Appendix

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

SourceiOSAndroid
Local fileShopLiveVideoEditorData(videoUrl:)ShopLiveEditorLocalData(uri)
Camera captureShopLiveVideoEditorData(videoUrl:)ShopLiveEditorCameraData(uri)
Remote URLShopLiveVideoEditorData(videoRemoteUrl:)—
Absolute + relative pairShopLiveVideoEditorData(videoAbsoluteUrl:videoRelativeUrl:)—
Be clear about 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.
An unsupported codec produces 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.
Appendix

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.
Image-to-video is Android only If your product requires the same feature on both platforms, either build it in the app on iOS or take it out of scope.
Appendix

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.
Info.plist strings are reviewed If 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).
Reference

Data Field Dictionary

Configuration — where each setting lives

What you want to setiOSAndroid
Aspect ratiovideoCropOptionShopLiveVideoEditorData.aspectRatio
Result durationminVideoDuration · maxVideoDuration (seconds)minVideoDuration · maxVideoDuration (ms)
Selectable source durationmediaPickerVideoDurationOption—
Which editing tools showvisibleContents.editOptionsvisibleActionButton
Description / tag inputsvisibleContents.isDescriptionVisible/isTagsVisibleShopLiveShortformUploaderData.ui
Output quality and resolutionvideoOutputOptionoutputVideoQuality · outputResolution
Upload visibilityvideoUploadOption.shortsStatusshortformStatus
Register as a shortvideoUploadOption.isCreatedShortformisCreatedShortform
Colors and iconsShopLiveShortformEditor.*Config.globalsetVideoEditorCustomize(…)
Upload pre-fill (products, tags)ShopLiveShortformUploader.setUploaderData(…)setShortformUploaderData(…)

Ratio & duration

FieldDefault (iOS)Default (Android)Description
width : height9 : 169 : 16Crop ratio
isFixedtruefalseCan the user change the ratio — the platform defaults differ
minVideoDuration1 (s)1000 (ms)Minimum length of the result
maxVideoDuration60 (s)60000 (ms)Maximum length of the result
Media picker minimum3 (s)—Shortest source selectable from the gallery
Media picker maximum900 (s = 15 min)—Longest source selectable from the gallery
Invalid duration combinations are handled differently per platform iOS silently reverts to the defaults (1–60 s) when 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

FieldiOS ShopliveEditorResultDataAndroid ShopLiveEditorResultData
Shorts IDshortsIdshortsId
Local videolocalVideoUrl (String)localVideoUri (Uri)
Local coverlocalCoverImage (UIImage)localCoverImageUri (Uri)
Uploaded videoremoteOriginVideoUrlremoteOriginVideoUrl
Uploaded coverremoteCoverImageUrlremoteCoverImageUrl
Resolutionwidth · heightwidth · height
Durationduration (Double)duration (Long)
Capture timevideoCreatedAt—

Delegate & handler

WheniOS (full flow)Android (full flow)
Upload succeededonShopLiveShortformEditorUploadSuccess(result:)onSuccess(activity)
Encoding finishedonShopLiveShortformEditorVideoConvertSuccess(videoPath:)—
Cover confirmedonShopLiveShortformEditorCoverImageSuccess(image:)—
Closed / cancelledonShopLiveShortformEditorClosed()onCancel()
ErroronShopLiveShortformEditorError(error:)onError(context, error)
Behavior eventonShopLiveShortformEditorOnEvent(name:payload:)onEvent(context, command, payload)
WheniOS (editor alone)Android (editor alone)
SuccessonShopLiveVideoEditorUploadSuccess(editor:result:)onSuccess(activity, result)
Encoding finishedonShopLiveVideoEditorVideoConvertSuccess(editor:videoPath:)—
CancelledonShopLiveVideoEditorCancelled(editor:)onCancel()
ErroronShopLiveVideoEditorError(editor:error:)onError(context, error)
iOS callbacks include the 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.
Reference

Types & States

Editing tools (iOS SLEditOptions)

ValueToolAndroid equivalent
.filterFilterisUsedFilterButton
.playBackSpeedPlayback speedisUsedPlaybackSpeedButton
.volumeVolumeisUsedVolumeButton
.cropCropisUsedCropButton
The iOS default is all four ([.filter, .playBackSpeed, .volume, .crop]).

Output quality & resolution

AspectiOSAndroid
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)
The default quality differs by platform The same video produces a bigger file on iOS (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

ValueMeaning
OPENEDPublic immediately after upload (default)
CLOSEDUploaded 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.

CommandWhen
VIDEO_EDITOR_CLICK_CLOSE / …CONFIRMEditor closed / confirmed
VIDEO_EDITOR_CLICK_CROP / …CROP_CONFIRM / …CROP_CANCELCrop opened / confirmed / cancelled
VIDEO_EDITOR_CLICK_FILTER / …FILTER_CONFIRM / …FILTER_CANCELFilter opened / confirmed / cancelled
VIDEO_EDITOR_CLICK_VOLUME / …VOLUME_CONFIRM / …VOLUME_CANCELVolume opened / confirmed / cancelled
VIDEO_EDITOR_CLICK_PLAYBACK_SPEED / …CONFIRM / …CANCELSpeed opened / confirmed / cancelled
COVER_PICKER_CLICK_CAMERA_ROLL / …CLOSE / …CONFIRMCover picker (Android)
Reference

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).

CodeNameCause · fix
9000NOT_INITIALIZED_ACCESS_KEYThe editor started before setAccessKey
9002NOT_INITIALIZED_CONFIGEditor configuration not received from the server → check the network and retry
9050REQUIRED_COMPONENT_ACTIVITY(Android) A non-ComponentActivity context was passed
9300FAILED_ENCODINGEncoding failed → check free storage and the source file
9301FAILED_VIDEO_ENCODINGVideo encoding failed
9302FAILED_COVER_ENCODINGCover image generation failed
9320WITHOUT_THUMBNAILNo thumbnail could be produced → ask the user to pick a cover again
9501UNSUPPORTED_MEDIAUnsupported codec → tell the user this video can't be used
9900FAILED_NETWORKNetwork failure during upload → offer a retry
10000UNEXPECTED_ERRORUnclassified → contact us with the logs
Encoding and upload take time — design the interruption path Longer videos can take tens of seconds to encode. In that window the user may leave the app or take a call, breaking the flow. Always implement what to tell them on failure in onError.
Reference

Client-Configured Items

ItemWhereWhy it matters
accessKeyIssued by ShopliveRequired by every call
User identity (JWT)Your backendOwnership of the uploaded short. Without it nobody finds their own uploads
UGC enabledShoplive AdminUploads only succeed if UGC is open for your account
Filter presetsShoplive AdminWith none registered, the filter list shows up empty
Moderation policyYour operationsUpload as CLOSED or publish immediately (Mission 2)
Info.plist stringsYour appReviewed by Apple (install)
Reference

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.

Shoplive Shortform Editor SDK Integration Guide · based on iOS 1.8.12 / Android 1.8.14 · Docs