Custom Video Recorder для iOS приложений

1b59bc4994c84620b2fa3bd4a6132fb7.jpg Приложение Камера для iPhone / iPad очень удобно в использовании. Пользователь легко может переключаться из режима фотографирования на видеосъемку. В режиме видеосъемки показывается время съемки и всего одна кнопка (Старт / Стоп). К сожалению, при использовании стандартного UIImagePickerController«а нет возможности контролировать количество кадров в секунду и некоторые другие параметры. Я покажу, как, используя AVFoundation framework, получить доступ к более тонким настройкам камеры, таким как, количество кадров в секунду, качество видео, длительность записи, размер видео файла. Пользователь сразу будет видеть на экране видео в том качестве, в котором оно будет сохранено.

Основной объект, который позволит мне вести видеосъемку:
AVCaptureSession  // сессия захвата видео с камеры

Кроме того, мне понадобятся:
AVCaptureVideoPreviewLayer  // слой, в котором будем показывать видео с камеры в реальном времени
AVCaptureDevice  // устройство захвата видео / аудио
AVCaptureDeviceInput  // вход видео / аудио для AVCaptureSession
AVCaptureMovieFileOutput  // выход AVCaptureSession для записи захваченного видео в файл

Дизайн можно хранить в xib файле или storyboard«е. Используя Autolayout и Constraints в дизайнере можно добиться того, что все панели будут автоматически растягиваться, кнопки выравниваться по центру (левому или правому краю). У нашего VideoRecorderController«а будет три режима работы:
  1. Готов к съемке: AVCaptureSession запущена, на экране видео с камеры в реальном времени, но запись не идет.

    На нижней панели активна кнопка Cancel — отмена съемки, также активна кнопка Start — начало записи, кнопка Use Video скрыта.

    На верхней панели показано время записи — 00:00. После нажатия кнопки Cancel у делегата видеосъемки срабатывает метод -(void)videoRecorderDidCancelRecordingVideo. После нажатия кнопки Start переходим в следующий режим.

  2. Идет съемка: AVCaptureSession запущена, на экране видео с камеры в реальном времени, при этом идет запись видео в файл. На нижней панели вместо кнопки Start появляется кнопка Stop — конец записи, кнопка Cancel скрыта, кнопка Use Video также скрыта. На верхней панели показано текущее время записи — 00:22. После нажатия кнопки Stop запись останавливается, переходим в следующий режим.
  3. Съемка завершена: AVCaptureSession остановлена, на экране последний кадр отснятого видео, запись видео в файл завершена. По центру экрана появляется кнопка Play Video.
    На нижней панели вместо кнопки Cancel появляется кнопка Retake — переснять видео, появляется кнопка Use Video, кнопка Start скрыта.

    На верхней панели показана длительность видеозаписи — 00:25.
    После нажатия кнопки Play Video начнется просмотр отснятого видео с помощью AVPlayer.
    После нажатия кнопки Retake возвращаемся в первый режим.
    После нажатия кнопки Use Video у делегата видеосъемки срабатывает метод -(void)videoRecorderDidFinishRecording VideoWithOutputURL:(NSURL *)outputURL.


Три режима работы — экраны
7287dae720ce417194a13f2a79102230.PNG 0461397c7e344e30a3bc0fa1b903b3c3.PNG 5f1a342c329e41f7b3c802c321d6d71f.PNG


В файле заголовка мне необходимо описать протокол делегата видеосъемки для обработки отмены видеозаписи и успешного завершения видеозаписи.
Вот так будет выглядеть файл заголовка VideoRecorderController.h
#import 
#import 
#import 
#import 
#import 
#import 

@protocol VideoRecorderDelegate 
// метод делегата, срабатывает при успешном завершении видеозаписи
- (void)videoRecorderDidFinishRecordingVideoWithOutputPath:(NSString *)outputPath;
// метод делегата, срабатывает при отмене видеозаписи
 - (void)videoRecorderDidCancelRecordingVideo;
@end

@interface VideoRecorderController : UIViewController
@property (nonatomic, retain) NSString *outputPath;		// путь к файлу видеозаписи
@property (nonatomic, assign) id delegate;
@end


