开发者

detecting when user starts video recording

I have a UIImagePickerController which I use to record video. now i want to detect when the user taps on record button. the delegate doesn't provide any such callback.

Is there any good way to find out wh开发者_StackOverflowen the video is being recorded?


You’re right: the delegate doesn’t receive any information on when video capture is happening. The easy solution is to simply roll your own camera controls—setting the showsCameraControls property to NO and providing your own controls in a custom view with the cameraOverlayView property—and have your “capture” button call the image picker controller’s -startVideoCapture and -stopVideoCapture methods, along with providing whatever notification you need to other parts of the app.


In a perfect world you'd want Apple to provide just a couple of delegates that would do that. For example :

  • (void)imagePickerControllerDidStartVideoCapturing:(UIImagePickerController *)picker
  • (void)imagePickerControllerDidStopVideoCapturing:(UIImagePickerController *)picker

Reality however (as per apple documentation) is that :

  • Protocol for UIImagePickerController is too basic to do that
  • This class is intended to be used as-is and does not support subclassing
  • The view hierarchy for this class is private and must not be modified

Documentation also states : "You can assign a custom view to the cameraOverlayView property and use that view to present additional information or manage the interactions between the camera interface and your code".

In my application I needed to present "UIProgressView" to indicate how much longer the video could be recorded. In order to accomplish that I needed to be able to detect the moment video capturing started.

I didn't want to disable native camera controls because they are cool and I'm lazy so that I didn't want to spend much time reinventing the wheel. Simply all I needed was to capture the fact that a big RED button was tapped to either start or stop recording.

My solution was to "cover" original Start/Stop recording button with a custom view and enable user interaction for that view as follow :

overlayView = [[UIView alloc] initWithFrame:self.view.frame];

// Start/ Stop fake button
UIView *ssView = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width / 2 - 35, self.view.frame.size.height - 71, 70, 70)];
[ssView setUserInteractionEnabled:YES];

// Background color below is only there to make sure my pseudo-button overlaps native Start/Stop button. Comment out the line below to leave it transparent
[ssView setBackgroundColor:[UIColor colorWithRed:0 green:1 blue:0 alpha:0.5f]];

UITapGestureRecognizer *t = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
[ssView addGestureRecognizer:t];

[overlayView addSubview:ssView];  

// My own progress bar
UIProgressView *p = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
[p setTintColor:[UIColor redColor]];
[p setCenter:CGPointMake(30, 130)];
[p setTransform:CGAffineTransformMakeRotation( M_PI / 2 )];
[p setProgress:0];

[overlayView addSubview:p];

pickerController.cameraOverlayView = overlayView;

I then defined event handler for a tap as follow :

-(void)tapped:(id)sender {

    if (isRecording) {
        [pickerController stopVideoCapture];
        NSLog(@"Video capturing stopped...");
        // add your business logic here ie stop updating progress bar etc...
        [pickerController.cameraOverlayView setHidden:YES];
        isRecording = NO;
        return;
    }

    if ([pickerController startVideoCapture]) {
        NSLog(@"Video capturing started...");
        // add your business logic here ie start updating progress bar etc...
        isRecording = YES;
    }

}

Full code of the interface file :

#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>

@interface ViewController : UIViewController <UIImagePickerControllerDelegate>
- (IBAction)openCamera:(id)sender;

@end

Implementation file :

#import "ViewController.h"

@interface ViewController () {
    UIImagePickerController *pickerController;
    UIView* overlayView;
    BOOL isRecording;
}

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    isRecording = NO;

    pickerController = [[UIImagePickerController alloc] init];
    pickerController.delegate = self;
    pickerController.allowsEditing = NO;
    pickerController.videoMaximumDuration = 30.0f;
    pickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
    pickerController.mediaTypes = [[NSArray alloc] initWithObjects: (NSString *) kUTTypeMovie, nil];

    // I want default controls be available here...
    pickerController.showsCameraControls = YES;

    overlayView = [[UIView alloc] initWithFrame:self.view.frame];

    // Start/ Stop fake button
    UIView *ssView = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width / 2 - 35, self.view.frame.size.height - 71, 70, 70)];
    [ssView setUserInteractionEnabled:YES];
    // Background color below is only there to make sure myt pseudo-button overlaps native Start/Stop button
    [ssView setBackgroundColor:[UIColor colorWithRed:0 green:1 blue:0 alpha:0.5f]];

   UITapGestureRecognizer *t = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
   [ssView addGestureRecognizer:t];

   [overlayView addSubview:ssView];

   // My own progress bar
   UIProgressView *p = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
   [p setTintColor:[UIColor redColor]];
   [p setCenter:CGPointMake(30, 130)];
   [p setTransform:CGAffineTransformMakeRotation( M_PI / 2 )];
   [p setProgress:0];

   [overlayView addSubview:p];

   pickerController.cameraOverlayView = overlayView;

}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
    // Cancel button tapped
    [picker dismissViewControllerAnimated:YES completion:nil];
}

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {

    NSLog(@"Got image : %@", info);
    [picker dismissViewControllerAnimated:YES completion:nil];

    // Do something with video captured
}

