AVAssetExportSession progress not updating - swift

I make use of AVAssetExportSession to export movies to MP4 format using the codes below:
Step 1: User clicks a button to start conversion
#IBAction func clickConvert(_ sender:UIButton) {
self.convertProgress?.progress = 0
self.convertProgress?.isHidden = false
var preset = AVAssetExportPresetHighestQuality
switch self.qualitySelection?.selectedSegmentIndex {
case 0:
preset = AVAssetExportPresetLowQuality
break
case 1:
preset = AVAssetExportPresetMediumQuality
break
case 2:
preset = AVAssetExportPresetHighestQuality
break
default:
break
}
DispatchQueue.global(qos: .background).async {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMddHHmmss"
let fileName = formatter.string(from: Date()) + ".mp4"
let convertGroup = DispatchGroup()
convertGroup.enter()
do {
let documentDirectory = try self.fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let filePath = documentDirectory.appendingPathComponent(fileName)
if(self.videoURL != nil) {
self.convertVideo(fromURL: self.videoURL!, toURL: filePath, preset: preset, dispatchGroup: convertGroup)
} else {
print("nil Video URL")
}
convertGroup.notify(queue: DispatchQueue.main) {
// reset Convert button state
self.convertButton?.titleLabel?.text = "Convert"
self.convertButton?.isEnabled = true
self.delegate?.updateFileList()
// Take back to old VC, update file list
if let navController = self.navigationController {
navController.popViewController(animated: true)
}
}
} catch {
print(error)
}
}
}
Step 2: Trigger convert video function
func convertVideo(fromURL: URL, toURL: URL, preset:String, dispatchGroup: DispatchGroup) {
let outFileType = AVFileType.mp4
let inAsset = AVAsset(url: fromURL)
let startDate = Date()
AVAssetExportSession.determineCompatibility(ofExportPreset: preset, with: inAsset, outputFileType: outFileType, completionHandler: { (isCompitable) in
if !isCompitable {
return
}
guard let export = AVAssetExportSession(asset: inAsset, presetName: preset) else {
return
}
export.outputFileType = outFileType
export.outputURL = toURL
export.shouldOptimizeForNetworkUse = true
let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0)
let range = CMTimeRangeMake(start: start, duration: inAsset.duration)
export.timeRange = range
// Timer for progress updates
self.exportTimer = Timer()
if #available(iOS 10.0, *) {
print("start exportTimer")
self.exportTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { _ in
let progress = Float(export.progress)
print("Export Progress: \(progress)")
self.updateProgressDisplay(progress: progress)
if progress < 0.99 {
let dict:[String: Float] = ["progress": progress]
NotificationCenter.default.post(name: Notification.Name(Constants.Notifications.ConvertProgress.rawValue), object: nil, userInfo: dict)
}
})
}
export.exportAsynchronously { () -> Void in
// Handle export results
switch export.status {
case .exporting:
print("Exporting...")
self.updateProgressDisplay(progress: export.progress)
break
case .failed:
print("Error: %#!", export.error!)
break
case .cancelled:
print("export cancelled")
break
case .completed:
let endDate = Date()
let elapsed = endDate.timeIntervalSince(startDate)
print("Elapsed: \(elapsed)")
print("successful")
self.exportTimer?.invalidate() // Stop the timer
self.generateThumbnail(path: toURL)
break
default:
break
}
dispatchGroup.leave()
}
})
}
However, status update is not working, as the timer exportTimer never fires (attempt 1), and the exportSession.exporting case never fires (attempt 2).
p.s. The video can be converted without any problem
p.s. the Notification has been added in viewDidLoad() as follow:
NotificationCenter.default.addObserver(self, selector: #selector(onDidReceiveConvertProgress(_:)), name: Notification.Name(Constants.Notifications.ConvertProgress.rawValue), object: nil)
Status update functions (both attempts) are as follow:
#objc func onDidReceiveConvertProgress(_ notification:Notification) {
print ("onDidReceiveConvertProgress")
if let data = notification.userInfo as? [String:Float] {
print("Progress: \(String(describing: data["progress"]))")
self.convertProgress?.progress = data["progress"]!
}
}
func updateProgressDisplay(progress: Float) {
print("updateProgressDisplay")
self.convertProgress?.progress = progress
}
What did I miss?

I'm not sure if you figured this out, but just in case someone else try your code, the problem why the progress timer is not firing is because you missed two things.
You never called the function to start the timer. e.g. self.exportTimer.fire()
You have to make sure to update this timer on the main Queue.
I had the same problems and doing these two things fixed my issue.

Related

Mapbox migrating to v10.1 offlineManager.loadStylePack does not complete or return any progress

