Custom Video Recorder для iOS додатків

Додаток Камера для 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.
Три режими роботи — екрани

У файлі заголовка мені необхідно описати протокол делегата відеозйомки для обробки скасування відеозапису та успішного завершення відеозапису.

Ось так буде виглядати файл заголовка VideoRecorderController.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AVKit/AVKit.h>

@protocol VideoRecorderDelegate <NSObject>
// метод делегата, спрацьовує при успішному завершенні відеозапису
- (void)videoRecorderDidFinishRecordingVideowithoutputpath:(NSString *)outputPath;
// метод делегата, спрацьовує при скасуванні відеозапису
- (void)videoRecorderDidCancelRecordingVideo;
@end

@interface VideoRecorderController : UIViewController
@property (nonatomic, retain) NSString *outputPath; // шлях до файлу відеозапису
@property (nonatomic, assign) id<VideoRecorderDelegate> представник;
@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 () <AVCaptureFileOutputRecordingDelegate>
{
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<UIViewControllerTransitionCoordinator>)coordinator {
[self setupLayoutInRect:CGRectMake(0, 0, size.width, size.height)];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorcontext> context) { 
} completion:^(id<UIViewControllerTransitionCoordinatorcontext> 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.представник 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.представник 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


Тут можна знайти вихідний код мого проекту і спробувати, як працює додаток.

Посилання на джерела:

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.