본문 바로가기
ios 뽀개기/objective-c

AVCaptureVideoDataOutput을 이용해서 카메라 만들기 3 - 녹화

by 인생여희 2019. 4. 17.
반응형

AVCaptureVideoDataOutput을 이용해서 카메라 만들기 3 - 녹화

로직 작성 순서는 아래와 같다

//녹화 step1
//IDCaptureSessionPipelineViewController클래스 -  녹화,정지 토글 버튼 기능

- (IBAction)toggleRecording:(id)sender
{
    NSLog(@"IDCaptureSessionPipelineViewController - toggleRecording 버튼");
    
    if (_recording) {
        [_captureSessionCoordinator stopRecording];
        
    } else {
        
        //잠자기 모드 비활성화
        [UIApplication sharedApplication].idleTimerDisabled = YES;
        //아직 세션이 완전히 시작된것이 아니기 때문에 일단 비활성화.
        //IDCaptureSessionCoordinatorDelegate methods 인 - (void)coordinatorDidBeginRecording 에서 다시 활성화 해준다.
        self.recordButton.enabled = NO;
        self.recordButton.title = @"정지";
        [self.captureSessionCoordinator startRecording];
        _recording = YES;
    }
}
//녹화 step2
//IDCaptureSessionPipelineViewController클래스 - 녹화가 시작되면 버튼 활성화

#pragma mark = IDCaptureSessionCoordinatorDelegate methods
- (void)coordinatorDidBeginRecording:(IDCaptureSessionCoordinator *)coordinator
{
    NSLog(@"IDCaptureSessionPipelineViewController coordinatorDidBeginRecording 호출");
    _recordButton.enabled = YES;

}



//녹화 step3
//IDCaptureSessionAssetWriterCoordinator 클래스
#pragma mark - Recording State Machine
// call under @synchonized( self ) // 레코딩 상태 변경해주기!
- (void)transitionToRecordingStatus:(RecordingStatus)newStatus error:(NSError *)error
{
    RecordingStatus oldStatus = _recordingStatus; //제일 처음에는 0
    _recordingStatus = newStatus;                           // 처음에 1을 넘겨 받는다.
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - transitionToRecordingStatus 진입 : oldStatus : %ld , newStatus : %ld" , (long)oldStatus , (long)newStatus);
    //녹화버튼 클릭
    // oldStatus : 0, newStatus: 1
    //oldStatus : 1, newStatus: 2       //  녹화실행
    
    //정지 버튼 클릭
    //oldStatus : 2, newStatus: 3
    //oldStatus : 3, newStatus: 0
    

    if(newStatus != oldStatus){
      
        //newStatus 가 0, 멈춤 상태일때
        if (error && (newStatus == RecordingStatusIdle)) {
            NSLog(@"IDCaptureSessionAssetWriterCoordinator - 녹화 멈춤 상태");
            
        } else {
            
            //멈춤상태가 아니면
            error = nil;
            //RecordingStatusStartingRecording = 1,    RecordingStatusRecording = 2
            if (oldStatus == RecordingStatusStartingRecording && newStatus == RecordingStatusRecording) {
                //녹화 버튼을 -> 정지 한 뒤 -> 활성화 시켜준다.
                //delegateCallbackQueue는 부모 클래스인 IDCaptureSessionCoordinator에서 가지고 있고
                //IDCaptureSessionPipelineViewController 클래스에서 dispatch_get_main_queue()를 이미 넣어주었다.
                dispatch_async(self.delegateCallbackQueue, ^{
                    @autoreleasepool{
                        
                        [self.delegate coordinatorDidBeginRecording:self];
                    }
                });
                
            } else if(oldStatus == RecordingStatusStoppingRecording && newStatus == RecordingStatusIdle){
                NSLog(@"IDCaptureSessionAssetWriterCoordinator - 녹화 정지 상태");
            }
            
        }
    }
}



//녹화 step4

//IDAssetWriterCoordinator 클래스

