rFlex / SCRecorder

iOS camera engine with Vine-like tap to record, animatable filters, slow motion, segments editing
Apache License 2.0
3.06k stars 583 forks source link

Crash when adding UIView as overlay to export session #321

Open UIApplicationMain opened 8 years ago

UIApplicationMain commented 8 years ago

My app crashes when I try to add a UIView as an overlay for a video export session. I'm not sure if I am misunderstanding how to use overlay for videos

I have a UITextView that the user can edit on my screen, when time to save the video, I do the following:

let tempView: UIView = UIView(frame: CGRectMake(0, 0, self.view.frame.width, self.view.frame.height))
tempView.backgroundColor = UIColor.clearColor()

NSKeyedUnarchiver.unarchiveObjectWithData(NSKeyedArchiver.archivedDataWithRootObject(overlayText))!

tempView.addSubview(tempOverlayText as! UITextView)

exportSession.videoConfiguration.overlay = tempView

It crashes at line [overlay.layer renderInContext:ctx]; (Line 181) in - (void)CGRenderWithInputPixelBuffer:(CVPixelBufferRef)inputPixelBuffer toOutputPixelBuffer:(CVPixelBufferRef)outputPixelBuffer atTimeInterval:(NSTimeInterval)timeSeconds

with the error: Assertion failure in void UIPerformResizeOfTextViewForTextContainer(NSLayoutManager , UIView , NSTextContainer , NSUInteger)(), /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIFoundation/UIFoundation-432.1/UIFoundation/TextSystem/NSLayoutManagerPrivate.m:1551 ** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Only run on the main thread!'

UIApplicationMain commented 8 years ago

Would it be plausible to wrap

            CGContextRef ctx = SCCreateContextFromPixelBuffer(outputPixelBuffer);
            overlay.frame = CGRectMake(0, 0, CVPixelBufferGetWidth(outputPixelBuffer), CVPixelBufferGetHeight(outputPixelBuffer));
            [overlay layoutIfNeeded];

            [overlay.layer renderInContext:ctx];

            CGContextRelease(ctx);

in a dispatch_sync(dispatch_get_main_queue(), ^{ }); ?

rFlex commented 8 years ago

Seems like UITextView requires to layout in the main thread. You can't just have a dispatch_sync every time since it will probably greatly slow down the encoding. I just added a new method in SCVideoOverlay: requiresUpdateOnMainThreadAtVideoTime:videoSize: that is called to determine whether setFrame:, updateWithVideoTime: and layoutIfNeeded should be called on the main thread.

Here is what I would do now:

That way your initial layout will be called in the main thread and it should be fast for any following frames. Let me know if that fix your issue. Thanks!

UIApplicationMain commented 8 years ago

Hi rFlex,

Thanks for the turnaround. I'm trying to override requiresUpdateOnMainThreadAtVideoTime in my UIView subclass, but am running into the error: "Method does not override any method from its superclass"

I tried

    override func requiresUpdateOnMainThreadAtVideoTime(videoSize: CGSize) -> Bool {
        print("**requiresUpdateOnMainThreadAtVideoTime called**")

        if videoSize == CGSizeZero || videoSize != self.bounds.size {
            return true
        }

        return false
    }

However if I do not override it in the new tempView subclass (which just inits a UIView with same dimensions as my captured video and copies the UITextView over into the UIView subclass and calls self.addSubview(UITextView)) it works. Or is that still synchronously putting it on the video?

rFlex commented 8 years ago

Make sure to make your subview implement SCVideoOverlay. Your implementation will then look like this:

func requiresUpdateOnMainThreadAtVideoTime(videoTime: NSTimeInterval, videoSize: CGSize) -> Bool {
        print("**requiresUpdateOnMainThreadAtVideoTime called**")

        return videoTime == 0.0 || videoSize != self.bounds.size
}
UIApplicationMain commented 8 years ago

I tried it two ways:

1) To programmatically re-create a new UITextView with attributes of my editable one scaled to video size, this is done in the subclass of UIView implementing SCVideoOverlay. This calls the override function and works 100% of the time. But here I assume it doesn't have to lay out anything since it isn't a direct copy of the original UITextView.

2) To create a UITextView attribute within my subclass and set it with

let tempView: SCVideoOverlayView = SCVideoOverlayView(frame: CGRectMake(0, 0, 720.0, 1080.0))
            tempView.backgroundColor = UIColor.clearColor()
            tempView.tempTextView = NSKeyedUnarchiver.unarchiveObjectWithData(NSKeyedArchiver.archivedDataWithRootObject(overlayText)) as! UITextView

This failed with requiresUpdateOnMainThreadAtVideoTime called 2016-06-20 17:39:22.745 TallyyBeta[4845:1136198] ** Assertion failure in void UIPerformResizeOfTextViewForTextContainer(NSLayoutManager , UIView , NSTextContainer , NSUInteger)(), /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIFoundation/UIFoundation-432.1/UIFoundation/TextSystem/NSLayoutManager_Private.m:1551 2016-06-20 17:39:22.749 _* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Only run on the main thread!'

It could be that my 2nd approach is not the correct way to tackle the feature.

UIApplicationMain commented 8 years ago

Forgot to mention that in 2nd approach it still fails at [overlay.layer renderInContext:ctx];

EDIT: I tried switching the return argument to return videoTime == 0.0 || videoSize != CGSizeMake(720, 1280)

since the UITextView that is being copied isn't the same size as the video bounds (video is 720x1280), and is instead the frame size of the view.

However it returns true because videoTime is 0, but if I force return true or false they still both end up failing.

rFlex commented 8 years ago

Would you mind create a sample project so I can fix the issue directly? As a workaround in the mean time you could just create a UIImage out of your UITextView and use it instead. You could also try creating a snapshot view from the overlayText instead of making a deep copy using NSKeyedArchiver.