Following migration example provided but loadStylePack never completes or returns the progress. At the same time, TileStore.default.loadTileRegion from the same example works and returns progress just fine. Has anyone run into the same problem any suggestions on what to try? Would have no errors no log messages to go on...
Below is the code used.
guard let stylePackLoadOptions = StylePackLoadOptions(glyphsRasterizationMode: .allGlyphsRasterizedLocally, metadata: ["id": self.id], acceptExpired: false) else {
return
}
var url: StyleURI = .satellite
if let style = self.mapbox_style, let custom = URL(string: style) {
url = StyleURI(url: custom) ?? .satellite
}
ResourceOptionsManager.default.resourceOptions.tileStore = TileStore.default
let offlineManager = OfflineManager(resourceOptions: ResourceOptionsManager.default.resourceOptions)
let group = DispatchGroup()
group.enter()
offlineManager.removeStylePack(for: url)
self.stylePackCancelable = offlineManager.loadStylePack(for: url, loadOptions: stylePackLoadOptions) { progress in
print("Style Progress: size: \(progress.completedResourceSize) completed: \(progress.completedResourceCount) total: \(progress.requiredResourceCount)")
} completion: { result in
group.leave()
switch result {
case let .success(stylePack):
// Style pack download finishes successfully
print("Process Style COMPLETED \(stylePack.debugDescription)")
case let .failure(error):
let statusMessage = "Failed to load map.".localized
// Handle error occurred during the style pack download
if case StylePackError.canceled = error {
//handleCancelation()
} else {
self.statusDelegate?.updateStatus(SyncStatus(message: statusMessage, progress: 0, date: nil, showProgress: true, criticalFailure:true, error:error))
}
resultsLoaded(.local, [])
}
}
guard let coordinates = self.bounds?.map ({$0.value}) else {
self.statusDelegate?.updateStatus(SyncStatus(message: "Bounds are not set.", progress: 0, date: nil, showProgress: true, criticalFailure:true, error:nil))
resultsLoaded(.local, [])
return
}
let bounds = MultiPoint(coordinates)
group.enter()
let options = TilesetDescriptorOptions(styleURI: url, zoomRange: 0...16)
let tilesetDescriptor = offlineManager.createTilesetDescriptor(for: options)
if let tileRegionLoadOptions = TileRegionLoadOptions( geometry: Geometry(bounds), descriptors: [tilesetDescriptor], acceptExpired: true) {
self.tileRegionCancelable = TileStore.default.loadTileRegion(forId: self.id, loadOptions: tileRegionLoadOptions) { progress in
print("Tile Progress: size: \(progress.completedResourceSize) completed: \(progress.completedResourceCount) total: \(progress.requiredResourceCount)")
} completion: { result in
group.leave()
switch result {
case let .success(tileRegion):
// Tile region download finishes successfully
print("Process \(tileRegion.debugDescription)")
case let .failure(error):
// Handle error occurred during the tile region download
if case TileRegionError.canceled = error {
//handleCancelation()
} else {
//handleFailure(error)
self.statusDelegate?.updateStatus(SyncStatus(message: statusMessage, progress: 0, date: nil, showProgress: true, criticalFailure:true, error:error))
}
}
}
}
group.wait()
You should start by calling loadTileRegion and on completion/success call loadStylePack. Then you will get progress from loadStylePack.

Trim video always fail when use AVAssetExportPresetPassthrough

My trimming video code:
func trim(createNew: Bool, with name: String?, file: File, startTime: Double, endTime: Double, completion: #escaping ResultCompletion<File>) {
guard checkFreeSpace(with: file) else {
return handleFailure(error: .fullStorage, completion: completion)
}
guard let originUrl = file.localUrl(),
let originName = file.name.components(separatedBy: ".").first,
let originExtension = file.name.components(separatedBy: ".").last else {
return handleFailure(error: .mediaSavingError, completion: completion)
}
let asset = AVAsset(url: originUrl)
let timeZoneOffset = TimeInterval(TimeZone.current.secondsFromGMT())
let newDate = Date().addingTimeInterval(timeZoneOffset)
let temporaryFileId = "\(file.device ?? "Unknown")-\(newDate.milliseconds)-0-\(originName)_trim.\(originExtension)"
let outputUrl = file.buildLocalUrl(id: temporaryFileId)
AVAssetExportSession.determineCompatibility(
ofExportPreset: AVAssetExportPresetPassthrough,
with: asset,
outputFileType: .mp4) { isCompatible in
print("Can Trim MP4: ", isCompatible)
}
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else {
return handleFailure(error: .mediaSavingError, completion: completion)
}
exportSession.outputURL = outputUrl
exportSession.outputFileType = .mp4
let timeRange = CMTimeRange(start: CMTime(seconds: startTime, preferredTimescale: 900),
end: CMTime(seconds: endTime, preferredTimescale: 900))
exportSession.timeRange = timeRange
exportSession.exportAsynchronously { [weak self] in
guard let self = self else {
return
}
exportProgressTimer.invalidate()
switch exportSession.status {
case .completed:
completion(.success(file))
case .failed:
self.handleFailure(error: .mediaSavingError, completion: completion)
case .cancelled:
completion(.failure(FileError.mediaExportCanceled))
default:
break
}
}
}
determineCompatibility return true
When use another preset like that contained in AVAssetExportSession.exportPresets(compatibleWith: item.asset), all do makes awesome
Maybe someone can explain what goes wrong and how I can fixed it? i need resolution the same like at original video
Or how I can convert with AVAssetWriter and AVAssetReader ?
I wrote anwer in another question, they have the same problem, but it different way. That's how I solved the problem
https://stackoverflow.com/a/70294032/7217629