-(instancetype)initWithURL:(NSURL *)URL
{
    
    if (!URL) {
        return nil;
    }
    
    self = [super init];
    if (self) {
         NSLog(@"IDAssetWriterCoordinator - initWithURL 진입");
        _writingQueue = dispatch_queue_create("com.example.assetwriter.writing", DISPATCH_QUEUE_SERIAL);
        _videoTrackTransform = CGAffineTransformMakeRotation(M_PI_2); // 세로 모드
        _URL = URL;
    }
    return self;
}



//녹화 step5

//IDAssetWriterCoordinator 클래스

//IDCaptureSessionAssetWriterCoordinator 클래스에서 넘겨준 오디오 관련 CMFormatDescriptionRef(output), NSDictionary 값 변수에 셋팅
- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings
{
     NSLog(@"IDAssetWriterCoordinator - addAudioTrackWithSourceFormatDescription 진입");
    //formatDescription 가 없다면
    if ( formatDescription == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
        return;
    }
    
    @synchronized(self)
    {
    
        //WriterStatusIdle = 0
        if (_status != WriterStatusIdle) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if ( _audioTrackSourceFormatDescription ) {
            NSLog(@"IDAssetWriterCoordinator - Cannot add more than one audio track");
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one audio track" userInfo:nil];
            return;
        }
        _audioTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
        _audioTrackSettings = [audioSettings copy];
    }
}



//녹화 step6
//IDAssetWriterCoordinator 클래스

//IDCaptureSessionAssetWriterCoordinator 클래스에서 넘겨준 오디오 관련 CMFormatDescriptionRef(output), NSDictionary 값 변수에 셋팅
- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)videoSettings
{
    NSLog(@"IDAssetWriterCoordinator - addVideoTrackWithSourceFormatDescription 진입");
    if ( formatDescription == NULL ){
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
        return;
    }
    @synchronized( self )
    {
        if (_status != WriterStatusIdle){
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if(_videoTrackSourceFormatDescription ){
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one video track" userInfo:nil];
            return;
        }
        
        _videoTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
        _videoTrackSettings = [videoSettings copy];
    }
}

//녹화 step7
//IDAssetWriterCoordinator 클래스
- (void)setDelegate:(id<IDAssetWriterCoordinatorDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue
{
     NSLog(@"IDAssetWriterCoordinator - setDelegate <IDAssetWriterCoordinatorDelegate> 진입");
    NSLog(@"IDAssetWriterCoordinator - setDelegate 진입");
    if ( delegate && ( delegateCallbackQueue == NULL ) ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Caller must provide a delegateCallbackQueue" userInfo:nil];
    }
    
    @synchronized( self )
    {
        _delegate = delegate;
        if ( delegateCallbackQueue != _delegateCallbackQueue  ) {
            _delegateCallbackQueue = delegateCallbackQueue;
        }
    }
}

//녹화 step8
//IDCaptureSessionAssetWriterCoordinator 클래스
#pragma mark - IDAssetWriterCoordinatorDelegate methods

//녹화 시작 버튼 클릭했을때 - 쓰기 준비!!
- (void)writerCoordinatorDidFinishPreparing:(IDAssetWriterCoordinator *)coordinator
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator -  writerCoordinatorDidFinishPreparing 진입 :");
    @synchronized(self)
    {
        if(_recordingStatus != RecordingStatusStartingRecording){
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Expected to be in StartingRecording state" userInfo:nil];
            return;
        }
        // 2
        [self transitionToRecordingStatus:RecordingStatusRecording error:nil];
    }
}

//녹화 step9
//IDCaptureSessionAssetWriterCoordinator 클래스
//녹화 시작 후 쓰기중에 오류발생 했을때!!
- (void)writerCoordinator:(IDAssetWriterCoordinator *)recorder didFailWithError:(NSError *)error
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator -  didFailWithError 진입 :");
    @synchronized( self ) {
        self.assetWriterCoordinator = nil;
        [self transitionToRecordingStatus:RecordingStatusIdle error:error];
    }
}