В файле реализации VideoRecorderController.m я задаю несколько констант для видеозаписи и описываю свойства и методы, которые нужно привязать в дизайнере интерфейса. Мне понадобятся также:
  • сессия захвата видео AVCaptureSession
  • файл видео выхода AVCaptureMovieFileOutput
  • устройство видео входа AVCaptureDeviceInput
  • слой для отображения видео в реальном времени AVCaptureVideoPreviewLayer
  • таймер и время для индикатора времени записи

Файл реализации VideoRecorderController.m — объявление переменных
#import "VideoRecorderController.h"

#define TOTAL_RECORDING_TIME    60*20	// максимальное время видеозаписи в секундах	
#define FRAMES_PER_SECOND       30		// количество кадров в секунду
#define FREE_DISK_SPACE_LIMIT   1024 * 1024	// минимальный размер свободного места (байт)
#define MAX_VIDEO_FILE_SIZE     160 * 1024 * 1024	// максимальный размер видеофайла (байт)
#define CAPTURE_SESSION_PRESET  AVCaptureSessionPreset352x288 // качество видеозаписи

#define BeginVideoRecording     1117	// звук начала записи видео
#define EndVideoRecording       1118	// звук конца записи видео

@interface VideoRecorderController () 
{
    BOOL WeAreRecording;	// флаг, определяющий идет ли запись видео
    
    AVCaptureSession *CaptureSession;		
    AVCaptureMovieFileOutput *MovieFileOutput;	
    AVCaptureDeviceInput *VideoInputDevice;	
}

// Эти элементы и методы нужно привязать в дизайнере интерфейса
@property (retain) IBOutlet UILabel *timeLabel; 	// индикатор времени записи на верхней панели
@property (retain) IBOutlet UIButton *startButton; 	// кнопка Start / Stop
@property (retain) IBOutlet UIImageView *circleImage;  // кружок вокруг кнопки Start
@property (retain) IBOutlet UIButton *cancelButton;      // кнопка Cancel
@property (retain) IBOutlet UIButton *useVideoButton; // кнопка Use Video
@property (retain) IBOutlet UIView *bottomView;	       // нижняя панель
@property (retain) IBOutlet UIButton *playVideoButton; // кнопка Play Video

- (IBAction)startStopButtonPressed:(id)sender;	 // обработчик нажатия кнопки Start / Stop
- (IBAction)cancel:(id)sender;			 // обработчик нажатия кнопки  Cancel
- (IBAction)useVideo:(id)sender;			// обработчик нажатия кнопки  Use Video
- (IBAction)playVideo:(id)sender;			// обработчик нажатия кнопки  Play Video

@property (retain) AVCaptureVideoPreviewLayer *PreviewLayer;

// таймер и время для индикатора времени записи
@property (retain) NSTimer *videoTimer;
@property (assign) NSTimeInterval elapsedTime;

@end


После того, как отработал метод viewDidLoad, необходимо выполнить следующие действия:
  • задать путь к файлу видеозаписи outputPath и удалить предыдущую запись
  • добавить обработчик на выход приложения в фон UIApplicationDidEnterBackgroundNotification
  • инициализировать сессию AVCaptureSession
  • найти видеоустройство по умолчанию AVCaptureDevice и создать устройство видео входа AVCaptureDeviceInput
  • перед тем, как добавить устройство видео входа, нужно обязательно вызвать
    метод [CaptureSession beginConfiguration]
  • затем добавить устройство видео входа в сессию AVCaptureSession
  • после добавления устройства видео входа нужно обязательно вызвать метод [CaptureSession commitConfiguration]
  • найти аудиоустройство по умолчанию AVCaptureDevice, создать устройство аудио входа AVCaptureDeviceInput и добавить это устройство в сессию AVCaptureSession
  • создать слой AVCaptureVideoPreviewLayer, на котором будет отображаться видео в реальном времени, привязать его к сессии AVCaptureSession, растянуть этот слой, на весь экран с сохранением пропорций (края кадра не попадут на экран)
  • пересчитать размеры слоя AVCaptureVideoPreviewLayer в зависимости от ориентации устройства и отправить этот слой на задний план, чтобы поверх него отображались все панели и кнопки управления
  • инициализировать видео выход в файл AVCaptureMovieFileOutput
  • задать частоту кадров в секунду, максимальную длину видео в секундах
  • задать максимальную длину видео в байтах
  • задать минимальный размер свободного места на диске в байтах
  • добавить видео выход в файл в сессию AVCaptureSession
  • задать качество видео для сессии AVCaptureSession
  • наконец выставить правильную ориентацию файла видео выхода AVCaptureMovieFileOutput и слоя просмотра видео AVCaptureVideoPreviewLayer