Cancelling Remaining AssetExports

I'm trying to allow a user to cancel the exporting of a series of videos, while in the middle exporting (Goal: cancel the remaining unexpected videos).
Code for button:
var cancelExportButton = UIButton()
cancelExportButton.addTarget(self, action: #selector(cancelExport(sender:)), for: .touchUpInside)
#objc func cancelExport(sender: UIButton!) {
print("export cancelled")
//cancel remaining exports code
}
for i in 0...videoURLs.count - 1 {
overlayVideo(titleImage: titleImage, captionImage: captionImage, videoURL: videoURLs[i])
}
func overlayVideo(titleImage: UIImage, captionImage: UIImage, videoURL: URL) {
//...composition and mixing code, not important to exporting
// Exporting
let number = Int.random(in: 0...99999)
let savePathUrl: URL = URL(fileURLWithPath: NSHomeDirectory() + "/Documents/\(number).mp4")
do { // delete old video
try FileManager.default.removeItem(at: savePathUrl)
} catch { print(error.localizedDescription) }
let assetExport: AVAssetExportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!
assetExport.videoComposition = layerComposition
assetExport.outputFileType = AVFileType.mov
assetExport.outputURL = savePathUrl
assetExport.shouldOptimizeForNetworkUse = true
assetExport.exportAsynchronously { () -> Void in
switch assetExport.status {
case AVAssetExportSessionStatus.completed:
print("success")
case AVAssetExportSessionStatus.failed:
print("failed \(assetExport.error?.localizedDescription ?? "error nil")")
case AVAssetExportSessionStatus.cancelled:
print("cancelled \(assetExport.error?.localizedDescription ?? "error nil")")
default:
print("complete")
}
}
}
What code (using queues or monitoring properties) would work in cancelExport to cancel all remaining function instances/unexported assets?
I've tried turning assetExport into a class variable and setting it nil after successful exports, but exports are being done at the same time so it messes up.

EventKit Unit testing - stall on main thread