//녹화 step10
//IDAssetWriterCoordinator 클래스
// call under @synchonized( self )
- (void)transitionToStatus:(WriterStatus)newStatus error:(NSError *)error
{
    NSLog(@"IDAssetWriterCoordinator - transitionToStatus 진입");
     BOOL shouldNotifyDelegate = NO;
    
    // 처음에 준비할때 는 1
    
    if (newStatus != _status) {
        
        if((newStatus == WriterStatusFinished) || (newStatus == WriterStatusFailed)){
            
            shouldNotifyDelegate = YES;
            //asset writer과 인풋을 해체하기 전에 , 더 이상 샘플 버퍼가 없는지 확인!
            
            dispatch_async(_writingQueue, ^{
                self->_assetWriter = nil;
                self->_videoInput = nil;
                self -> _audioInput = nil;
                
                //상태가 실패면 임시 저장 파일에서 삭제
                if (newStatus == WriterStatusFailed) {
                    [[NSFileManager defaultManager] removeItemAtURL:self->_URL error:NULL];
                }
            });//dispatch_async end -
            
        }else if(newStatus == WriterStatusRecording){   //WriterStatusRecording = 2
            //녹화상태
            shouldNotifyDelegate = YES;
        }
        
        _status = newStatus;
    
    }
   
    
    if (shouldNotifyDelegate && self.delegate) { // delegate =  IDAssetWriterCoordinatorDelegate
        
        //IDCaptureSessionAssetWriterCoordinator 클래스에서 콜백큐 넣어준다.
        dispatch_async(_delegateCallbackQueue, ^{
            
            
            @autoreleasepool
            {
                switch (newStatus) {
                    case WriterStatusRecording:             //2 - 쓰기 준비
                        [self.delegate writerCoordinatorDidFinishPreparing:self];
                        break;
                    case WriterStatusFinished:              //5 -  쓰기 종료
                        [self.delegate writerCoordinatorDidFinishRecording:self];
                        break;
                    case WriterStatusFailed:                 //6
                        [self.delegate writerCoordinator:self didFailWithError:error];
                        break;
                        
                    default:
                        break;
                }
            }
            
        });
        
    }
}



//녹화 step11 - (NSDictionary)videoSettings 이 null 일때 호출됨
//IDAssetWriterCoordinator 클래스
- (NSDictionary *)fallbackVideoSettingsForSourceFormatDescription:(CMFormatDescriptionRef)videoFormatDescription
{
    NSLog(@"IDAssetWriterCoordinator - fallbackVideoSettingsForSourceFormatDescription 진입");
    
    float bitsPerPixel;
    CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(videoFormatDescription);
    int numPixels = dimensions.width * dimensions.height;
    int bitsPerSecond;
    
    NSLog(@"비디오 셋팅이 제공되지 않아, 기본값으로 셋팅합니다.....");
    
    //SD보다 낮은 해상도는 스트리밍을위한 것이고 낮은 비트 전송률을 사용한다고 가정한다.
    if (numPixels < (640 * 480)) {
        bitsPerPixel = 4.05;            //이 비트율은 AVCaptureSessionPresetMedium 또는 Low로 생성되는 퀄리티와 거의 일치한다.
    }
    else{
        bitsPerPixel = 10.1;            //이 비트 전송률은 AVCaptureSessionPresetHigh에서 생성되는 품질과 거의 일치한다.
    }
    
    bitsPerSecond = numPixels * bitsPerPixel;
    
    NSDictionary *compressionProperties = @{AVVideoAverageBitRateKey : @(bitsPerSecond),
                                            AVVideoExpectedSourceFrameRateKey : @(30),
                                            AVVideoMaxKeyFrameIntervalKey : @(30) };
    
    return @{
             AVVideoCodecKey : AVVideoCodecTypeH264 ,
             AVVideoWidthKey : @(dimensions.width),
             AVVideoHeightKey : @(dimensions.height),
             AVVideoCompressionPropertiesKey : compressionProperties
             };
}



//녹화 step11 - 2 (NSDictionary)videoSettings 이 null 일때 호출됨
//IDAssetWriterCoordinator 클래스
- (NSError *)cannotSetupInputError
{
    NSLog(@"IDAssetWriterCoordinator - cannotSetupInputError 진입");
    

    NSString *localizedDescription = NSLocalizedString( @"녹화가 시작되지 못했습니다.", nil );
    NSString *localizedFailureReason = NSLocalizedString( @"asset writer input 을 셋팅할 수 없습니다.....", nil );
    NSDictionary *errorDict = @{ NSLocalizedDescriptionKey : localizedDescription,
                                 NSLocalizedFailureReasonErrorKey : localizedFailureReason };
    return [NSError errorWithDomain:@"com.example" code:0 userInfo:errorDict];
}




