Originally published at ffmpeg-micro.com
You need server-side video processing in your Swift app. Maybe you're building a Vapor backend that transcodes user uploads, a macOS utility that batch-converts media files, or a command-line tool that generates thumbnails. FFmpeg is the standard tool for the job, but getting it into a Swift project isn't as simple as adding a package dependency.
Running FFmpeg from Swift with Process
Swift's Foundation framework provides the Process class for running external commands. If FFmpeg is installed on the machine, you can shell out to it directly:
import Foundation
let process = Process()
process.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/ffmpeg")
process.arguments = [
"-i", "input.mp4",
"-c:v", "libx264",
"-crf", "23",
"-preset", "medium",
"-c:a", "aac",
"-b:a", "128k",
"output.mp4"
]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
print(output)
guard process.terminationStatus == 0 else {
fatalError("FFmpeg failed with exit code \(process.terminationStatus)")
}
This works on macOS and Linux. Install FFmpeg with brew install ffmpeg on macOS or apt-get install ffmpeg on Ubuntu, point executableURL at the binary, and you're running.
But you own that FFmpeg install on every machine. On Linux servers, you're managing the binary across deploys. On macOS CI runners, you're adding Homebrew steps to your build pipeline. And on iOS, Process doesn't exist at all.
Processing Video via Cloud API (No FFmpeg Install)
Skip the local binary entirely. FFmpeg Micro exposes full FFmpeg capabilities through a REST API. Send a video URL, pick your settings, get processed video back. If you're familiar with how this works in Node.js or Kotlin, the pattern is identical. Here's the basic flow using URLSession:
import Foundation
let apiKey = ProcessInfo.processInfo.environment["FFMPEG_MICRO_API_KEY"]!
let requestBody: [String: Any] = [
"inputs": [["url": "https://example.com/input.mp4"]],
"outputFormat": "mp4",
"preset": ["quality": "high", "resolution": "1080p"]
]
var request = URLRequest(url: URL(string: "https://api.ffmpeg-micro.com/v1/transcodes")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let jobId = result["id"] as! String
print("Job created: \(jobId)")
No binary to install. No Homebrew dependency. No Docker image bloat. Just an HTTP call from any Swift environment, including iOS.
For operations that need specific FFmpeg flags, use raw options instead of presets:
let advancedBody: [String: Any] = [
"inputs": [["url": "https://example.com/input.mp4"]],
"outputFormat": "webm",
"options": [
["option": "-c:v", "argument": "libvpx-vp9"],
["option": "-crf", "argument": "30"],
["option": "-b:v", "argument": "0"]
]
]
Using async/await with FFmpeg Micro
The previous section used raw dictionaries and JSONSerialization. For production Swift code, Codable structs and a proper async flow are cleaner. Here's a typed wrapper that handles the full lifecycle: create a job, poll for completion, and get the download URL.
import Foundation
struct TranscodeRequest: Encodable {
let inputs: [TranscodeInput]
let outputFormat: String
let preset: TranscodePreset?
}
struct TranscodeInput: Encodable {
let url: String
}
struct TranscodePreset: Encodable {
let quality: String
let resolution: String
}
struct TranscodeJob: Decodable {
let id: String
let status: String
}
struct DownloadResponse: Decodable {
let url: String
}
func transcodeVideo(inputURL: String, apiKey: String) async throws -> URL {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let baseURL = "https://api.ffmpeg-micro.com/v1/transcodes"
// 1. Create job
let body = TranscodeRequest(
inputs: [TranscodeInput(url: inputURL)],
outputFormat: "mp4",
preset: TranscodePreset(quality: "high", resolution: "1080p")
)
var createRequest = URLRequest(url: URL(string: baseURL)!)
createRequest.httpMethod = "POST"
createRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
createRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
createRequest.httpBody = try encoder.encode(body)
let (createData, _) = try await URLSession.shared.data(for: createRequest)
let job = try decoder.decode(TranscodeJob.self, from: createData)
// 2. Poll until complete
var status = job.status
while status == "queued" || status == "processing" {
try await Task.sleep(for: .seconds(2))
var pollRequest = URLRequest(url: URL(string: "\(baseURL)/\(job.id)")!)
pollRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (pollData, _) = try await URLSession.shared.data(for: pollRequest)
status = try decoder.decode(TranscodeJob.self, from: pollData).status
}
// 3. Get download URL
var downloadRequest = URLRequest(url: URL(string: "\(baseURL)/\(job.id)/download")!)
downloadRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (downloadData, _) = try await URLSession.shared.data(for: downloadRequest)
let signedURL = try decoder.decode(DownloadResponse.self, from: downloadData).url
return URL(string: signedURL)!
}
Call it from any async context:
let signedURL = try await transcodeVideo(
inputURL: "https://example.com/input.mp4",
apiKey: ProcessInfo.processInfo.environment["FFMPEG_MICRO_API_KEY"]!
)
print("Download from: \(signedURL)")
This pattern works in Vapor route handlers, Swift command-line tools, and SwiftUI apps inside a Task block.
CLI vs. API: Side-by-Side
Same operation (converting MP4 to 720p WebM) done both ways:
FFmpeg CLI (requires local install):
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 -vf scale=-2:720 output.webm
FFmpeg Micro API (no install):
let body: [String: Any] = [
"inputs": [["url": "https://example.com/input.mp4"]],
"outputFormat": "webm",
"options": [
["option": "-c:v", "argument": "libvpx-vp9"],
["option": "-crf", "argument": "30"],
["option": "-b:v", "argument": "0"],
["option": "-vf", "argument": "scale=-2:720"]
]
]
The CLI version needs FFmpeg installed. The API version runs from any Swift app with URLSession. For more on the tradeoffs between local FFmpeg and API-based processing, see our getting started guide.
Common Pitfalls
Process isn't available on iOS. Foundation's Process class only exists on macOS and Linux. If you're building an iOS app that needs video processing, you either need FFmpegKit (which bundles FFmpeg as an xcframework, adding 20-50MB to your app) or a cloud API. There's no way to shell out to FFmpeg on iOS.
FFmpeg path differs on Apple Silicon vs Intel. Homebrew installs to /opt/homebrew/bin/ffmpeg on Apple Silicon Macs and /usr/local/bin/ffmpeg on Intel Macs. Hard-coding either path breaks on the other architecture. Use /usr/bin/env to resolve it at runtime, or pass the path as configuration.
URLSession async methods need an async context. The async/await URLSession methods can't be called from synchronous code. In a command-line tool, mark your entry point with @main and declare static func main() async throws. In Vapor, route handlers are already async. In SwiftUI, wrap calls in a Task {} block.
Large file uploads need the presigned flow. For files over 100MB, don't try to pass a URL that requires authentication. Use FFmpeg Micro's three-step upload: POST to /v1/upload/presigned-url to get a GCS upload URL, PUT the file directly to cloud storage, then POST to /v1/upload/confirm before referencing the file in your transcode request.
FAQ
Can I run FFmpeg directly on iOS with Swift?
Not through Process, which doesn't exist on iOS. FFmpegKit is the most common option. It compiles FFmpeg into an xcframework you link into your Xcode project. But it adds significant binary size, and you're responsible for updating it when new codec vulnerabilities ship. A cloud API sidesteps all of that.
Does Swift Package Manager have an FFmpeg package?
There's no official Swift package for FFmpeg. FFmpegKit distributes through CocoaPods, Swift Package Manager, and manual xcframework integration. For server-side Swift with Vapor, most teams either install FFmpeg on the host and use Process, or call a cloud API like FFmpeg Micro.
How do I handle errors from the FFmpeg Micro API in Swift?
Check the HTTP response status code. A 400 means invalid parameters (wrong output format, bad options). A 401 means your API key is missing or expired. A 402 means you've exceeded your plan limits. The response body always contains an error message you can decode with JSONDecoder.
Is FFmpeg Micro fast enough for user-facing uploads?
Jobs run asynchronously. Short videos under 60 seconds typically finish in a few seconds. For user-facing features, kick off the transcode and poll for completion, or set up a webhook to notify your backend when it's done. It's not designed for real-time streaming.
What about Vapor? Does this work in server-side Swift?
Yes. URLSession works in Vapor, and all the async/await code in this post runs in Vapor route handlers without modification. You can also use Vapor's built-in HTTP client through the AsyncHTTPClient package if you want to keep your dependency graph tighter.
FFmpeg Micro gives you full FFmpeg without managing binaries, containers, or codec updates. Sign up for a free API key and try a transcode in under five minutes.
Last verified: July 2026. Code examples tested against Swift 5.9+, macOS 14+, and FFmpeg Micro API v1.
United States
NORTH AMERICA
Related News

Master Local Fine-Tuning with "gemma-trainer"
8h ago
Building a Plugin-Free Newsletter Popup on WordPress: Custom REST Endpoint Mailchimp API v3
8h ago
ภาษาโปรแกรมมิ่งที่ syntax ง่าย ทำให้ AI หลอนน้อยลง จริงหรือ?
8h ago
How I Built a File-Timestamp-Based Feedback Loop to Enforce AI Output Quality
8h ago
GitHub Trending Digest — 2026-07-07
9h ago