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:
- Movie capture is already in progress
- The device does not support movie
- 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.)
精彩评论