//녹화 step12
//video input(AVAssetWriterInput) 생성 , assetWriter에 videoInput 추가
//IDAssetWriterCoordinator 클래스
- (BOOL)setupAssetWriterVideoInputWithSourceFormatDescription:(CMFormatDescriptionRef)videoFormatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings error:(NSError **)errorOut
{
    NSLog(@"IDAssetWriterCoordinator - setupAssetWriterVideoInputWithSourceFormatDescription 진입");
    
    //비디오 셋팅이 존재하지 않으면 defalt 값을 만들어서 할당해 준다.
    if (!videoSettings) {
        videoSettings = [self fallbackVideoSettingsForSourceFormatDescription:videoFormatDescription];
    }
    
    //AVAssetWriter가 [비디오 셋팅값]을 지원한다면
    if ([_assetWriter canApplyOutputSettings:videoSettings forMediaType:AVMediaTypeVideo]) {
        
        //AVAssetWriterInput 에 [비디오 셋팅]과 [비디오 포멧 설명]을 넣고 초기화, 생성 해준다.
        _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings sourceFormatHint:videoFormatDescription];
        _videoInput.expectsMediaDataInRealTime = YES;
        _videoInput.transform = transform;
        
        //AVAssetWriter 에 AVAssetWriterInput을 넣어준다.
        if ([_assetWriter canAddInput:_videoInput]) {
            [_assetWriter addInput:_videoInput];
        }else{
            
            if (errorOut) {
                *errorOut = [self cannotSetupInputError];
            }
             return NO;
        }
        
    }else{
        if (errorOut) {
            *errorOut = [self cannotSetupInputError];
        }
        return NO;
    }
    
    return YES;
}



//녹화 step12 - 2
//audio input(AVAssetWriterInput) 생성 , assetWriter에 videoInput 추가
//IDAssetWriterCoordinator 클래스
- (BOOL)setupAssetWriterAudioInputWithSourceFormatDescription:(CMFormatDescriptionRef)audioFormatDescription settings:(NSDictionary *)audioSettings error:(NSError **)errorOut
{
    NSLog(@"IDAssetWriterCoordinator - setupAssetWriterAudioInputWithSourceFormatDescription 진입");
    
    //audioSettings이 존재하지 않다면..
    if (!audioSettings) {
        audioSettings = @{AVFormatIDKey : @(kAudioFormatMPEG4AAC)};
    }
    
    //assetWriter 가 [오디오 셋팅]을 지원하는지 확인
    if ([_assetWriter canApplyOutputSettings:audioSettings forMediaType:AVMediaTypeAudio]) {
        _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings sourceFormatHint:audioFormatDescription];
        _audioInput.expectsMediaDataInRealTime = YES;
        
        //assetWriter 에 [오디오 인풋] 삽입
        if ([_assetWriter canAddInput:_audioInput]) {
            [_assetWriter addInput:_audioInput];
        }else {
            if (errorOut ) {
                *errorOut = [self cannotSetupInputError];
            }
            return NO;
        }
        
    }else
    {
        if (errorOut) {
            *errorOut = [self cannotSetupInputError];
        }
        return NO;
    }
    
    
    return YES;
}



