Implementing AI Video Generation (Runway/Sora) in a Mobile App
AI video generation currently server-only task. Models like Runway Gen-3, Sora, Kling, Hailuo require A100/H100 GPU and 30 seconds to several minutes per clip. Mobile dev task — correctly organize async flow, interface state, and deliver result to user.
Available API overview
| Provider | API | Clip length | Typical time | Input data |
|---|---|---|---|---|
| Runway Gen-3 Alpha | REST + polling | 5–10 sec | 30–90 sec | Text, Image-to-Video |
| Kling AI | REST API | 5–10 sec | 60–180 sec | Text, Image-to-Video |
| Hailuo (MiniMax) | REST API | 6 sec | 45–120 sec | Text, Image-to-Video |
| Luma Dream Machine | REST API | 5 sec | 30–60 sec | Text, Image, Keyframes |
| Replicate (various) | REST + WebSocket | 2–10 sec | 30–120 sec | Depends on model |
Sora from OpenAI available via API only within ChatGPT Enterprise — no public API as of March 2025. Runway — most mature public API with SDK for TypeScript/Python.
Async architecture: main challenge
User taps "Generate", waits a minute. App must:
- Show progress (though API usually returns only
PENDING/PROCESSING/SUCCEEDED) - Survive app minimizing
- Deliver result even if user returns in 5 minutes
// iOS: generation via Runway API
class VideoGenerationService {
func generate(prompt: String, sourceImage: UIImage?) async throws -> URL {
// 1. Create task
let taskId = try await runwayClient.createTask(
prompt: prompt,
imageURL: sourceImage.map { try await uploadImage($0) },
duration: 5,
ratio: "1280:768"
)
// 2. Save taskId — in case app minimizes
UserDefaults.standard.set(taskId, forKey: "pendingVideoTaskId")
// 3. Poll with escalating interval
return try await pollWithBackoff(taskId: taskId)
}
private func pollWithBackoff(taskId: String) async throws -> URL {
let intervals: [TimeInterval] = [3, 5, 8, 10, 10, 15, 15, 20, 20, 30]
for interval in intervals + Array(repeating: 30.0, count: 10) {
try await Task.sleep(nanoseconds: UInt64(interval * 1e9))
let task = try await runwayClient.getTask(id: taskId)
switch task.status {
case .succeeded:
UserDefaults.standard.removeObject(forKey: "pendingVideoTaskId")
return task.output.first!
case .failed:
throw VideoGenError.generationFailed(task.failure ?? "Unknown")
default: continue
}
}
throw VideoGenError.timeout
}
}
Android: WorkManager with CoroutineWorker — right choice for long background tasks. Polling in doWork(), Result.retry() on PROCESSING, Result.success(outputData) on SUCCEEDED.
Estimate real progress without API data
Runway and most APIs don't return percent complete — only status. But user wants progress. Solution: simulated progress bar based on typical generation time.
Start timer from beginning. Knowing average time 60 seconds, animate progress to 95% in 55 seconds, then stop and wait for real response. On success — quickly reach 100%. Better than spinner without context.
Download and play result
Runway returns temporary file URL (TTL usually 24–48 hours). Don't rely on it — download immediately to local storage.
// Android: download and cache video
class VideoDownloader(private val context: Context) {
suspend fun downloadAndCache(remoteUrl: String, videoId: String): File {
val cacheDir = File(context.filesDir, "generated_videos")
cacheDir.mkdirs()
val file = File(cacheDir, "$videoId.mp4")
// Download and save to file
return file
}
}
Store in app's cache directory (or user documents on request). Video player — ExoPlayer on Android, AVPlayer on iOS.
State persistence and recovery
If app crashes during polling — task continues on backend. On next open:
// Check if there are pending tasks on app launch
func checkPendingGenerations() async {
guard let taskId = UserDefaults.standard.string(forKey: "pendingVideoTaskId") else { return }
let task = try? await runwayClient.getTask(id: taskId)
if task?.status == .succeeded {
showNotification("Video ready: \(task?.output.first ?? "")")
}
}
Legal and acceptable content
Runway, Kling require terms agreeing generated videos won't be used for misinformation, non-consensual content, violence. Client must validate prompt before send (keyword filtering). If detected — reject on-client before API call, don't burden backend.
Timeline
Basic generation flow (API integration, polling, playback) — 4–6 days. With progress estimation, state recovery, caching, sharing to social — 2–3 weeks. Cost depends on provider and expected volume.