-(void)tapped:(id)sender {

    if (isRecording) {
        [pickerController stopVideoCapture];
        NSLog(@"Video capturing stopped...");
        // add your business logic here ie stop updating progress bar etc...
        [pickerController.cameraOverlayView setHidden:YES];
        isRecording = NO;
        return;
    }

    if ([pickerController startVideoCapture]) {
        NSLog(@"Video capturing started...");
        // add your business logic here ie start updating progress bar etc...
        isRecording = YES;
    }

}

- (IBAction)openCamera:(id)sender {
    [pickerController.cameraOverlayView setHidden:NO];
    [self presentViewController:pickerController animated:YES completion:nil];    
}
@end

You might have noticed that I'm hiding cameraOverlayView once video capturing is stopped.

[pickerController.cameraOverlayView setHidden:YES];

This is to allow "Retake / Play and Use" native controls to work properly after video has been recorded.


Here is a swift 3 UIImagePickerController class that will only allow the user to capture video if the camera is in landscape.

When you create your UIImagePickerController:

var imagePicker = CameraVideo_ViewController()

You need to have this in your delegate as well:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    self.imagePicker.isInFinalScreen = false
}

And this is the class. I don't really have time to tidy it up and it's really messy... I created this class by trying everything I could without removing the default controls. A bunch of hacks were used so be very careful when removing variables because most of them count even if they seem useless.I will try to repost a cleaner version when I can find some time but until then if someone could repost a cleaner and more newbie friendly version of this I/we would appreciate that.

import UIKit
import AVFoundation;

class CameraVideo_ViewController: UIImagePickerController {

var viewMain:UIView!
var lastOrientationWasLandscape:UIDeviceOrientation?
var isForLibrary:Bool! = false
var parentController:UIViewController!

override func viewDidAppear(_ animated: Bool) {
    if isForLibrary == true {
        return
    }
    let orientation = UIDevice.current.orientation
    if orientation == .landscapeLeft || orientation == .landscapeRight {
        lastOrientationWasLandscape = orientation
    }
    if (self.isInFinalScreen == true){
        self.recordBut.frame = CGRect(x: self.view.frame.size.width - 70 - 16, y: self.view.frame.size.height / 2 - 35, width: 70, height: 70)
        self.isInFinalScreen = false
    }
    recordBut.alpha = 1
    recordBut.isUserInteractionEnabled = true
    retakeBut.alpha = 1
    retakeBut.isUserInteractionEnabled = true
    UIDevice.current.beginGeneratingDeviceOrientationNotifications()
    NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged(_:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
    let notif = Notification.init(name: NSNotification.Name.UIDeviceOrientationDidChange)
    orientationChanged(notif)
}
override func viewWillDisappear(_ animated: Bool) {
    viewMain.alpha = 0
    viewMain.isUserInteractionEnabled = false
    lastOrientationWasLandscape = nil
    recordBut.alpha = 0
    recordBut.isUserInteractionEnabled = false
    retakeBut.alpha = 0
    retakeBut.isUserInteractionEnabled = false
    self.viewMain.alpha = 0
    self.viewMain.isUserInteractionEnabled = false
    isInFinalScreenBool = false
    recordedThisSession = false
    if isForLibrary == true {
        return
    }
    print("hidingCameraView")
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
    UIDevice.current.endGeneratingDeviceOrientationNotifications()

}

override func viewDidLoad() {
    super.viewDidLoad()
    if isForLibrary == true {
        return
    }

    viewMain = UIView()
    viewMain.frame = CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.height)
    viewMain.backgroundColor = UIColor.clear

    let viewBg = UIView()
    viewBg.frame = CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.height)
    viewBg.backgroundColor = UIColor.black
    viewBg.alpha = 0.5

    let viewAlertBg = UIView()
    viewAlertBg.frame = CGRect(x: view.frame.width/2 - 250/2, y: view.frame.height/2 - 100/2 - 25, width: 250, height: 100)
    viewAlertBg.backgroundColor = UIColor.white
    viewAlertBg.alpha = 1
    viewAlertBg.clipsToBounds = true;
    var path = UIBezierPath(roundedRect:viewAlertBg.bounds,
                            byRoundingCorners:[.topLeft, .topRight],
                            cornerRadii: CGSize(width: 25, height:  25))