//녹화 step13
- (void)prepareToRecord
{
     NSLog(@"IDAssetWriterCoordinator - prepareToRecord 진입");
    
    @synchronized( self )
    {
        if (_status != WriterStatusIdle){   //WriterStatusIdle = 0
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"예외: 이미 준비가 완료되었습니다, 다시 준비 할 수 없습니다." userInfo:nil];
            return;
        }
                        //  1
        [self transitionToStatus:WriterStatusPreparingToRecord error:nil];
    }
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        
        NSError *error = nil;
         // AVAssetWriter 은 이미 존재하고 있는 파일 경로에 덮어쓰기 할 수 없다..
        [[NSFileManager defaultManager] removeItemAtURL:self->_URL error:NULL];
        
        //assetWriter 초기화
        self->_assetWriter = [[AVAssetWriter alloc] initWithURL:self->_URL fileType:AVFileTypeQuickTimeMovie error:&error];
        
        //  assetWriter 생성하고  Writeinputs 넣기
        if (!error && self->_videoTrackSourceFormatDescription ) {
            
            [self setupAssetWriterVideoInputWithSourceFormatDescription:self->_videoTrackSourceFormatDescription transform:self->_videoTrackTransform settings:self->_videoTrackSettings error:&error];
            
        }
         //  assetWriter 생성하고  Writeinputs 넣기
        if (!error && self->_audioTrackSourceFormatDescription ) {
            [self setupAssetWriterAudioInputWithSourceFormatDescription:self->_audioTrackSourceFormatDescription settings:self->_audioTrackSettings error:&error];
        }
        
        //AVAssetWriter 시작!
        if (!error) {
            BOOL success = [self -> _assetWriter startWriting];
            if (!success) {
                error = self -> _assetWriter.error;
            }
        }
        
        @synchronized(self)
        {
            
            if (error) {
                //6번
                [self transitionToStatus:WriterStatusFailed error:error];
            }else{
                
                // 2 = 레코딩 시작 상태로 변경
                [self transitionToStatus:WriterStatusRecording error:nil];
                
            }
            
        }
        
        
    });
}



//녹화 step14
#pragma mark - Recording
//IDCaptureSessionAssetWriterCoordinator 클래스 (자식)
- (void)startRecording
{
   
    @synchronized(self)
    {
    
    // 처음 상태 값 : _recordingStatus : 0 ,  RecordingStatusIdle = 0
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - startRecording 진입 : _recordingStatus : %ld , RecordingStatusIdle : %ld" , (long)_recordingStatus , (long)RecordingStatusIdle);
    
    //값이 다르면 이미 녹화가 되고 있다
    if (_recordingStatus != RecordingStatusIdle) {
        @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"이미 녹화되고 있습니다." userInfo:nil];
        return;
    }
    
    //  //레코딩 상태 변경해주기,  RecordingStatusStartingRecording = 1 값을 가지고 있다.
    [self transitionToRecordingStatus:RecordingStatusStartingRecording error:nil];
    
    }//@synchronized end
    
    //임시 저장 경로 얻기
    IDFileManager *fm = [IDFileManager new];
    _recordingURL = [fm tempFileURL];
    
    //레코딩 임시저장 경로 셋팅
    self.assetWriterCoordinator = [[IDAssetWriterCoordinator alloc] initWithURL:_recordingURL];
    
    // outputAudioFormatDescription 값이 존재한다면 ,  참고 : didOutputSampleBuffer 에서 할당 해줬음
    if (_outputAudioFormatDescription != nil) {
        
        [_assetWriterCoordinator addAudioTrackWithSourceFormatDescription:self.outputAudioFormatDescription settings:_audioCompressionSettings];
    
    }
    
      [_assetWriterCoordinator addVideoTrackWithSourceFormatDescription:self.outputVideoFormatDescription settings:_videoCompressionSettings];
    
    //writercallback  큐 생성
    dispatch_queue_t callbackQueue = dispatch_queue_create("com.example.capturesession.writercallback", DISPATCH_QUEUE_SERIAL);
    [_assetWriterCoordinator setDelegate:self callbackQueue:callbackQueue];
    
    //비동기식이면 recorderDidFinishPreparing : 또는 recorder : didFailWithError :를 호출하면 다시 호출됨.
      [_assetWriterCoordinator prepareToRecord];
}


