Published on

Edit bars

Authors

In NanoFlick, we let the user trim a shot after taking it. There's a lot of potential for regret in the trim operation, though. If you mistrim, you can't undo it - we overwrite the file. My task for today was to make the trim operation more forgiving. Now, if you mistrim, you can just tap the trim button again to reset the trim. I decided to implement this by making the trim operation not modify the file, but mark the file as "trimmed" in the shot metadata. Whenever the movie is compiled (in our case, into an AVComposition object, but you could imagine an ffmpeg call), we only use the trimmed portion of the file.

Unfortunately, the system video editor, UIVideoEditorController, only provides a file export API, and doesn't provide time ranges. I would like to use the stock trimmer view controller but just override what the button does, but there's no out of the box way of finding the button in the view controller.

My first instinct was to find what the button was originally calling.

final class MyVideoEditor: UIVideoEditorController {    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print(navigationBar.topItem!.rightBarButtonItem!.target!)
        print(navigationBar.topItem!.rightBarButtonItem!.action!)
    }
}

The action was a selector called _trimVideo:. Pretty bog-standard underscored private API doing basically what we'd expect.

The target, on the other hand, was not self but a completely separate instance of PLUIEditVideoViewController. A few minutes of looking around locally before deciding to google led to a header dump.

At this point, my hypothesis is that photo pickers also allow for trimming, and that UI code is shared with the video editor. The photo picker has a private field that noted the trimming start and end times, but setting a breakpoint for that delegate method in lldb (breakpoint set -r didFinishPickingMediaWithInfo) didn't yield any hits.

So, back to square one. Looking at the implementation of _trimVideo: in objdump reveals that it calls cropOverlayWasOKed:. Loading the library up in cutter1 reveals two methods called cropOverlayWasOKed: (one UIImageViewController, one UIEditImageViewController), so loading into lldb ()breakpoint set -r cropOverlayWasOKed) shows which one we hit.

We hit on PLUIImageViewController's version (strange, you'd figure EditVideoViewController would use the editor, but perhaps it's just a photo editor).

Looking at the disassembly on cutter, we see calls to _handleVideoSelected, which has calls to... startTime and endTime! Yay!

Now that we know the data is there, we have to figure out how to extract it.

I have lldb set a breakpoint on any symbol that looks like startTime, and I get a method called startTime in PLVideoView that calls scrubberStartTime and movieTimeFromScrubberStartTime. Looking at the symbols for PLVideoView it appears to handle all the trimming - this is our view. If we find an instance of it, we can just call the methods to get the start and end times.

Running pviews in lldb (from Chisel) shows us a list of all the views in the app. We can see that there's a PLVideoView in the view hierarchy

(lldb) pviews
<UIWindow: 0x100b0a260; frame = (0 0; 375 812); gestureRecognizers = <NSArray: 0x2810ff480>; layer = <UIWindowLayer: 0x2810ff1b0>>
   | <UITransitionView: 0x100c0a210; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x281e96de0>>
   |    | <UIDropShadowView: 0x100c0a410; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x281e8d480>>
   | <UITransitionView: 0x102805f70; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x281eb8640>>
   |    | <UILayoutContainerView: 0x100b08720; frame = (0 0; 375 812); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x2810e1e30>; layer = <CALayer: 0x281ea4e60>>
   |    |    | <UINavigationTransitionView: 0x100b12160; frame = (0 0; 375 812); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x281ecc4c0>>
   |    |    |    | <UIViewControllerWrapperView: 0x100b0e0d0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x281ecd880>>
   |    |    |    |    | <PLUIView: 0x102a04080; frame = (0 88; 375 724); autoresize = W+H; backgroundColor = UIExtendedGrayColorSpace 0 1; layer = <CALayer: 0x281ea82c0>>
   |    |    |    |    |    | <PLTileContainerView: 0x100c15de0; frame = (0 0; 375 724); autoresize = W+H; layer = <CALayer: 0x281eb4200>>
   |    |    |    |    |    |    | *<<PLVideoView: 0x100c14a20; frame = (0 0; 375 724); autoresize = W+H; layer = <CALayer: 0x281ecbe40>> path:/private/var/mobile/Containers/Data/Application/D4D01DBD-3532-4D0E-A1E2-BEF73DBCCF2F/tmp/vater.mov>*
   |    |    |    |    |    |    |    | <PLMoviePlayerView: 0x1028075a0; frame = (0 0; 375 724); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x281eb8b20>>
   |    |    |    |    |    |    |    |    | <PLAVPlayerView: 0x102807970; frame = (0 0; 375 724); autoresize = W+H; userInteractionEnabled = NO; layer = <AVPlayerLayer: 0x2810d0390>>
   |    |    |    |    |    |    |    |    |    | <AVPlayerLayerIntermediateLayer: 0x2810d0300> (layer)
   |    |    |    |    |    |    |    |    |    |    | <FigVideoContainerLayer: 0x283d94280> (layer)
   |    |    |    |    |    |    |    |    |    |    |    | <FigVideoLayer: 0x28100c9c0> (layer)
Abbreviated output of pviews in lldb

Getting the video view from this is simply trawling through views. Unfortunately, just trying to directly run videoView.perform(NSSelectorFromString("startTime")).takeRetainedValue() yields no value, so we have to do some more digging. Using lumos and customDump to explore the object revealed MANY variables and options, such as...

  • _exportSession if we want to change the export settings sometime down the line
  • _scrubber of type UIMovieScrubber if this approach doesn't pan out and we need another way to get the start/end times

At this point, I realize that it's a trivial property that I can just get, so a simple videoView.value(forKey: "startTime") as! Double does the trick. I do the same for endTime and we have our start and end times!

Retrospective

Two alternate approaches that may've served me better:

  • just have lldb break on symbols with start and end before pressing the button and hope that the debugger would stop at a place to lead to me finding VideoView.
  • run pviews and look at the view class names - this is my bad for assuming that UIVideoEditorController contained all the logic and didn't delegate it to subviews / child view controllers. Still, this approach didn't take too long. I probably could've only saved around a half hour doing it this other way, and it was kinda fun this way, so not too big of a deal.

Footnotes

  1. Cutter is a great tool for reverse engineering. It's a GUI for the command-line tool radare2. I've heard a lot about it, but only recently started using it. It's great!