25

I've got an app that exports an AVMutableComposition into a .mov file, and I'd like for the user to see the status of the export with a progress bar the same way that you would if you sent a text message or uploaded a file.

I know how to create a progress bar when I know the duration of a task (such as playing an audio file), but since there's no set duration for the export I'm unsure how to proceed.

I've got an activity indicator currently but it doesn't provide the best user experience.

Does anyone have any pointers?

Jon Kiparsky
  • 7,499
  • 2
  • 23
  • 38
Orpheus Mercury
  • 1,617
  • 2
  • 15
  • 30

5 Answers5

39

I came up with an answer a while back so I'll post it in case it can help someone:

First, in the method in which you call AVAssetExportSession you've got to set up a timer to update your UIProgressView once you've initiated the export:

//`AVAssetExportSession` code here
    self.exportProgressBarTimer = [NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:@selector(updateExportDisplay) userInfo:nil repeats:YES];
...

Then you need a method to update your display taking into account that the progress property on AVAssetExportSession goes from 0 - 1:

- (void)updateExportDisplay {
    self.exportProgressBar.progress = exportSession.progress;
    if (self.exportProgressBar.progress > .99) {
        [self.exportProgressBarTimer invalidate];
    }
}
James Nelson
  • 1,793
  • 1
  • 20
  • 25
Orpheus Mercury
  • 1,617
  • 2
  • 15
  • 30
  • are you calling `self.exportProgressBarTimer =` inside or outside of the `exportAsynchronouslyWithCompletionHandler` block? `self.exportSession.progress` always shows as 1.0 in `updateExportDisplay` for me. – Ivan Lesko May 07 '14 at 17:29
  • Outside of the `exportAsynchronouslyWithCompletionHandler` block. It is working beautifully for me. – James Nelson Dec 22 '14 at 21:23
10

Swift 3 example

Using Notification Center to send progress updates to listeners

//`AVAssetExportSession` code above
var exportProgressBarTimer = Timer() // initialize timer
if #available(iOS 10.0, *) {
    exportProgressBarTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
    // Get Progress
    let progress = Float((exportSession?.progress)!);
    if (progress < 0.99) {
       let dict:[String: Float] = ["progress": progress]
       NotificationCenter.default.post(name: Notification.Name("ProgressBarPercentage"), object: nil, userInfo: dict)
    }
  }
}

// on exportSession completed
exportSession?.exportAsynchronously(completionHandler: {
   exportProgressBarTimer.invalidate(); // remove/invalidate timer
   if exportSession?.status == AVAssetExportSessionStatus.completed { 
      // [....Some Completion Code Here]
   }
})

Then setup the notification center listener anywhere you'd like using

NotificationCenter.default.addObserver(self, selector: #selector(self.statusUpdate(_:)), name: NSNotification.Name(rawValue: "ProgressBarPercentage"), object: nil)
andrewoodleyjr
  • 2,971
  • 2
  • 21
  • 21
2

Same issue i have faced in iOS 8.0, i resolved it using dispatch quee

- (void)convertVideoToLowQuailtyWithInputURL:(NSURL*)inputURL outputURL:(NSURL*)outputURL handler:(void (^)(AVAssetExportSession*))handler{

[[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:inputURL options:nil];

exportSession2 = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetLowQuality];
exportSession2.outputURL = outputURL;
exportSession2.outputFileType = AVFileTypeQuickTimeMovie;

[exportSession2 exportAsynchronouslyWithCompletionHandler:^(void)
 {
     handler(exportSession2);
 }];

 dispatch_async(dispatch_get_main_queue(), ^(void){

      self.exportProgressBarTimer = [NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:@selector(updateExportDisplay) userInfo:nil repeats:YES];
 });

}

Waseem Sarwar
  • 2,645
  • 1
  • 21
  • 18
0

Use below lines of code.

AVAssetExportSession *session = [AVAssetExportSession exportSessionWithAsset:composition presetName:AVAssetExportPresetMediumQuality];
self.exportSession = session;

// 出力先(テンポラリファイル)の設定。
NSString *filePath = NSTemporaryDirectory();
filePath = [filePath stringByAppendingPathComponent:@"out.mov"];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
session.outputURL = [NSURL fileURLWithPath:filePath];

// 出力タイプの設定。
session.outputFileType = AVFileTypeQuickTimeMovie;

// 非同期エクスポートの開始。
[session exportAsynchronouslyWithCompletionHandler:^{
    if (session.status == AVAssetExportSessionStatusCompleted) {
        // フォトアルバムへの書き込み。
        ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
        [library writeVideoAtPathToSavedPhotosAlbum:session.outputURL completionBlock:^(NSURL *assetURL, NSError *error){
            if (error) {
                self.resultLabel.text = [NSString stringWithFormat:@"アセット書き込み失敗\n%@", error];
            } else {
                self.resultLabel.text = [NSString stringWithFormat:@"完了\n%@", assetURL];
            }
        }];
        [library autorelease];
    } else if (session.status == AVAssetExportSessionStatusCancelled) {
        self.resultLabel.text = @"エクスポート中断";
    } else {
        self.resultLabel.text = [NSString stringWithFormat:@"エクスポート失敗\n%@", session.error];
    }
}];


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    while (session.status == AVAssetExportSessionStatusExporting) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.progressView.progress = session.progress;
        });
    }
});

Reference Link : https://github.com/keijiro/iOS4BookSampleCode/blob/master/3.3.SimpleExport/Classes/SimpleExportViewController.m

Payal Maniyar
  • 4,293
  • 3
  • 25
  • 51
0

I've got a solution which is designed to work well with SwiftUI and async/await.

class ObservableExporter {
    
    var progressTimer: Timer?
    let session: AVAssetExportSession
    public let progress: Binding<Double>
    public var duration: TimeInterval?
    
    init(session: AVAssetExportSession, progress: Binding<Double>) {
        self.session = session
        self.progress = progress
    }
    
    func export() async throws -> AVAssetExportSession.Status {
        progressTimer = Timer(timeInterval: 0.1, repeats: true, block: { timer in
            self.progress.wrappedValue = Double(self.session.progress)
        })
        RunLoop.main.add(progressTimer!, forMode: .common)
        let startDate = Date()
        await session.export()
        progressTimer?.invalidate()
        let endDate = Date()
        duration = endDate.timeIntervalSince(startDate)
        if let error = session.error {
            throw error
        } else {
            return session.status
        }
    }
}

You just initialise the ObservableExporter class with a session and the progress is a binding on a @State variable in your SwiftUI class suitable for use by ProgressView.

An example which shows this embodied in a sample app is Watermark Sample App

Faisal Memon
  • 2,711
  • 16
  • 30