//녹화 step15 -2
//IDAssetWriterCoordinator 클래스
//최종적으로 버퍼 더하기
- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer ofMediaType:(NSString *)mediaType
{
    NSLog(@"IDAssetWriterCoordinator - appendSampleBuffer 진입");
   
    //버퍼 값 유무 체크
    if (sampleBuffer == NULL) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"sample 버퍼가 비었어요.." userInfo:nil];
        return;
    }
    // 녹화 준비 체크
    @synchronized(self){
        if (_status < WriterStatusRecording) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"녹화 준비가 안됐습니다.." userInfo:nil];
            return;
        }
    }
    
    
    CFRetain(sampleBuffer);
    
    dispatch_async(_writingQueue, ^{
        
        @autoreleasepool
        {
            
            @synchronized(self)
            {
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when samples are appended and we are no longer recording.
                // Instead of throwing an exception we just release the sample buffers and return.
                //클라이언트 관점에서 보면, 무비 레코더는 버퍼 추가의 결과로 비동기 적으로 오류 상태로 전환 될 수 있다..
                //이 때문에 샘플이 추가되고 더 이상 녹음하지 않을 때  오류 관리에 허술해질 수 있다.
                // 예외를 던지는 대신 샘플 버퍼를 해제하고 반환한다.
                
               
                if (self->_status  > WriterStatusFinishingRecordingPart1) {      //버퍼가 추가되기를 기다리는 중 3번 보다 클때
                    CFRelease(sampleBuffer);
                    return ;
                }

            } //@synchronized end /
            
            
            if (!self->_haveStartedSession && mediaType == AVMediaTypeVideo) {
                //기록!
                [self->_assetWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
                self -> _haveStartedSession = YES;
            }
            
            AVAssetWriterInput *input = (mediaType == AVMediaTypeVideo) ? self-> _videoInput : self -> _audioInput;
            
            if (input.readyForMoreMediaData) {
               //************input 준비하고 데이터 버퍼 더하기****************!!!
                
                BOOL success = [input appendSampleBuffer:sampleBuffer];
                if (!success) {
                    NSError *error = self->_assetWriter.error;
                    @synchronized(self){
                        NSLog(@"버퍼 기록 중 WriterStatusFailed");
                        [self transitionToStatus:WriterStatusFailed error:error];
                    }
                }
                

            }else{
                 NSLog( @"%@ 미디어 데이터를 위한 인풋이 준비되지 않았습니다.., dropping buffer", mediaType );
            }
            
            //버퍼 비우기?
            CFRelease(sampleBuffer);
            
        }//@autoreleasepool end /
    });
}



//녹화 step16 -1
//IDAssetWriterCoordinator 클래스
- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
     NSLog(@"IDAssetWriterCoordinator - appendVideoSampleBuffer 진입");
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
}

//녹화 step16 -2
//IDAssetWriterCoordinator 클래스
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
     NSLog(@"IDAssetWriterCoordinator - appendAudioSampleBuffer 진입");
     [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio];
}

- (void)finishRecording
{
     NSLog(@"IDAssetWriterCoordinator - finishRecording 진입");
}



#pragma mark - SampleBufferDelegate methods
////녹화 step17
//IDCaptureSessionAssetWriterCoordinator 클래스
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :");
    
    CMFormatDescriptionRef formatDescription =  CMSampleBufferGetFormatDescription(sampleBuffer);
    
    
    if (connection == _videoConnection) {
        NSLog(@"IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection");
        
        if (self.outputVideoFormatDescription == nil) {
            NSLog(@"IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription == nil");
            
            // Don't render the first sample buffer.
            // This gives us one frame interval (33ms at 30fps) for setupVideoPipelineWithInputFormatDescription: to complete.
            // Ideally this would be done asynchronously to ensure frames don't back up on slower devices.
            
            //TODO: outputVideoFormatDescription should be updated whenever video configuration is changed (frame rate, etc.)
            //Currently we don't use the outputVideoFormatDescription in IDAssetWriterRecoredSession
            
            /*
             
             첫 번째 샘플 버퍼를 렌더링하지 마십시오.
             이 작업을 완료하려면 setupVideoPipelineWithInputFormatDescription :에 대해 하나의 프레임 간격 (30fps에서 33ms)이 필요합니다.
             이상적으로 이것은 느린 장치에서 프레임이 백업되지 않도록 비동기 적으로 수행됩니다.
             
             TODO : 비디오 구성이 변경 될 때마다 outputVideoFormatDescription을 업데이트해야합니다 (프레임 속도 등)
             현재 우리는 IDAssetWriterRecoredSession에서 outputVideoFormatDescription을 사용하지 않습니다
             */
            
            [self setupVideoPipelineWithInputFormatDescription:formatDescription];
            
            
        }else{
            NSLog(@"IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil");
            self.outputVideoFormatDescription = formatDescription;
            @synchronized(self){
                
                //2 이면
                if(_recordingStatus == RecordingStatusRecording){
                    
                    NSLog(@"IDCaptureSessionAssetWriterCoordinator - [_assetWriterCoordinator appendVideoSampleBuffer:sampleBuffer]  호출 :");
                    //버퍼 붙이기 *****
                    [_assetWriterCoordinator appendVideoSampleBuffer:sampleBuffer];
                }
                
            }
            
        }
        
        
    }else if(connection == _audioConnection){
        
       NSLog(@"IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection");
        self.outputAudioFormatDescription = formatDescription;
        @synchronized( self ) {
             //2 면
            if(_recordingStatus == RecordingStatusRecording){
                //버퍼 붙이기 *****
                [_assetWriterCoordinator appendAudioSampleBuffer:sampleBuffer];
            }
        }
        
    }
    
}


