AVFoundation is Apple's framework for all audio and video work. It handles MP4-to-audio extraction natively — no FFmpeg, no shell processes, no extra dependencies. The main gotcha: AVFoundation exports M4A (AAC in MPEG-4 container) natively but not raw MP3. For true MP3 output, the ChangeThisFile API or a process call to FFmpeg is required. This guide covers all three paths clearly.

Method 1: AVFoundation AVAssetExportSession (native, outputs M4A/AAC)

AVFoundation exports AAC audio in an M4A container natively. M4A is smaller than MP3 at equivalent quality and plays in all Apple devices, modern browsers, and most media players.

import AVFoundation
import Foundation

func mp4ToM4A(inputURL: URL, outputURL: URL) async throws {
    let asset = AVAsset(url: inputURL)

    // Verify asset has an audio track
    let audioTracks = try await asset.loadTracks(withMediaType: .audio)
    guard !audioTracks.isEmpty else {
        throw ExportError.noAudioTrack
    }

    guard let exportSession = AVAssetExportSession(
        asset: asset,
        presetName: AVAssetExportPresetAppleM4A
    ) else {
        throw ExportError.sessionCreationFailed
    }

    // Remove existing output file
    try? FileManager.default.removeItem(at: outputURL)

    exportSession.outputURL = outputURL
    exportSession.outputFileType = .m4a

    try await exportSession.export(to: outputURL, as: .m4a)

    switch exportSession.status {
    case .completed:
        print("Saved: \(outputURL.path)")
    case .failed:
        throw exportSession.error ?? ExportError.unknown
    case .cancelled:
        throw ExportError.cancelled
    default:
        throw ExportError.unknown
    }
}

enum ExportError: Error {
    case noAudioTrack
    case sessionCreationFailed
    case cancelled
    case unknown
}

// Usage
Task {
    let input  = URL(fileURLWithPath: "/tmp/video.mp4")
    let output = URL(fileURLWithPath: "/tmp/audio.m4a")
    try await mp4ToM4A(inputURL: input, outputURL: output)
}

Why M4A and not MP3? Apple deprecated MP3 encoding in AVFoundation — AVFileTypeMPEGLayer3 was removed from export presets in recent macOS/iOS versions. M4A (AAC) is the Apple-native output format and is universally compatible. If you specifically need MP3, see Method 2 or 3.

Method 2: FFmpeg via Process (true MP3 output on macOS)

For actual MP3 output on macOS, call FFmpeg via Swift's Process API. Requires FFmpeg installed (brew install ffmpeg).

import Foundation

func mp4ToMp3FFmpeg(
    inputPath: String,
    outputPath: String,
    bitrate: String = "192k"
) throws -> Int32 {
    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/usr/local/bin/ffmpeg")
    process.arguments = [
        "-i", inputPath,
        "-vn",
        "-acodec", "libmp3lame",
        "-ab", bitrate,
        "-y",
        outputPath
    ]

    let pipe = Pipe()
    process.standardError = pipe
    process.standardOutput = pipe

    try process.run()
    process.waitUntilExit()

    let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
    if process.terminationStatus != 0 {
        fputs("FFmpeg error:\n\(output)\n", stderr)
    } else {
        print("Saved: \(outputPath)")
    }
    return process.terminationStatus
}

// Usage
try mp4ToMp3FFmpeg(inputPath: "/tmp/video.mp4", outputPath: "/tmp/audio.mp3", bitrate: "192k")

FFmpeg location varies: /usr/local/bin/ffmpeg on Intel Macs, /opt/homebrew/bin/ffmpeg on Apple Silicon. Use which ffmpeg or make the path configurable. On iOS, Process is not available — use Method 1 (M4A) or Method 3 (API) instead.

Method 3: ChangeThisFile API via URLSession (MP3 on any platform)

True MP3 output, no AVFoundation restrictions, no FFmpeg install. Works on macOS, iOS, Linux Swift, and CI runners. Free tier: 1,000 conversions/month.

# curl reference
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key_here" \
  -F "file=@video.mp4" \
  -F "target=mp3" \
  --output audio.mp3
import Foundation

func mp4ToMp3API(
    inputURL: URL,
    outputURL: URL,
    apiKey: String
) async throws {
    let endpoint = URL(string: "https://changethisfile.com/v1/convert")!
    let boundary = UUID().uuidString
    let fileData = try Data(contentsOf: inputURL)

    var request = URLRequest(url: endpoint)
    request.httpMethod = "POST"
    request.timeoutInterval = 300  // 5 min for long videos
    request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    var body = Data()
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(inputURL.lastPathComponent)\"\r\n".data(using: .utf8)!)
    body.append("Content-Type: video/mp4\r\n\r\n".data(using: .utf8)!)
    body.append(fileData)
    body.append("\r\n".data(using: .utf8)!)
    body.append("--\(boundary)\r\n".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name=\"target\"\r\n\r\n".data(using: .utf8)!)
    body.append("mp3\r\n".data(using: .utf8)!)
    body.append("--\(boundary)--\r\n".data(using: .utf8)!)
    request.httpBody = body

    let (data, response) = try await URLSession.shared.data(for: request)
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    try data.write(to: outputURL)
    print("Saved: \(outputURL.path)")
}

// Usage
Task {
    try await mp4ToMp3API(
        inputURL: URL(fileURLWithPath: "/tmp/video.mp4"),
        outputURL: URL(fileURLWithPath: "/tmp/audio.mp3"),
        apiKey: "ctf_sk_your_key_here"
    )
}

When to use each

ApproachBest forTradeoff
AVFoundation (M4A)iOS apps, macOS, no system deps, AAC output is fineOutputs M4A (AAC), not MP3 — MP3 encoding removed
Process + FFmpegmacOS CLI tools, scripts, true MP3 outputRequires FFmpeg install; not available on iOS
ChangeThisFile API (URLSession)True MP3, iOS-compatible, no FFmpeg installNetwork latency, 25MB free-tier upload limit

Production tips

  • M4A vs MP3: prefer M4A unless the receiver explicitly requires MP3. AAC (M4A) has better quality-per-bit than MP3 and plays in all modern players. MP3 is the compatibility format for legacy systems.
  • Check for audio tracks before exporting. Not all MP4 files contain audio (screen recordings, dashcam footage). AVAsset.loadTracks(withMediaType: .audio) returning empty should surface a clear error, not a silent zero-byte output.
  • Handle the export on a background Task. AVAssetExportSession.export() is async but can be slow for large files. Always run it off the main actor to avoid blocking UI.
  • FFmpeg path differs on Apple Silicon. Homebrew installs to /opt/homebrew on Apple Silicon, /usr/local on Intel. Either use the output of which ffmpeg at runtime or make the path an environment variable.
  • Increase URLSession timeout for large files. The default URLSession timeout is 60 seconds. A 500MB MP4 upload will exceed this. Set request.timeoutInterval = 300 (5 minutes) or create a custom URLSessionConfiguration.

If M4A is acceptable (and it usually is), AVFoundation is the cleanest native path. For MP3 specifically on macOS, FFmpeg via Process. For iOS or zero-dependency pipelines, the API. Free tier covers 1,000 conversions/month.