При переходе в другое приложение, если идет видеозапись, она останавится. После того, как отработал метод viewWillAppear, надо запустить сессию AVCaptureSession, на экране начинает отображаться видео в реальном времени. Но если произошел переход на этот экран после просмотра видео, то не нужно запускать AVCaptureSession — должна быть проверка, что нет файла видеозаписи.
Файл реализации VideoRecorderController.m — загрузка View Controller’а
@implementation VideoRecorderController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.outputPath = [[NSString alloc] initWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"];
    [self deleteVideoFile];
    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(applicationDidEnterBackground:)
                                                 name: UIApplicationDidEnterBackgroundNotification
                                               object: nil];
    CaptureSession = [[AVCaptureSession alloc] init];
    AVCaptureDevice *VideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    if (VideoDevice) {
        NSError *error = nil;
        VideoInputDevice = [AVCaptureDeviceInput deviceInputWithDevice:VideoDevice error:&error];
        if (!error) {
            [CaptureSession beginConfiguration];
            if ([CaptureSession canAddInput:VideoInputDevice]) {
                 [CaptureSession addInput:VideoInputDevice];
            }
            [CaptureSession commitConfiguration];
        }
    }
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    NSError *error = nil;
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
    if (audioInput) {
        [CaptureSession addInput:audioInput];
    }
    [self setPreviewLayer:[[AVCaptureVideoPreviewLayer alloc] initWithSession:CaptureSession]];
    [self.PreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    [self setupLayoutInRect:[[[self view] layer] bounds]];
    UIView *CameraView = [[UIView alloc] init];
    [[self view] addSubview:CameraView];
    [self.view sendSubviewToBack:CameraView];
    [[CameraView layer] addSublayer:self.PreviewLayer];
    MovieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
    CMTime maxDuration = CMTimeMakeWithSeconds(TOTAL_RECORDING_TIME, FRAMES_PER_SECOND);
    MovieFileOutput.maxRecordedDuration = maxDuration;
    MovieFileOutput.maxRecordedFileSize = MAX_VIDEO_FILE_SIZE;
    MovieFileOutput.minFreeDiskSpaceLimit = FREE_DISK_SPACE_LIMIT;
    if ([CaptureSession canAddOutput:MovieFileOutput]) {
        [CaptureSession addOutput:MovieFileOutput];
    }
    if ([CaptureSession canSetSessionPreset:CAPTURE_SESSION_PRESET]) {
        [CaptureSession setSessionPreset:CAPTURE_SESSION_PRESET];
    }
    [self cameraSetOutputProperties];
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    if (WeAreRecording) {
        [self stopRecording];
    }
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if (![[NSFileManager defaultManager] fileExistsAtPath:self.outputPath]) {
        WeAreRecording = NO;
        [CaptureSession startRunning];
    }
}


Поворот экрана разрешен только если сессия запущена, а запись еще не идет. Перед поворотом экрана необходимо пересчитать размеры экрана в зависимости от ориентации. После поворота надо выставить правильную ориентацию файла видео выхода AVCaptureMovieFileOutput и слоя просмотра видео AVCaptureVideoPreviewLayer.
Файл реализации VideoRecorderController.m — обработка поворотов
- (BOOL)shouldAutorotate {
    return (CaptureSession.isRunning && !WeAreRecording);
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscape | UIInterfaceOrientationMaskPortraitUpsideDown);
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator {
    [self setupLayoutInRect:CGRectMake(0, 0, size.width, size.height)];
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    [coordinator animateAlongsideTransition:^(id context) {  
    } completion:^(id context) {
        [self cameraSetOutputProperties];
    }];
}

