Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 31 additions & 22 deletions ios/Classes/src/features/audio/ExtractAudio.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class NoAudioTrackException: NSError, @unchecked Sendable {
userInfo: [NSLocalizedDescriptionKey: "No audio track found in video"]
)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Expand All @@ -26,7 +26,7 @@ class NoAudioTrackException: NSError, @unchecked Sendable {
/// - Provides progress tracking during extraction
/// - Supports cancellation of active extraction jobs
class ExtractAudio {

/// Extracts audio from a video file asynchronously.
///
/// This method uses AVAssetExportSession for fast Passthrough export,
Expand All @@ -44,7 +44,7 @@ class ExtractAudio {
onComplete: @escaping (FlutterStandardTypedData?) -> Void,
onError: @escaping (Error) -> Void
) -> AudioExtractJobHandle {

// Check if WAV format is requested - requires transcoding
let outputExtension = config.getOutputExtension().lowercased()
if outputExtension == "wav" {
Expand All @@ -55,7 +55,7 @@ class ExtractAudio {
onError: onError
)
}

// Use passthrough export for other formats
return extractPassthrough(
config: config,
Expand All @@ -64,34 +64,34 @@ class ExtractAudio {
onError: onError
)
}

/// Extracts audio using passthrough (no transcoding) for M4A, AAC, CAF formats.
private static func extractPassthrough(
config: AudioExtractConfig,
onProgress: @escaping (Double) -> Void,
onComplete: @escaping (FlutterStandardTypedData?) -> Void,
onError: @escaping (Error) -> Void
) -> AudioExtractJobHandle {

var exportSession: AVAssetExportSession?
var progressTimer: Timer?
var isCancelled = false

// Execute extraction on background queue
DispatchQueue.global(qos: .userInitiated).async {
do {
// Load source video asset
let sourceURL = URL(fileURLWithPath: config.inputPath)
let asset = AVURLAsset(url: sourceURL)

// Wait for tracks to be loaded
let loadSemaphore = DispatchSemaphore(value: 0)
var loadError: Error?

asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"]) {
let tracksStatus = asset.statusOfValue(forKey: "tracks", error: nil)
let durationStatus = asset.statusOfValue(forKey: "duration", error: nil)

if tracksStatus == .failed || durationStatus == .failed {
loadError = NSError(
domain: "ExtractAudio",
Expand All @@ -101,13 +101,13 @@ class ExtractAudio {
}
loadSemaphore.signal()
}

loadSemaphore.wait()

if let error = loadError {
throw error
}

// Determine output file location
let outputURL: URL
if let outputPath = config.outputPath {
Expand All @@ -117,14 +117,14 @@ class ExtractAudio {
let filename = "audio_\(Date().timeIntervalSince1970).\(config.getOutputExtension())"
outputURL = tempDir.appendingPathComponent(filename)
}

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

// Determine output file type based on extension
let fileExtension = outputURL.pathExtension.lowercased()
let outputFileType: AVFileType

switch fileExtension {
case "m4a":
outputFileType = .m4a
Expand Down Expand Up @@ -198,7 +198,7 @@ class ExtractAudio {
userInfo: [NSLocalizedDescriptionKey: "Failed to create export session"]
)
}

exportSession = session
session.outputURL = outputURL
session.outputFileType = outputFileType
Expand Down Expand Up @@ -330,7 +330,8 @@ class ExtractAudio {
var assetReader: AVAssetReader?
var assetWriter: AVAssetWriter?
var isCancelled = false

var sessionStarted = false

DispatchQueue.global(qos: .userInitiated).async {
do {
// Load source video asset
Expand Down Expand Up @@ -410,7 +411,7 @@ class ExtractAudio {
AVLinearPCMBitDepthKey: 16,
AVLinearPCMIsFloatKey: false,
AVLinearPCMIsBigEndianKey: false,
AVLinearPCMIsNonInterleaved: false
AVLinearPCMIsNonInterleaved: false,
]

let readerOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: readerOutputSettings)
Expand Down Expand Up @@ -499,9 +500,7 @@ class ExtractAudio {
userInfo: [NSLocalizedDescriptionKey: "Failed to start writing"]
)
}

writer.startSession(atSourceTime: .zero)


// Calculate total duration for progress
let totalDuration = CMTimeGetSeconds(timeRange.duration)

Expand All @@ -517,6 +516,11 @@ class ExtractAudio {
writerInput.requestMediaDataWhenReady(on: processingQueue) {
while writerInput.isReadyForMoreMediaData && !isCancelled {
if let sampleBuffer = readerOutput.copyNextSampleBuffer() {
if !sessionStarted {
writer.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
sessionStarted = true
}

// Update progress
let currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let elapsed = CMTimeGetSeconds(currentTime) - CMTimeGetSeconds(timeRange.start)
Expand All @@ -531,6 +535,11 @@ class ExtractAudio {
}
} else {
// No more samples
if !sessionStarted && !isCancelled {
// Fallback session start if no samples were found
writer.startSession(atSourceTime: timeRange.start)
sessionStarted = true
}
writerInput.markAsFinished()
break
}
Expand Down
16 changes: 13 additions & 3 deletions macos/Classes/src/features/audio/ExtractAudio.swift
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ class ExtractAudio {
var assetReader: AVAssetReader?
var assetWriter: AVAssetWriter?
var isCancelled = false
var sessionStarted = false

DispatchQueue.global(qos: .userInitiated).async {
do {
Expand Down Expand Up @@ -398,6 +399,7 @@ class ExtractAudio {
}

// Calculate time range
// Use the audio track's actual timeRange for full extraction
// Audio tracks may not start at zero due to encoding delays or sync adjustments
var timeRange: CMTimeRange
if let startUs = config.startUs, let endUs = config.endUs {
Expand Down Expand Up @@ -522,9 +524,7 @@ class ExtractAudio {
userInfo: [NSLocalizedDescriptionKey: "Failed to start writing"]
)
}

writer.startSession(atSourceTime: .zero)


// Calculate total duration for progress
let totalDuration = CMTimeGetSeconds(timeRange.duration)

Expand All @@ -540,6 +540,11 @@ class ExtractAudio {
writerInput.requestMediaDataWhenReady(on: processingQueue) {
while writerInput.isReadyForMoreMediaData && !isCancelled {
if let sampleBuffer = readerOutput.copyNextSampleBuffer() {
if !sessionStarted {
writer.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
sessionStarted = true
}

// Update progress
let currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let elapsed =
Expand All @@ -555,6 +560,11 @@ class ExtractAudio {
}
} else {
// No more samples
if !sessionStarted && !isCancelled {
// Fallback session start if no samples were found
writer.startSession(atSourceTime: timeRange.start)
sessionStarted = true
}
writerInput.markAsFinished()
break
}
Expand Down
Loading