Audio unter iOS & macOS mit FFmpeg extrahieren
Das Extrahieren einer Tonspur ist für Sprachnotizen, Transkription und Audiobearbeitung nützlich.
Diese Anleitung nutzt FFmpeg mit Swift unter macOS und AVFoundation für dieselbe Aufgabe in
Apple-Apps, einschließlich iOS. Beide Beispiele schreiben eine reine Audiodatei im Format
.m4a und lassen eine bereits vorhandene Zieldatei unverändert.
Einführung in FFmpeg auf Apple-Plattformen
FFmpeg kann einen kompatiblen Audiostream kopieren, ohne ihn erneut zu codieren. Das macOS-Beispiel
startet eine lokale ausführbare FFmpeg-Datei über die API Process von Foundation.
Process ist unter iOS nicht verfügbar. Das AVFoundation-Beispiel nutzt das
integrierte Medienframework von Apple und exportiert stattdessen AAC-Audio.
Audioextraktion auf Apple-Plattformen einrichten
FFmpegKit wurde eingestellt. Das ursprüngliche Repository ist kein installierbares Swift-Paket. Die früheren Anweisungen zum Hinzufügen über den Swift Package Manager funktionieren daher nicht. Die Mitteilung des Autors zur Einstellung und seine Informationen zur Fortführung beschreiben die Änderungen an der Bereitstellung. Diese Anleitung ist nicht mehr auf diese Binärdateien angewiesen.
FFmpeg und Swift vorbereiten
Für diese Beispiele benötigen Sie kein Swift-Paket von Drittanbietern. Installieren Sie für macOS eine funktionsfähige ausführbare FFmpeg-Datei aus einer Distribution, die auf der FFmpeg-Downloadseite aufgeführt ist. Prüfen Sie ihre Version und übergeben Sie ihren absoluten Pfad an das Beispiel:
ffmpeg -version
command -v ffmpeg
Der folgende Swift-Code ist für macOS 13 oder iOS
16 und neuere Versionen ausgelegt. Für die Ausführung über die
macOS-Befehlszeile benötigen Sie Apple Command Line Tools mit Swift 5.9
oder neuer. Zum Erstellen der iOS-App benötigen Sie die vollständige Xcode-Installation und ein
iOS-SDK. Die native Überprüfung für diese Anleitung deckt macOS ab. Builds für iOS-Geräte, der
Zugriff über die Dateiauswahl und das Verhalten im App-Lebenszyklus müssen noch in Ihrer App
validiert werden.
Audiostreams aus Videodateien extrahieren
Die FFmpeg-Variante wählt den ersten Audiostream mit -map 0:a:0 aus.
Das Kopieren eines Streams in M4A erfordert einen kompatiblen Codec wie AAC. Die AVFoundation-Variante
wählt die erste Audiospur aus und exportiert sie mit AVAssetExportPresetAppleM4A.
Dabei kann das Audio erneut codiert werden; es handelt sich nicht um eine bytegenaue Kopie.
Schritt-für-Schritt-Anleitung zur Audioextraktion
Speichern Sie diesen Code als AudioExtraction.swift. Jeder Vorgang schreibt in ein neues
Verzeichnis neben der Zieldatei, stellt das Ergebnis erst nach erfolgreichem Abschluss bereit und
entfernt bei einem Fehler seine temporären Dateien. Übergeben Sie lokale Datei-URLs, die Ihre App
lesen kann, und eine Ausgabe-URL in einem vorhandenen, beschreibbaren Verzeichnis.
import AVFoundation
import Foundation
enum AudioExtractionError: Error {
case invalidPaths, outputExists, noAudio, exportUnavailable, exportFailed
}
private func extractionDirectory(inputURL: URL, outputURL: URL) throws -> URL {
let files = FileManager.default
guard inputURL.isFileURL, outputURL.isFileURL,
files.isReadableFile(atPath: inputURL.path),
outputURL.pathExtension.lowercased() == "m4a" else {
throw AudioExtractionError.invalidPaths
}
guard !files.fileExists(atPath: outputURL.path) else {
throw AudioExtractionError.outputExists
}
let directory = outputURL.deletingLastPathComponent()
.appendingPathComponent("audio-\(UUID().uuidString)", isDirectory: true)
try files.createDirectory(at: directory, withIntermediateDirectories: false)
return directory
}
#if os(macOS)
// This blocking function belongs on a worker queue when called from a macOS UI.
func extractAudioWithFFmpeg(from videoURL: URL, to outputURL: URL, ffmpegURL: URL) throws {
let directory = try extractionDirectory(inputURL: videoURL, outputURL: outputURL)
defer { try? FileManager.default.removeItem(at: directory) }
let temporaryOutput = directory.appendingPathComponent("audio.m4a")
let process = Process()
process.executableURL = ffmpegURL
process.environment = [:]
process.arguments = [
"-nostdin", "-hide_banner", "-loglevel", "error", "-n",
"-protocol_whitelist", "file,pipe", "-i", videoURL.path,
"-map", "0:a:0", "-vn", "-c:a", "copy", "-f", "ipod", temporaryOutput.path
]
// In this command-line example, diagnostics go to stderr without a pipe that could fill up.
process.standardError = FileHandle.standardError
try process.run()
process.waitUntilExit()
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
throw AudioExtractionError.exportFailed
}
try FileManager.default.moveItem(at: temporaryOutput, to: outputURL)
}
#endif
func extractAudioWithAVFoundation(from videoURL: URL, to outputURL: URL) async throws {
let directory = try extractionDirectory(inputURL: videoURL, outputURL: outputURL)
defer { try? FileManager.default.removeItem(at: directory) }
let temporaryOutput = directory.appendingPathComponent("audio.m4a")
let asset = AVURLAsset(url: videoURL)
guard let sourceTrack = try await asset.loadTracks(withMediaType: .audio).first else {
throw AudioExtractionError.noAudio
}
let composition = AVMutableComposition()
guard let track = composition.addMutableTrack(
withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid
) else {
throw AudioExtractionError.exportUnavailable
}
let timeRange = try await sourceTrack.load(.timeRange)
try track.insertTimeRange(timeRange, of: sourceTrack, at: .zero)
guard let session = AVAssetExportSession(
asset: composition, presetName: AVAssetExportPresetAppleM4A
), session.supportedFileTypes.contains(.m4a) else {
throw AudioExtractionError.exportUnavailable
}
session.outputURL = temporaryOutput
session.outputFileType = .m4a
await session.export()
guard session.status == .completed else {
throw AudioExtractionError.exportFailed
}
try FileManager.default.moveItem(at: temporaryOutput, to: outputURL)
}
Anwendungsbeispiel
Speichern Sie für ein macOS-Befehlszeilenprogramm diesen Einstiegspunkt als
ExtractAudio.swift neben der vorherigen Datei. Das Programm wartet auf den Abschluss der
Extraktion und gibt bei einem Fehler einen Exit-Status ungleich null zurück.
import Foundation
@main
struct ExtractAudio {
static func main() async {
let arguments = CommandLine.arguments
guard arguments.count == 4 || arguments.count == 5 else {
FileHandle.standardError.write(Data(
"Usage: extract-audio avfoundation|ffmpeg input output.m4a [ffmpeg-path]\n".utf8
))
exit(1)
}
do {
let input = URL(fileURLWithPath: arguments[2])
let output = URL(fileURLWithPath: arguments[3])
switch arguments[1] {
case "ffmpeg" where arguments.count == 5:
try extractAudioWithFFmpeg(
from: input, to: output, ffmpegURL: URL(fileURLWithPath: arguments[4])
)
case "avfoundation" where arguments.count == 4:
try await extractAudioWithAVFoundation(from: input, to: output)
default:
throw AudioExtractionError.invalidPaths
}
print("Audio extraction successful: \(output.path)")
} catch {
FileHandle.standardError.write(Data("Audio extraction failed.\n".utf8))
exit(1)
}
}
}
swiftc -parse-as-library AudioExtraction.swift ExtractAudio.swift -o extract-audio
./extract-audio ffmpeg "sample video.mp4" copied.m4a "$(command -v ffmpeg)"
./extract-audio avfoundation "sample video.mp4" encoded.m4a
Fügen Sie in einer iOS-App nur AudioExtraction.swift hinzu und warten Sie in Ihrem
vorhandenen Task oder ViewModel auf extractAudioWithAVFoundation. Halten Sie jeden
Security-Scoped-Dateizugriff offen, bis der Export abgeschlossen ist. Dieses Beispiel verarbeitet eine
Datei, die der Nutzer bereits ausgewählt hat; es nimmt kein Audio über ein Mikrofon auf.
Häufige Probleme beheben
Hier finden Sie häufige Probleme und ihre Lösungen:
- Dateizugriff: Verwenden Sie ein app-eigenes Verzeichnis oder behalten Sie den Zugriff bei, den die Systemdateiauswahl gewährt hat. macOS-Apps in einer Sandbox benötigen außerdem eine passend gebündelte ausführbare Datei und eine entsprechende App-Sandbox-Konfiguration.
- Vorhandene Ausgabedatei: Wählen Sie ein neues Ziel. Die Beispiele ersetzen niemals eine vorhandene Datei.
- Keine Audiospur: Ein Video ohne Ton enthält nichts, was extrahiert werden könnte, und führt zu einem Fehler.
- Nicht unterstützte Codecs: Das Kopieren von Streams mit FFmpeg kann fehlschlagen, wenn der Audiocodec nicht in M4A gespeichert werden kann. Codieren Sie stattdessen erneut mit AAC oder nutzen Sie die AVFoundation-Variante für Eingaben, die das Framework von Apple unterstützt.
- Diagnose: Prüfen Sie die stderr-Ausgabe von FFmpeg lokal. Prüfen Sie beim Debuggen von
AVFoundation
session.erroran der Stelle der Fehlerprüfung, ohne interne Fehler in der Benutzeroberfläche Ihrer App anzuzeigen.
Leistung optimieren
So optimieren Sie die Leistung:
- Kopieren Sie kompatibles Audio mit
-c:a copy, wenn Sie den codierten Stream erhalten müssen. - Warten Sie nicht blockierend im Hauptthread einer macOS-App auf FFmpeg-Prozesse.
- Warten Sie asynchron auf AVFoundation-Exporte, ohne das gesamte Video in den Arbeitsspeicher zu laden. Eine Beschleunigung des Decodierens von Video bringt keinen Nutzen bei einem Vorgang, der nur den Audiostream kopiert.
Weitere Ressourcen
Weitere Informationen zur Auswahl von Streams und zur Konfiguration von Exporten finden Sie in der FFmpeg-Dokumentation zur Streamauswahl und in Apples Leitfaden zum Medienexport.
Bei Transloadit verwenden wir FFmpeg in unseren Diensten für Audio-Encoding und Video-Encoding. Damit betreiben wir Robots wie /audio/encode und /video/encode.