I need to create multiple calendar events for Unit Test in Xcode. The call needs to be async, because of the access rights request:
eventStore.requestAccess(to: EKEntityType.event, completion: {
granted, error in
//create events
})
This is done in setUp() in a loop to create multiple events. The problem is that not all the completion blocks are done when the test starts running. I have implemented the waitForExpectations() method, to make it wait till the counter meets the requirements. But I get the following error message:
Stall on main thread
Here is the relevant code - setUp:
override func setUp()
{
super.setUp()
if let filepath = Bundle.main.path(forResource: "MasterCalendar", ofType: "csv") {
do {
let expectationCreation = expectation(description: "create events expectation")
let contents = try String(contentsOfFile: filepath)
//print(contents)
let testDataArray = importTestDataArrayFrom(file: contents)
createTestEvents(testDataArray: testDataArray)
{ (eventsArray) in
self.testEvents = eventsArray
print("finished creating events")
expectationCreation.fulfill()
}
waitForExpectations(timeout: 10.0) { (error) in
if error != nil {
XCTFail((error?.localizedDescription)!)
}
}
} catch {
print("contents could not be loaded")
}
} else {
print("example.txt not found!")
}
}
Create events:
func createTestEvents(testDataArray: [TestData], completion: #escaping (_ eventArray: [EKEvent]) -> Void) -> Void
{
let eventStore = EKEventStore()
var testEvents = [EKEvent]()
var index = 0
for testDataEvent in testDataArray
{
eventStore.requestAccess(to: EKEntityType.event, completion: {
granted, error in
if (granted) && (error == nil) {
print("===GRATNED, CREATING EVENT-", index)
index += 1
let event = EKEvent.init(eventStore: eventStore)
event.title = testDataEvent.combinedFields
event.startDate = Date()
event.endDate = Date()
event.isAllDay = false
var attendees = [EKParticipant]()
for i in 0 ..< 5 {
if let attendee = self.createParticipant(email: "test\(i)#email.com") {
attendees.append(attendee)
}
}
event.setValue(attendees, forKey: "attendees")
try! eventStore.save(event, span: .thisEvent)
let meetingObj = Parser.parse(calendarEvent: event)
testDataEvent.meeting = meetingObj
testEvents.append(event)
if(testEvents.count == testDataArray.count){
completion (testEvents)
}
}
})
}
}

Swift - AVAudioPlayer doesn't work properly

I have the following code :
let speechRecognizer = SFSpeechRecognizer()!
let audioEngine = AVAudioEngine()
var recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
var recognitionTask = SFSpeechRecognitionTask()
var audioPlayer : AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
playSound(sound: "oops")
speechRecognizer.delegate = self
requestSpeechAuth()
}
func requestSpeechAuth(){
SFSpeechRecognizer.requestAuthorization { (authStatus) in
OperationQueue.main.addOperation({
switch authStatus {
case.authorized:
print("authorized")
case.denied:
print("denied")
case.restricted:
print("restricted")
case.notDetermined:
print("not determined")
}
})
}
}
// Function called when I press on my record button
func SpeechButtonDown() {
print("Start recording")
if audioEngine.isRunning {
endRecording() {
} else {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(AVAudioSessionCategoryRecord)
try audioSession.setMode(AVAudioSessionModeMeasurement)
try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
if let inputNode = audioEngine.inputNode {
recognitionRequest.shouldReportPartialResults = true
recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
print("1")
if let result = result {
self.instructionLabel.text = result.bestTranscription.formattedString
print("2")
if result.isFinal {
self.audioEngine.stop()
inputNode.removeTap(onBus: 0)
if self.instructionLabel.text != "" {
self.compareWordwithVoice()
}
}
}
})
let recognitionFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recognitionFormat, block: { (buffer, when) in
self.recognitionRequest.append(buffer)
})
audioEngine.prepare()
try audioEngine.start()
}
} catch {
}
}
}
// Function called when I release the record button
func EndRecording() {
endRecording()
print("Stop recording")
}
func endRecording() {
audioEngine.stop()
recognitionRequest.endAudio()
audioEngine.inputNode?.removeTap(onBus: 0)
}
func playSound(sound: String) {
if let url = Bundle.main.url(forResource: sound, withExtension: "wav") {
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
guard let player = audioPlayer else { return }
player.prepareToPlay()
player.play()
print("tutu")
} catch let error {
print(error.localizedDescription)
}
}
}
func compareWordwithVoice() {
let StringToLearn = setWordToLearn()
print("StringToLearn : \(StringToLearn)")
if let StringRecordedFull = instructionLabel.text{
let StringRecorded = (StringRecordedFull as NSString).replacingOccurrences(of: " ", with: "").lowercased()
print("StringRecorded : \(StringRecorded)")
if StringRecorded == "appuyezsurleboutonendessousetprenoncezl’expression" {
print("not yet")
} else {
if StringToLearn == StringRecorded {
playSound(sound: "success")
print("success")
// update UI
} else {
playSound(sound: "oops")
print("oops")
// update UI
}
}
}
}
func setWordToLearn() -> String {
if let wordToLearnFull = expr?.expression {
print(wordToLearnFull)
var wordToLearn = (wordToLearnFull as NSString).replacingOccurrences(of: " ", with: "").lowercased()
wordToLearn = (wordToLearn as NSString).replacingOccurrences(of: ".", with: "")
wordToLearn = (wordToLearn as NSString).replacingOccurrences(of: "!", with: "")
wordToLearn = (wordToLearn as NSString).replacingOccurrences(of: "?", with: "")
wordToLearn = (wordToLearn as NSString).replacingOccurrences(of: ",", with: "")
wordToLearn = (wordToLearn as NSString).replacingOccurrences(of: "/", with: "")
print(wordToLearn)
return wordToLearn
}
print("no wordToLearn")
return ""
}
The problem is that the playSound works perfectly when it is in the viewDidLoad but doesn't work when it is called by the compareThing() function but it display "tutu" on both cases so it performs the playSound function every time.
Can the problem be if AVAudioPlayer and AVAudioEngine cannot work at the same time ?
Thx
Ive experienced the same thing with my code and from searching online it seems like there is an unspoken bug "when using AvAudioPlayer and Engine separately"
I got the information from the following link. I did not find anything else online that states why this bug happens though.
https://swiftios8dev.wordpress.com/2015/03/05/sound-effects-using-avaudioengine/
The suggestion was to use AVAudioEngine for everything.
I think "compareThings" always plays "oops" sound and this sound is not good (too quiet or broken).
Please try to play "oops" sound from "viewDidLoad" func to make sure sound is okay.
If it is okay (I don't think so) - set breakpoint in "playSound" func to see what is going on (sound name, does it exists etc).