// Этот метод выставляет правильную ориентацию файла видео выхода и слоя просмотра
// Он аналогичен viewWillTransitionToSize, нужен для поддержки версий iOS 7 и более ранних
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    [self setupLayoutInRect:[[[self view] layer] bounds]];
    [self cameraSetOutputProperties];
}

// Пересчитываем размеры слоя просмотра в зависимости от ориентации устройства
- (void)setupLayoutInRect:(CGRect)layoutRect {
    [self.PreviewLayer setBounds:layoutRect];
    [self.PreviewLayer setPosition:CGPointMake(CGRectGetMidX(layoutRect),  CGRectGetMidY(layoutRect))];
}

// Выставляем правильную ориентацию файла видео выхода и слоя просмотра
- (void)cameraSetOutputProperties {
    AVCaptureConnection *videoConnection = nil;
    for ( AVCaptureConnection *connection in [MovieFileOutput connections] ) {
        for ( AVCaptureInputPort *port in [connection inputPorts] ) {
            if ( [[port mediaType] isEqual:AVMediaTypeVideo] ) {
                videoConnection = connection;
            }
        }
    }
    
    if ([videoConnection isVideoOrientationSupported]) {
        if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortrait) {
            self.PreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;
            [videoConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
        }
        else if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortraitUpsideDown) {
            self.PreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
            [videoConnection setVideoOrientation:AVCaptureVideoOrientationPortraitUpsideDown];
        }
        else if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeLeft) {
            self.PreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
            [videoConnection setVideoOrientation:AVCaptureVideoOrientationLandscapeLeft];
        }
        else {
            self.PreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
            [videoConnection setVideoOrientation:AVCaptureVideoOrientationLandscapeRight];
        }
    }
}


По нажатию кнопки Start / Stop начнется запись, если запись еще не идет. Если запись уже идет, то запись будет останавлена. По нажатию кнопки Cancel срабатывает метод делегата видеозаписи videoRecorderDidCancelRecordingVideo. По нажатию кнопки Retake сбрасывается таймер, меняются названия кнопок, скрывается кнопка Use Video, заново запускается сессия захвата видео. По нажатию кнопки Use Video срабатывает метод делегата видеозаписи videoRecorderDidFinishRecordingVideoWithOutputPath, в который необходимо передать путь к видео файлу. По нажатию кнопки Play Video начинается показ отснятого видео, используя AVPlayer. Когда срабатывает таймер видеозаписи, обновляется индикатор времени на верхней панели. Метод делегата файла видео записи срабатывает, если размер файла достиг максимально допустимого значения или время записи достигло максимально установленного. В этот момент запись останавливается.
Файл реализации VideoRecorderController.m — обработка нажатий кнопок, метод делегата AVCaptureFileOutputRecordingDelegate
- (IBAction)startStopButtonPressed:(id)sender {
    if (!WeAreRecording) {
        [self startRecording];
    }
    else {
        [self stopRecording];
    }
}

 - (IBAction)cancel:(id)sender {
    if ([CaptureSession isRunning]) {
        if (self.delegate) {
            [self.delegate videoRecorderDidCancelRecordingVideo];
        }
    }
    else {
        self.circleImage.hidden = NO;
        self.startButton.hidden = NO;
        self.useVideoButton.hidden = YES;
        [self.cancelButton setTitle:@"Cancel" forState:UIControlStateNormal];
        self.timeLabel.text = @"00:00";
        self.elapsedTime = 0;
        [CaptureSession startRunning];
    }
}

- (IBAction)useVideo:(id)sender {
    if (self.delegate) {
        [self.delegate videoRecorderDidFinishRecordingVideoWithOutputPath:self.outputPath];
    }
}

