Custom Video Recorder для iOS приложений
Основной объект, который позволит мне вести видеосъемку:
AVCaptureSession // сессия захвата видео с камеры
Кроме того, мне понадобятся:
AVCaptureVideoPreviewLayer // слой, в котором будем показывать видео с камеры в реальном времени
AVCaptureDevice // устройство захвата видео / аудио
AVCaptureDeviceInput // вход видео / аудио для AVCaptureSession
AVCaptureMovieFileOutput // выход AVCaptureSession для записи захваченного видео в файл
Дизайн можно хранить в xib файле или storyboard«е. Используя Autolayout и Constraints в дизайнере можно добиться того, что все панели будут автоматически растягиваться, кнопки выравниваться по центру (левому или правому краю). У нашего VideoRecorderController«а будет три режима работы:
- Готов к съемке: AVCaptureSession запущена, на экране видео с камеры в реальном времени, но запись не идет.
На нижней панели активна кнопка Cancel — отмена съемки, также активна кнопка Start — начало записи, кнопка Use Video скрыта.
На верхней панели показано время записи — 00:00. После нажатия кнопки Cancel у делегата видеосъемки срабатывает метод -(void)videoRecorderDidCancelRecordingVideo. После нажатия кнопки Start переходим в следующий режим.
- Идет съемка: AVCaptureSession запущена, на экране видео с камеры в реальном времени, при этом идет запись видео в файл. На нижней панели вместо кнопки Start появляется кнопка Stop — конец записи, кнопка Cancel скрыта, кнопка Use Video также скрыта. На верхней панели показано текущее время записи — 00:22. После нажатия кнопки Stop запись останавливается, переходим в следующий режим.
- Съемка завершена: AVCaptureSession остановлена, на экране последний кадр отснятого видео, запись видео в файл завершена. По центру экрана появляется кнопка Play Video.
На нижней панели вместо кнопки Cancel появляется кнопка Retake — переснять видео, появляется кнопка Use Video, кнопка Start скрыта.На верхней панели показана длительность видеозаписи — 00:25.
После нажатия кнопки Play Video начнется просмотр отснятого видео с помощью AVPlayer.
После нажатия кнопки Retake возвращаемся в первый режим.
После нажатия кнопки Use Video у делегата видеосъемки срабатывает метод -(void)videoRecorderDidFinishRecording VideoWithOutputURL:(NSURL *)outputURL.
В файле заголовка мне необходимо описать протокол делегата видеосъемки для обработки отмены видеозаписи и успешного завершения видеозаписи.
#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
- таймер и время для индикатора времени записи
#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 — должна быть проверка, что нет файла видеозаписи.
@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.
- (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. Когда срабатывает таймер видеозаписи, обновляется индикатор времени на верхней панели. Метод делегата файла видео записи срабатывает, если размер файла достиг максимально допустимого значения или время записи достигло максимально установленного. В этот момент запись останавливается.
- (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?Заранее спасибо.
Шарик