////녹화 step18
//IDCaptureSessionAssetWriterCoordinator 클래스
//자식
- (void)startRecording
{
   
    @synchronized(self)
    {
    
    // 처음 상태 값 : _recordingStatus : 0 ,  RecordingStatusIdle = 0
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - startRecording 진입 : _recordingStatus : %ld , RecordingStatusIdle : %ld" , (long)_recordingStatus , (long)RecordingStatusIdle);
    
    //값이 다르면 이미 녹화가 되고 있다
    if (_recordingStatus != RecordingStatusIdle) {
        @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"이미 녹화되고 있습니다." userInfo:nil];
        return;
    }
    
    //  //레코딩 상태 변경해주기,  RecordingStatusStartingRecording = 1 값을 가지고 있다.
    [self transitionToRecordingStatus:RecordingStatusStartingRecording error:nil];
    
    }//@synchronized end
    
    //임시 저장 경로 얻기
    IDFileManager *fm = [IDFileManager new];
    _recordingURL = [fm tempFileURL];
    
    //레코딩 임시저장 경로 셋팅
    self.assetWriterCoordinator = [[IDAssetWriterCoordinator alloc] initWithURL:_recordingURL];
    
    // outputAudioFormatDescription 값이 존재한다면 ,  참고 : didOutputSampleBuffer 에서 할당 해줬음
    if (_outputAudioFormatDescription != nil) {
        
        [_assetWriterCoordinator addAudioTrackWithSourceFormatDescription:self.outputAudioFormatDescription settings:_audioCompressionSettings];
    
    }
    
      [_assetWriterCoordinator addVideoTrackWithSourceFormatDescription:self.outputVideoFormatDescription settings:_videoCompressionSettings];
    
    //writercallback  큐 생성
    dispatch_queue_t callbackQueue = dispatch_queue_create("com.example.capturesession.writercallback", DISPATCH_QUEUE_SERIAL);
    [_assetWriterCoordinator setDelegate:self callbackQueue:callbackQueue];
    
    //비동기식이면 recorderDidFinishPreparing : 또는 recorder : didFailWithError :를 호출하면 다시 호출됨.
      [_assetWriterCoordinator prepareToRecord];
}




////녹화 step19
//IDCaptureSessionPipelineViewController 클래스
- (IBAction)toggleRecording:(id)sender
{
    NSLog(@"IDCaptureSessionPipelineViewController - toggleRecording 버튼");
    
    if (_recording) {
        [_captureSessionCoordinator stopRecording];
        
    } else {
        
        //잠자기 모드 비활성화
        [UIApplication sharedApplication].idleTimerDisabled = YES;
        //아직 세션이 완전히 시작된것이 아니기 때문에 일단 비활성화.
        //IDCaptureSessionCoordinatorDelegate methods 인 - (void)coordinatorDidBeginRecording 에서 다시 활성화 해준다.
        self.recordButton.enabled = NO;
        self.recordButton.title = @"정지";
        [self.captureSessionCoordinator startRecording];
        _recording = YES;
    }
}

 

 

 

 

 

 

 

 

 

 

반응형

댓글