- (IBAction)playVideo:(id)sender {
    if ([[NSFileManager defaultManager] fileExistsAtPath:self.outputPath]) {
        NSURL *outputFileURL = [[NSURL alloc] initFileURLWithPath:self.outputPath];
        AVPlayer *player = [AVPlayer playerWithURL:outputFileURL];
        AVPlayerViewController *controller = [[AVPlayerViewController alloc] init];
        [self presentViewController:controller animated:YES completion:nil];
        controller.player = player;
        controller.allowsPictureInPicturePlayback = NO;
        [player play];
    }
}

// Это начало записи видео
- (void)startRecording {
    // Проигрываем звук начала записи видео
    AudioServicesPlaySystemSound(BeginVideoRecording);
    WeAreRecording = YES;
    [self.cancelButton setHidden:YES];
    [self.bottomView setHidden:YES];
    [self.startButton setImage:[UIImage imageNamed:@"StopVideo"] forState:UIControlStateNormal];
    self.timeLabel.text = @"00:00";
    self.elapsedTime = 0;
    self.videoTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updateTime) userInfo:nil repeats:YES];
    
    // Удаляем файл видеозаписи, если он существует, чтобы начать запись по новой
    [self deleteVideoFile];
    
    // Начинаем запись в файл видеозаписи
    NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:self.outputPath];
    [MovieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];
}

- (void)deleteVideoFile {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:self.outputPath]) {
        NSError *error = nil;
        if ([fileManager removeItemAtPath:self.outputPath error:&error] == NO) {
            // Обработчик ошибки удаления файла
        }
    }
}

// Это конец записи видео
- (void)stopRecording {
    // Проигрываем звук конца записи видео
    AudioServicesPlaySystemSound(EndVideoRecording);
    WeAreRecording = NO;
    [CaptureSession stopRunning];
    self.circleImage.hidden = YES;
    self.startButton.hidden = YES;
    [self.cancelButton setTitle:@"Retake" forState:UIControlStateNormal];
    [self.cancelButton setHidden:NO];
    [self.bottomView setHidden:NO];
    [self.startButton setImage:[UIImage imageNamed:@"StartVideo"] forState:UIControlStateNormal];
    // останавливаем таймер видеозаписи
    [self.videoTimer invalidate];
    self.videoTimer = nil;
    
    // Заканчиваем запись в файл видеозаписи
    [MovieFileOutput stopRecording];
}

- (void)updateTime {
    self.elapsedTime += self.videoTimer.timeInterval;
    NSInteger seconds = (NSInteger)self.elapsedTime % 60;
    NSInteger minutes = ((NSInteger)self.elapsedTime / 60) % 60;
    self.timeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
}

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray *)connections
                error:(NSError *)error {
    if (WeAreRecording) {
        [self stopRecording];
    }
    
    BOOL RecordedSuccessfully = YES;
    if ([error code] != noErr) {
        // Если при записи видео произошла ошибка, но файл был успешно сохранен,
        // будем все равно считать, что запись прошла успешно
        id value = [[error userInfo] objectForKey:AVErrorRecordingSuccessfullyFinishedKey];
        if (value != nil) {
            RecordedSuccessfully = [value boolValue];
        }
    }
    if (RecordedSuccessfully) {
        // Если запись прошла успешно, появляется кнопка Use Video
        self.useVideoButton.hidden = NO;
    }
}

- (void)viewDidUnload {
    [super viewDidUnload];
    CaptureSession = nil;
    MovieFileOutput = nil;
    VideoInputDevice = nil;
}
@end


Здесь можно найти исходный код моего проекта и попробовать, как работает приложение.

Ссылки на источники:

  • UIImagePickerController
  • AVFoundation
  • AVCaptureSession
  • AVCaptureDevice
  • AVCaptureDeviceInput
  • AVCaptureMovieFileOutput
  • AVCaptureVideoPreviewLayer
  • Полный список Video Input Presets

Комментарии (1)

  • 20 декабря 2016 в 15:25

    0

    Можно ли ускорить или замедлить скорость видео?
    Можно ли то же самое сделать на Swift 3.0?

    Заранее спасибо.
    Шарик

© Habrahabr.ru