    var maskLayer = CAShapeLayer()

    maskLayer.path = path.cgPath
    viewAlertBg.layer.mask = maskLayer


    let viewAlertBgUnderline = UIView()
    viewAlertBgUnderline.frame = CGRect(x: view.frame.width/2 - 250/2, y: viewAlertBg.frame.maxY, width: 250, height: 1)
    viewAlertBgUnderline.backgroundColor = UIColor.lightGray
    viewAlertBgUnderline.alpha = 1
    viewAlertBgUnderline.clipsToBounds = true;

    let viewAlertCancelBut = UIButton()
    viewAlertCancelBut.frame = CGRect(x: view.frame.width/2 - 250/2, y: viewAlertBgUnderline.frame.maxY, width: 250, height: 50)
    viewAlertCancelBut.backgroundColor = UIColor.white
    viewAlertCancelBut.setTitle("Back", for: .normal)
    viewAlertCancelBut.setTitleColor(UIColor.blue, for: .normal)
    path = UIBezierPath(roundedRect:viewAlertCancelBut.bounds,
                            byRoundingCorners:[.bottomLeft, .bottomRight],
                            cornerRadii: CGSize(width: 25, height:  25))

    maskLayer = CAShapeLayer()

    maskLayer.path = path.cgPath
    viewAlertCancelBut.layer.mask = maskLayer
    viewAlertCancelBut.addTarget(self, action: #selector(onBack(_:)), for: .touchUpInside)

    let alertLabel = UILabel()
    alertLabel.numberOfLines = 4
    alertLabel.frame = CGRect(x: 16, y: 16, width: 250 - 32, height: 100 - 32)
    alertLabel.textAlignment = NSTextAlignment.center
    alertLabel.adjustsFontSizeToFitWidth = true
    alertLabel.font = UIFont.systemFont(ofSize: 12)
    alertLabel.backgroundColor = UIColor.clear
    alertLabel.textColor = UIColor.black
    let boldText = "The video must be recorded in landscape mode.\n"
    let normalText  = "Please hold your device in a horizontal position!"
    let attrs = [NSFontAttributeName : UIFont.boldSystemFont(ofSize: 13)]
    let boldString = NSMutableAttributedString(string:boldText, attributes:attrs)
    let attributedString = NSMutableAttributedString(string:"")
    attributedString.append(boldString)
    attributedString.append(NSMutableAttributedString(string:normalText, attributes:nil))
    alertLabel.attributedText = attributedString

    viewAlertBg.addSubview(alertLabel)
    viewMain.addSubview(viewBg)
    viewMain.addSubview(viewAlertCancelBut)
    viewMain.addSubview(viewAlertBg)
    viewMain.addSubview(viewAlertBgUnderline)

    viewMain.alpha = 0
    viewMain.isUserInteractionEnabled = false

    // Start/ Stop fake button
    recordBut = UIButton()
    if (UIDevice.current.userInterfaceIdiom == .phone){
        recordBut.frame = CGRect(x: self.view.frame.size.width / 2 - 35, y: self.view.frame.size.height - 70 - 2, width: 70, height: 70)
    }else{
        recordBut.frame = CGRect(x: self.view.frame.size.height - 70 - 16, y: self.view.frame.size.width/2 - 35, width: 70, height: 70)
    }

    recordBut.setTitle("", for: .normal)
    recordBut.setTitleColor(UIColor.blue, for: .normal)
    recordBut.isUserInteractionEnabled = true
    recordBut.backgroundColor = UIColor(red: 0, green: 1, blue: 0, alpha: 0.5)
    recordBut.addTarget(self, action: #selector(tapped(_:)), for: .touchUpInside)
    self.view.addSubview(recordBut)

    retakeBut = UIButton()
    if (UIDevice.current.userInterfaceIdiom == .phone){
        retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
    }else{
        retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.width - 70, width: 80, height: 70)
    }
    retakeBut.setTitle("", for: .normal)
    retakeBut.setTitleColor(UIColor.blue, for: .normal)
    retakeBut.isUserInteractionEnabled = true
    retakeBut.backgroundColor = UIColor(red: 0, green: 1, blue: 0, alpha: 0.5)
    retakeBut.addTarget(self, action: #selector(retake(_:)), for: .touchUpInside)
    self.view.addSubview(retakeBut)
    self.view.addSubview(viewMain)

}
override func viewDidLayoutSubviews() {
    if isForLibrary == true {
        return
    }
    self.adjustViews(for: UIDevice.current.orientation)
}
var t:UITapGestureRecognizer!
var recordBut:UIButton!
var retakeBut:UIButton!
var isInFinalScreen:Bool  = false
var isRecording:Bool = false
var isInFinalScreenBool:Bool = false
var recordedThisSession:Bool = false
func tapped(_ sender:UIButton){
    if (isRecording == false && self.startVideoCapture()){
        recordedThisSession = true
        isRecording = true
        retakeBut.alpha = 0
        retakeBut.isUserInteractionEnabled = false
    }else{
        retakeBut.alpha = 1
        retakeBut.isUserInteractionEnabled = true
        recordBut.alpha = 0
        recordBut.isUserInteractionEnabled = false
        self.stopVideoCapture()
        isRecording = false
        if (UIDevice.current.orientation != .portrait){
            self.adjustViews(for: UIDevice.current.orientation)
        }
        isInFinalScreen = true
        return
    }
}
func retake(_ sender:UIButton){
    if (recordedThisSession == false){
        onBack(sender)
    }
    self.dismiss(animated: true, completion: {
        self.parentController.present((self.parentController as! AddVideo_ViewController).imagePicker, animated: true, completion: {

        })
    })
}

func onBack(_ sender:UIButton){
    self.isInFinalScreen = false
    self.dismiss(animated: true, completion: {
    })
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

func orientationChanged(_ notification: Notification) {
    self.adjustViews(for: UIDevice.current.orientation)
}

func adjustViews(for orient: UIDeviceOrientation) {
    var orientation = orient
    if (orientation.isLandscape == true) || (orientation.isFlat && lastOrientationWasLandscape?.isPortrait == false) {
        print(".....landscape.....")
            if (UIDevice.current.userInterfaceIdiom == .pad){
                self.recordBut.frame = CGRect(x: self.view.frame.size.width - 70 - 16, y: self.view.frame.size.height/2 - 35, width: 70, height: 70)
                self.retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
            }else{
                recordBut.frame = CGRect(x: self.view.frame.size.width / 2 - 35, y: self.view.frame.size.height - 70 - 2, width: 70, height: 70)
                retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
            }
            if (recordedThisSession == false){
                if (UIDevice.current.userInterfaceIdiom == .pad){
                    self.retakeBut.frame = CGRect(x: self.view.frame.size.width - 100, y: self.view.frame.size.height - 70, width: 100, height: 70)
                }else{
                    retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
                }
            }

            if (self.isInFinalScreenBool == true){
                return
            }
            if (self.isInFinalScreen == true){
                self.isInFinalScreenBool = !isInFinalScreenBool
                self.isInFinalScreen = false
                return
            }
            if (self.isRecording){
                return
            }
            self.viewMain.alpha = 0
            self.viewMain.isUserInteractionEnabled = false
            self.lastOrientationWasLandscape = orientation
    }
    else {
        print(".....portrait.....")
        self.lastOrientationWasLandscape = UIDeviceOrientation(rawValue: UIDeviceOrientation.portrait.rawValue)
            if (UIDevice.current.userInterfaceIdiom == .pad){
                self.recordBut.frame = CGRect(x: self.view.frame.size.width - 70 - 16, y: self.view.frame.size.height / 2 - 35, width: 70, height: 70)
                self.retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
            }else{
                recordBut.frame = CGRect(x: self.view.frame.size.width / 2 - 35, y: self.view.frame.size.height - 70 - 2, width: 70, height: 70)
                retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
            }
            if (recordedThisSession == false){
                if (UIDevice.current.userInterfaceIdiom == .pad){
                    self.retakeBut.frame = CGRect(x: self.view.frame.size.width - 100, y: self.view.frame.size.height - 70, width: 100, height: 70)
                }else{
                    retakeBut.frame = CGRect(x: 0, y: self.view.frame.size.height - 70, width: 80, height: 70)
                }
            }

            if (self.isInFinalScreenBool == true){
                return
            }
            if (self.isInFinalScreen){
                return
            }
            if (self.isRecording){
                return
            }
            self.viewMain.alpha = 1
            self.viewMain.isUserInteractionEnabled = true
    }

}
/*
// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
}
*/

}

Best of luck!


According to ios api, the method "startVideoCapture" gives a boolean return value

Yes means it is recording

No means one of the followings:

  1. Movie capture is already in progress
  2. The device does not support movie
  3. capture The device is out of disk space

[Reference: http://developer.apple.com/library/ios/#documentation/uikit/reference/UIImagePickerController_Class/UIImagePickerController/UIImagePickerController.html]

So as long as 'startRecording' is returning a Yes, you can almost certainly say it is recording. Of course, to check that, you can always manually define your own call back with NSTimer (although previously there was abit of a hot debate bout its accuracy.)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