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;
    }
}

 

 

 

 

 

 

 

 

 

 

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

 

#.지난번 포스팅에 이어서 '녹화' 버튼을 클릭했을때 작동되는 로직 순서를 알아보자.

1.녹화버튼을 클릭 하면 IDCaptureSessionPipelineViewController클래스의 toggleRecording 메소드가 작동된다.

2.IDCaptureSessionAssetWriterCoordinator 클래스의 startRecording 진입해서 레코딩 상태변화를 시키기위해 transitionToRecordingStatus 메소드를 호출한다. // oldStatus : 0, newStatus: 1 

3.IDCaptureSessionAssetWriterCoordinator 클래스의 startRecording 에서 계속해서 임시저장 경로를 얻고 IDAssetWriterCoordinator를 초기화 해준다.

4.IDAssetWriterCoordinator 객체가 생성되고 쓰기 작업을 위한 writing queue 와 비디오 방향, url 값이 셋팅된다.

5.그후 outputAudioFormatDescription 값이 존재한다면( 참고 : didOutputSampleBuffer 에서 할당 해줬음) IDAssetWriterCoordinator  클래스의 addAudioTrackWithSourceFormatDescription 메소드에

오디오(비디오) 관련 CMFormatDescriptionRef(output), NSDictionary셋팅값을 변수에 셋팅을 해준다.

6.그후 IDAssetWriterCoordinator 객체에 callback queue 와 setDelegate 를 해준다. (Self = IDCaptureSessionAssetWriterCoordinator)

7.assetWriterCoordinator 객체에 녹화준비를 위한 메소드를 호출한다. (prepareToRecord)

8.IDAssetWriterCoordinator클래스의 prepareToRecord 메소드에서 transitionToStatus 메소드에 1을 넣어준다.

—중간중간 IDCaptureSessionAssetWriterCoordinator 클래스의 didOutputSampleBuffer 메소드 호출(아직 쓸수없는 상황)—

9.IDAssetWriterCoordinator클래스에서 AssetWriter 초기화 시켜준다.(url 이용)

10.setupAssetWriterVideoInputWithSourceFormatDescription 메소드를 이용해서 AVAssetWriter가 [비디오 셋팅값]을 지원한다면 AVAssetWriterInput 에 [비디오 셋팅]과 [비디오 포멧 설명]을 넣고 초기화, 생성 해준다 그리고 AVAssetWriter 에 AVAssetWriterInput을 넣어준다.

11.setupAssetWriterAudioInputWithSourceFormatDescription 메소드를 이용해서 assetWriter 가 [오디오 셋팅]을 지원하는지 확인 후 , assetWriter 에 [오디오 인풋] 삽입한다.

12.AVAssetWriter 시작! [assetWriter startWriting]

13. transitionToStatus 에 2를 넘겨준다.

14.델리게이트 메소드인 writerCoordinatorDidFinishPreparing를 호출하고 거기서 transitionToRecordingStatus에 2를 넣고 호출 해준다.  // oldStatus : 1, newStatus: 2

15.그럼 transitionToRecordingStatus메소드에서 coordinatorDidBeginRecording 메소드를 이용해서 비활성화 되었던 정지버튼을 활성화 시켜준다!! (여기까지가 쓰기 준비!!)

16.그후 IDCaptureSessionAssetWriterCoordinator클래스에서  [_assetWriterCoordinator appendVideoSampleBuffer:sampleBuffer];를 호출

17.IDAssetWriterCoordinator클래스에서 AVAssetWriterInput(비디오, 오디오) 객체 초기화 한뒤, 이 인풋에 샘플 버퍼를 넣어준다. [input appendSampleBuffer:sampleBuffer];

(계속 반복……정지 버튼 누르기 전까지)

 

#.로그

[카메라 실행!]

IDCaptureSessionPipelineViewController setupWithPipelineMode IDCaptureSessionAssetWriterCoordinator 객체 생성시작!

2019-04-17 13:51:38.958716+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator init 진입

2019-04-17 13:51:38.958762+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator setupCaptureSession 진입 

2019-04-17 13:51:38.962791+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addDefaultCameraInputToCaptureSession 진입

2019-04-17 13:51:38.965839+0900 AssertWriterVideoRecorderNew[697:119055] [MC] System group container for systemgroup.com.apple.configurationprofiles path is /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles

2019-04-17 13:51:38.966153+0900 AssertWriterVideoRecorderNew[697:119055] [MC] Reading from public effective user settings.

2019-04-17 13:51:38.975087+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addInput toCaptureSession 진입

2019-04-17 13:51:38.977239+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addInput toCaptureSession - 세션에 캡쳐디바이스인풋 들어갔어요!

2019-04-17 13:51:38.977288+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addDefaultMicInputToCaptureSession 진입

2019-04-17 13:51:38.991662+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addInput toCaptureSession 진입

2019-04-17 13:51:38.993057+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addInput toCaptureSession - 세션에 캡쳐디바이스인풋 들어갔어요!

2019-04-17 13:51:38.993111+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionAssetWriterCoordinator - init 진입

2019-04-17 13:51:38.993158+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionAssetWriterCoordinator - addDataOutputsToCaptureSession 진입 : 비디오, 오디오 아웃풋 객체 생성 + 설정

2019-04-17 13:51:38.993419+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addOutput  toCaptureSession 진입

2019-04-17 13:51:38.994996+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator addOutput  toCaptureSession 진입

2019-04-17 13:51:38.995910+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionAssetWriterCoordinator - setCompressionSettings 진입 :

2019-04-17 13:51:38.996646+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator setDelegate 진입 - 전역변수에 IDCaptureSessionCoordinatorDelegate 할당, delegateCallbackQueue 할당

2019-04-17 13:51:38.996694+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionPipelineViewController - configureInterface 호출 -  AVCaptureVideoPreviewLayer 객체 생성

2019-04-17 13:51:38.996711+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator previewLayer 캡쳐 세션 넣어줌 초기화

2019-04-17 13:51:38.998285+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionCoordinator startRunning 진입 - dispatch_sync(세션큐, 캡쳐쎄션 스타트러닝!)

2019-04-17 13:51:39.192085+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:39.192150+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:39.192170+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription == nil

2019-04-17 13:51:39.192198+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - setupVideoPipelineWithInputFormatDescription 진입 :

2019-04-17 13:51:39.230852+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:39.236021+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:39.236830+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:39.236855+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:39.236869+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:39.238669+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:39.238712+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:39.247593+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:39.247652+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:39.247666+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

 

[녹화 버튼 클릭!]

 

2019-04-17 13:51:41.199519+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionPipelineViewController - toggleRecording 버튼

2019-04-17 13:51:41.202423+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionAssetWriterCoordinator - startRecording 진입 : _recordingStatus : 0 , RecordingStatusIdle : 0

2019-04-17 13:51:41.202525+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionAssetWriterCoordinator - transitionToRecordingStatus 진입 : oldStatus : 0 , newStatus : 1

2019-04-17 13:51:41.203757+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - initWithURL 진입

2019-04-17 13:51:41.203879+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - addAudioTrackWithSourceFormatDescription 진입

2019-04-17 13:51:41.203994+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - addVideoTrackWithSourceFormatDescription 진입

2019-04-17 13:51:41.204057+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - setDelegate <IDAssetWriterCoordinatorDelegate> 진입

2019-04-17 13:51:41.204103+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - setDelegate 진입

2019-04-17 13:51:41.204151+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - prepareToRecord 진입

2019-04-17 13:51:41.204196+0900 AssertWriterVideoRecorderNew[697:119055] IDAssetWriterCoordinator - transitionToStatus 진입

2019-04-17 13:51:41.210955+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.211035+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.217960+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.218091+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.218175+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.228418+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - setupAssetWriterVideoInputWithSourceFormatDescription 진입

2019-04-17 13:51:41.231649+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.231708+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.248734+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - setupAssetWriterAudioInputWithSourceFormatDescription 진입

2019-04-17 13:51:41.249014+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.249074+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.249095+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.255088+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.255149+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.278486+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.278900+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.297037+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.297126+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.297158+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.315903+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.315981+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.317641+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.317694+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.317750+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.337010+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.337086+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.360373+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.360462+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.360490+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.361846+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.361914+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.371072+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.371145+0900 AssertWriterVideoRecorderNew[697:119073] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.380267+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.380426+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.380448+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.385946+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - transitionToStatus 진입

2019-04-17 13:51:41.386317+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator -  writerCoordinatorDidFinishPreparing 진입 :

2019-04-17 13:51:41.386380+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - transitionToRecordingStatus 진입 : oldStatus : 1 , newStatus : 2

2019-04-17 13:51:41.386578+0900 AssertWriterVideoRecorderNew[697:119055] IDCaptureSessionPipelineViewController coordinatorDidBeginRecording 호출

!!!!!!!- <본격적으로 쓰기에 버퍼 넣기 시작> !!!!!!!!

2019-04-17 13:51:41.394184+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.394251+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.394326+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendAudioSampleBuffer 진입

2019-04-17 13:51:41.394365+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendSampleBuffer 진입

2019-04-17 13:51:41.394609+0900 AssertWriterVideoRecorderNew[697:119069] input 준비하고 데이터 버퍼 더하기

 

2019-04-17 13:51:41.412497+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.412649+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.412677+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.412720+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - [_assetWriterCoordinator append비디오SampleBuffer:sampleBuffer]  호출 :

2019-04-17 13:51:41.412764+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendVideoSampleBuffer 진입

2019-04-17 13:51:41.412789+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendSampleBuffer 진입

2019-04-17 13:51:41.412881+0900 AssertWriterVideoRecorderNew[697:119069] !self->_haveStartedSession && mediaType == AVMediaTypeVideo

2019-04-17 13:51:41.413553+0900 AssertWriterVideoRecorderNew[697:119069] input 준비하고 데이터 버퍼 더하기

 

2019-04-17 13:51:41.417366+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.417422+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.417450+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendAudioSampleBuffer 진입

2019-04-17 13:51:41.417474+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendSampleBuffer 진입

2019-04-17 13:51:41.417550+0900 AssertWriterVideoRecorderNew[697:119069] input 준비하고 데이터 버퍼 더하기

 

2019-04-17 13:51:41.443026+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.443116+0900 AssertWriterVideoRecorderNew[697:119071] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.443155+0900 AssertWriterVideoRecorderNew[697:119071] IDAssetWriterCoordinator - appendAudioSampleBuffer 진입

2019-04-17 13:51:41.443186+0900 AssertWriterVideoRecorderNew[697:119071] IDAssetWriterCoordinator - appendSampleBuffer 진입

2019-04-17 13:51:41.443296+0900 AssertWriterVideoRecorderNew[697:119071] input 준비하고 데이터 버퍼 더하기

 

2019-04-17 13:51:41.448048+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.448116+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

2019-04-17 13:51:41.448142+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

2019-04-17 13:51:41.448177+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - [_assetWriterCoordinator append비디오SampleBuffer:sampleBuffer]  호출 :

2019-04-17 13:51:41.448225+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendVideoSampleBuffer 진입

2019-04-17 13:51:41.448264+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendSampleBuffer 진입

2019-04-17 13:51:41.448365+0900 AssertWriterVideoRecorderNew[697:119069] input 준비하고 데이터 버퍼 더하기

 

2019-04-17 13:51:41.464489+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

2019-04-17 13:51:41.464578+0900 AssertWriterVideoRecorderNew[697:119069] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

2019-04-17 13:51:41.464600+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendAudioSampleBuffer 진입

2019-04-17 13:51:41.464616+0900 AssertWriterVideoRecorderNew[697:119069] IDAssetWriterCoordinator - appendSampleBuffer 진입

2019-04-17 13:51:41.464692+0900 AssertWriterVideoRecorderNew[697:119069] input 준비하고 데이터 버퍼 더하기

 

파일다운로드

AssertWriterVideoRecorderNew2.zip
0.10MB

AVCaptureVideoDataOutput을 이용해서 카메라 만들기1

iOS 비디오 녹화 어플을 만들다가 생각지도 못한 난관에 봉착했다.

비디오를 녹화하는 도중에 녹화한 데이터크기를 실시간으로 체크하는 기능을 추가해야하는 상황이 발생했다.

젠장…

AVCaptureMovieFileOutput을 이용해서 비디오를 커스터마이징 하고 있었는데, AVCaptureMovieFileOutput으로는 실시간으로 데이터를 체크가 불가능했다.

대신, AVCaptureVideoDataOutput 과 AVAssetWriter를 이용해야지 데이터 체크가 가능했다. 

그리고 AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate 메소드를 이용해야 했다.

초기화는 AVCaptureMovieFileOutput 과 비슷하다.

초기화 할때 세션을 만들고 그 세션에 디바이스인풋과 아웃풋을 넣고 세션을 가동시키는 것은 동일하다.

AVCaptureVideoDataOutput의 다른 점은 세션이 가동될때 실시간으로 데이터를 

몇 가지 알아본 것들을 예제 파일로 정리해본다.

 

클래스 설명

IDCaptureSessionPipelineViewController 

  • UIViewController 를 부모로 상속하고, (IDCaptureSessionCoordinator 클래스의 ) IDCaptureSessionCoordinatorDelegate 프로토콜을 따른다.

예) coordinatorDidBeginRecording()  didFinishRecordingToOutputFileURL()

  • IDCaptureSessionCoordinator을 프로퍼티로 가지고 있다. @property (nonatomic, strong) IDCaptureSessionCoordinator *captureSessionCoordinator;  
  • 전체 ui, 기능을 컨트롤 한다.
  • IDCaptureSessionCoordinator(부모 클래스)를 먼저 초기화 하고, IDCaptureSessionAssetWriterCoordinator(자식 클래스)를 초기화 한다.
  • IDCaptureSessionCoordinator 에 델리게이트(자신)와 콜백큐(디스페치 메인큐)를 할당해준다.
  • IDCaptureSessionCoordinator 클래스를 호출 해서 AVCaptureVideoPreviewLayer에 세션을 넣어주고 반환값을 받는다. 
  • IDCaptureSessionCoordinator 의 세션을 실행 시킨다. dispatch_sync(_sessionQueue, ^{  [self->_captureSession startRunning]; });

IDCaptureSessionCoordinator(부모)

  • NSObject를 상속받는다.
  • AVCaptureSession, AVCaptureDevice, dispatch_queue_t(세션큐) ,dispatch_queue_t(콜백큐),  AVCaptureVideoPreviewLayer 를 프로퍼티로 가지고 있다.
  • IDCaptureSessionCoordinatorDelegate 프로토콜을 구현해 놓았다.
  • 세션을 만들고, 인풋, 아웃풋객체를 만들고 세션에 넣어주는 역할을 하는 클래스다.
  • 초기화 될때, 세션큐를 생성한다. 
  • 초기화 될때, AVCaptureSession 을 만들어서 AVCaptureDeviceInput(AVMediaTypeVideo)을 AVCaptureSession에 넣어주고, AVCaptureDevice를 초기화 해준다.
  • 그다음 AVCaptureDeviceInput(AVMediaTypeAudio)을 AVCaptureSession에 넣어준다.

IDCaptureSessionAssetWriterCoordinator(자식) 

  • IDCaptureSessionCoordinator을 상속하는 클래스
  • <AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate, IDAssetWriterCoordinatorDelegate> 프로토콜을 따른다.
  • IDAssetWriterCoordinatorDelegate는 IDAssetWriterCoordinator클래스에 구현해 놓았다.

- (void)writerCoordinatorDidFinishPreparing:(IDAssetWriterCoordinator *)coordinator;

- (void)writerCoordinator:(IDAssetWriterCoordinator *)coordinator didFailWithError:(NSError *)error;

- (void)writerCoordinatorDidFinishRecording:(IDAssetWriterCoordinator *)coordinator;

  • 비디오아웃풋, 오디오 아웃풋을 생성해서 세션에 넣어주는 것이 주요 역할이다. 그리고 세션이 시작되면 해당 델리게이트 메소드를 통해서 버퍼를 얻는다.
  • AVCaptureVideoDataOutput(비디오, 오디오), AVCaptureConnection(비디오, 오디오),AVAssetWriter, CMFormatDescriptionRef, IDAssetWriterCoordinator(클래스), NSDictionary *videoCompressionSettings를 프로퍼티로 가지고 있다.
  • 초기화 될때 videoDataOutputQueue를 만들고 실행시킽다. 초기화될때 audioDataOutputQueue를 만든다.
  • 초기화 될때 , videoDataOutput 을 생성하고, audioDataOutput을 생성하고 위에서 만든 비디오큐, 오디오 큐를 동시에 setSampleBufferDelegate 델리게이트 메소드에 넣어준다.
  • 비디오 아웃풋을 IDCaptureSessionCoordinator(클래스의) 세션에 넣어주고 videoConnection 객체를 만든다.
  • 오디오 아웃풋을 IDCaptureSessionCoordinator(클래스의) 세션에 넣어주고 audioConnection 객체를 만든다.
  • 비디오, 오디오 아웃풋을 이용해서 videoCompressionSettings, audioCompressionSettings 객체를 초기화 해준다.
  • 세션이 실행되면 델리게이트 메소드인 (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection 가 계속 호출된다.
  • 여기서 레코딩을 하는지 아니면 그냥 대기 상태인지를 체크해서 AVAssetWriter에 기록해준다.

IDAssetWriterCoordinator 

(2편에서...정리)

소스코드

IDCaptureSessionPipelineViewController 

#import <UIKit/UIKit.h>

@interface IDCaptureSessionPipelineViewController : UIViewController


@end

#import "IDFileManager.h"
#import "IDPermissionsManager.h"


#import "IDCaptureSessionPipelineViewController.h"
#import "IDCaptureSessionAssetWriterCoordinator.h"    //IDCaptureSessionCoordinator을 상속

// (IDCaptureSessionCoordinator 클래스의 ) IDCaptureSessionCoordinatorDelegate 프로토콜을 따르기 때문에 해당 메소드를 이곳에 구현해 놓았다.
// coordinatorDidBeginRecording()  didFinishRecordingToOutputFileURL(),

@interface IDCaptureSessionPipelineViewController () <IDCaptureSessionCoordinatorDelegate>


@property (nonatomic, strong) IDCaptureSessionCoordinator *captureSessionCoordinator;       //부모: 세션생성 하고  인풋디바이스를 세션에 넣어주는 클래스
@property (nonatomic, assign) BOOL recording;
@property (nonatomic, assign) BOOL dismissing;
@property (retain, nonatomic) IBOutlet UIBarButtonItem *recordButton;

@end

@implementation IDCaptureSessionPipelineViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    NSLog(@"IDCaptureSessionPipelineViewController setupWithPipelineMode IDCaptureSessionAssetWriterCoordinator 객체 생성시작!");
    _captureSessionCoordinator = [IDCaptureSessionAssetWriterCoordinator new];          //부모객체 = 자식 new
    
    //캡쳐세션에 델리게이트 와 큐 설정
    [_captureSessionCoordinator setDelegate:self callbackQueue:dispatch_get_main_queue()];
    
    [self configureInterface];
}



- (IBAction)toggleRecording:(id)sender
{
    NSLog(@"IDCaptureSessionPipelineViewController - toggleRecording 버튼");
}

- (IBAction)closeCamera:(id)sender
{
    NSLog(@"IDCaptureSessionPipelineViewController - closeCamera 버튼");
}

#pragma mark - Private methods
- (void)configureInterface
{
 NSLog(@"IDCaptureSessionPipelineViewController - configureInterface 호출 -  AVCaptureVideoPreviewLayer 객체 생성");
    
    AVCaptureVideoPreviewLayer *previewLayer = [_captureSessionCoordinator previewLayer];
    previewLayer.frame = self.view.bounds;
    [self.view.layer insertSublayer:previewLayer atIndex:0];
    
    [_captureSessionCoordinator startRunning];
    
    
}





- (void)stopPipelineAndDismiss
{
    NSLog(@"IDCaptureSessionPipelineViewController - stopPipelineAndDismiss 호출");
}


- (void)checkPermissions
{
    NSLog(@"IDCaptureSessionPipelineViewController - IDPermissionsManager 객체 생성 + 카메라 오디오 권한 체크 시작");
}


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

}


//녹화가 끝날때 (정지 버튼을 눌렀을 때)호출
- (void)coordinator:(IDCaptureSessionCoordinator *)coordinator didFinishRecordingToOutputFileURL:(NSURL *)outputFileURL error:(NSError *)error
{
    NSLog(@"IDCaptureSessionPipelineViewController didFinishRecordingToOutputFileURL 호출");

}



- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

 

IDCaptureSessionCoordinator(부모)

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>


@protocol IDCaptureSessionCoordinatorDelegate;

@interface IDCaptureSessionCoordinator : NSObject



@property (nonatomic, strong) AVCaptureSession *captureSession;                  //세션 객체
@property (nonatomic, strong) AVCaptureDevice *cameraDevice;                      //디바이스 객체
@property (nonatomic, strong) dispatch_queue_t delegateCallbackQueue;       //콜백 큐

@property (nonatomic, weak) id<IDCaptureSessionCoordinatorDelegate> delegate;

- (void)setDelegate:(id<IDCaptureSessionCoordinatorDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue;

- (BOOL)addInput:(AVCaptureDeviceInput *)input toCaptureSession:(AVCaptureSession *)captureSession;       //인풋객체 넣기

- (BOOL)addOutput:(AVCaptureOutput *)output toCaptureSession:(AVCaptureSession *)captureSession;         //아웃풋 객체 넣기

- (void)startRunning;
- (void)stopRunning;

- (void)startRecording;
- (void)stopRecording;

- (AVCaptureVideoPreviewLayer *)previewLayer;                                                                                                           //미리보기
@end


@protocol IDCaptureSessionCoordinatorDelegate <NSObject>
@required
- (void)coordinatorDidBeginRecording:(IDCaptureSessionCoordinator *)coordinator;
- (void)coordinator:(IDCaptureSessionCoordinator *)coordinator didFinishRecordingToOutputFileURL:(NSURL *)outputFileURL error:(NSError *)error;
@end
#import "IDCaptureSessionCoordinator.h"


@interface IDCaptureSessionCoordinator ()

@property (nonatomic, strong) dispatch_queue_t sessionQueue;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;

@end


@implementation IDCaptureSessionCoordinator


-(instancetype)init{
     NSLog(@"IDCaptureSessionCoordinator init 진입");
    self = [super init];
    if(self){
        
        //세션 큐 생성
        _sessionQueue = dispatch_queue_create("com.abc.my.session", DISPATCH_QUEUE_SERIAL);
        //캡쳐 세션 생성
        _captureSession = [self setupCaptureSession];
        
    }
    return self;
}


/*
 AVFoundation비디오 캡처에 사용할 때는 사용자 정의 사용자 인터페이스를 제공해야합니다.
 모든 카메라 인터페이스의 주요 구성 요소는 실시간 미리보기입니다.
 이것은 AVCaptureVideoPreviewLayer카메라보기에 하위 레이어로 추가 된 객체를 통해 가장 쉽게 구현됩니다 .
 */
- (AVCaptureVideoPreviewLayer *)previewLayer
{
    if(!_previewLayer && _captureSession){
        NSLog(@"IDCaptureSessionCoordinator previewLayer 캡쳐 세션 넣어줌 초기화");
        _previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_captureSession];
    }
    return _previewLayer;
}

- (void)setDelegate:(id<IDCaptureSessionCoordinatorDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue{
     NSLog(@"IDCaptureSessionCoordinator setDelegate 진입 - 전역변수에 IDCaptureSessionCoordinatorDelegate 할당, delegateCallbackQueue 할당");
    
    if (delegate && (delegateCallbackQueue == NULL)) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"호출자는 델리게이트 콜백큐를 제공해야 한다!" userInfo:nil];
    }
    
    @synchronized(self)
    {
        _delegate = delegate;
        if (delegateCallbackQueue != _delegateCallbackQueue) {
            _delegateCallbackQueue = delegateCallbackQueue;
        }
    }
}



//세션러닝 시작
- (void)startRunning{
     NSLog(@"IDCaptureSessionCoordinator startRunning 진입 - dispatch_sync(세션큐, 캡쳐쎄션 스타트러닝!)");
    dispatch_sync(_sessionQueue, ^{
        [self->_captureSession startRunning];
    });
    
}
//러닝 정지
- (void)stopRunning{
     NSLog(@"IDCaptureSessionCoordinator stopRunning 진입");
}
//녹화 시작
- (void)startRecording{
     NSLog(@"IDCaptureSessionCoordinator startRecording 진입");
}
//녹화 정지
- (void)stopRecording{
     NSLog(@"IDCaptureSessionCoordinator stopRecording 진입");
}


#pragma mark - Capture Session Setup
//세션 셋팅
- (AVCaptureSession *)setupCaptureSession
{
     NSLog(@"IDCaptureSessionCoordinator setupCaptureSession 진입 ");
    
    AVCaptureSession *captureSession = [AVCaptureSession new];
    
    //비디오 인풋 디바이스 생성 + 캡쳐 세션에 넣어주기 여부 체크
    if (![self addDefaultCameraInputToCaptureSession:captureSession]) {
        NSLog(@"비디오 인풋을 캡쳐세션에 넣기 실패");
    }
    
    //오디오 인풋 디바이스 생성 + 캡쳐 세션에 넣어주기 여부 체크
    if (![self addDefaultMicInputToCaptureSession:captureSession]) {
        NSLog(@"오디오 인풋을 캡쳐세션에 넣기 실패");
    }
    return captureSession;
}


//비디오 입력을 구성하려면 AVCaptureDeviceInput원하는 카메라 장치 로 개체를 만들고 캡처 세션에 추가
//기본 설정된 카메라를 캡쳐세션에 추가
- (BOOL)addDefaultCameraInputToCaptureSession:(AVCaptureSession *)captureSession
{
     NSLog(@"IDCaptureSessionCoordinator addDefaultCameraInputToCaptureSession 진입");
    NSError *error;
    
    //비디오 인풋 디바이스 객체 생성 및 초기화
    AVCaptureDeviceInput *cameraDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo] error:&error];
    
    if (error) {
        NSLog(@"error configuring 비디오 input : %@" , [error localizedDescription]);
        return NO;
    } else {
        //캡쳐 세션에  카메라 인풋 넣어주기
        BOOL success = [self addInput:cameraDeviceInput toCaptureSession:captureSession];
     
        //캡쳐 세션에 인풋디바이스 넣기 성공했으면 디바이스 객체를 할당해 준다.
        _cameraDevice= cameraDeviceInput.device;
        
          return success;
    }
}



//마이크 인풋디바이스 객체를 생성해서 캡쳐 세션에 넣어준다.
- (BOOL)addDefaultMicInputToCaptureSession:(AVCaptureSession *)captureSession
{
     NSLog(@"IDCaptureSessionCoordinator addDefaultMicInputToCaptureSession 진입");
    
    NSError *error;
    AVCaptureDeviceInput *micDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio] error:&error];
    
    if (error) {
        NSLog(@"error configuring 오디오 input : %@" , [error localizedDescription]);
        return NO;
    } else {
      BOOL success =   [self addInput:micDeviceInput toCaptureSession:captureSession];
        return success;
    }
}


//인풋객체 넣기 -  전역 메소드
- (BOOL)addInput:(AVCaptureDeviceInput *)input toCaptureSession:(AVCaptureSession *)captureSession{
    NSLog(@"IDCaptureSessionCoordinator addInput toCaptureSession 진입");
    
    //캡쳐세션에 인풋 디바이스 넣을 수 있는지 확인
    if ([captureSession canAddInput:input]) {
        //캡쳐 세션에 인풋 디바이스 넣기
        [captureSession addInput:input];
        NSLog(@"IDCaptureSessionCoordinator addInput toCaptureSession - 세션에 캡쳐디바이스인풋 들어갔어요!");
        return YES;
    } else {
        NSLog(@"세션에 인풋 디바이스를 넣을 수 없습니다 : %@" , [input description]);
        return NO;
    }
    
    return YES;

}


//아웃풋 객체 넣기 -  전역 메소드
- (BOOL)addOutput:(AVCaptureOutput *)output toCaptureSession:(AVCaptureSession *)captureSession{
    NSLog(@"IDCaptureSessionCoordinator addOutput  toCaptureSession 진입");
    //아웃풋 객체를 세션에 넣어주기
    if ([captureSession canAddOutput:output]) {
        [captureSession addOutput:output];
        return YES;
    }else{
        NSLog(@"output을 세션에 넣을 수 없습니다! 설명 : %@" , [output description]);
    }
    
    
    return NO;

}

@end

 

IDCaptureSessionAssetWriterCoordinator(자식) 


#import "IDCaptureSessionCoordinator.h"

//@protocol IDCaptureSessionAssetWriterCoordinatorDelegate;
@interface IDCaptureSessionAssetWriterCoordinator : IDCaptureSessionCoordinator

@end

#import "IDCaptureSessionAssetWriterCoordinator.h"
#import <MobileCoreServices/MobileCoreServices.h>
#import "IDAssetWriterCoordinator.h"
#import "IDFileManager.h"

typedef NS_ENUM( NSInteger, RecordingStatus )
{
    RecordingStatusIdle = 0,
    RecordingStatusStartingRecording,
    RecordingStatusRecording,
    RecordingStatusStoppingRecording,
};                                                                          //internal state machine


@interface IDCaptureSessionAssetWriterCoordinator () <AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate, IDAssetWriterCoordinatorDelegate>

@property (nonatomic, strong) dispatch_queue_t videoDataOutputQueue;
@property (nonatomic, strong) dispatch_queue_t audioDataOutputQueue;

//data아웃풋
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
@property (nonatomic, strong) AVCaptureAudioDataOutput *audioDataOutput;

//캡쳐 커넥션
@property (nonatomic, strong) AVCaptureConnection *audioConnection;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;

//딕셔너리
@property (nonatomic, strong) NSDictionary *videoCompressionSettings;
@property (nonatomic, strong) NSDictionary *audioCompressionSettings;

@property (nonatomic, strong) AVAssetWriter *assetWriter;

@property (nonatomic, assign) RecordingStatus recordingStatus;              //NS_ENUM
@property (nonatomic, strong) NSURL *recordingURL;

@property(nonatomic, retain) __attribute__((NSObject)) CMFormatDescriptionRef outputVideoFormatDescription;
@property(nonatomic, retain) __attribute__((NSObject)) CMFormatDescriptionRef outputAudioFormatDescription;

@property(nonatomic, retain) IDAssetWriterCoordinator *assetWriterCoordinator;

@end

@implementation IDCaptureSessionAssetWriterCoordinator


- (instancetype)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"IDCaptureSessionAssetWriterCoordinator - init 진입");
        
        //비디오 큐 생성
        self.videoDataOutputQueue = dispatch_queue_create("com.example.capturesession.videodata", DISPATCH_QUEUE_SERIAL);
        
        dispatch_set_target_queue(_videoDataOutputQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
        
        //오디오 큐 생성
        self.audioDataOutputQueue = dispatch_queue_create("com.example.capturesession.audiodata", DISPATCH_QUEUE_SERIAL);
        
        [self addDataOutputsToCaptureSession:self.captureSession];  //captureSession은 부모 클래스가 가지고 있다.
    }
     return self;
}




#pragma mark - Recording

//자식
- (void)startRecording
{
   
}

- (void)stopRecording
{
   
}

#pragma mark - Private methods

- (void)addDataOutputsToCaptureSession:(AVCaptureSession *)captureSession
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - addDataOutputsToCaptureSession 진입 : 비디오, 오디오 아웃풋 객체 생성 + 설정");
    
    //비디오 객체 생성 + 설정
    self.videoDataOutput = [AVCaptureVideoDataOutput new];
    _videoDataOutput.videoSettings = nil;
    _videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
    
    //아웃풋에 델리게이트 설정 + 비디오 데이터 아웃풋 큐  할당
    [_videoDataOutput setSampleBufferDelegate:self queue:_videoDataOutputQueue];
    
    //오디오 아웃풋 객체 생성 + 오디오 데이터 아웃풋 큐  할당
    self.audioDataOutput  = [AVCaptureAudioDataOutput new];
    [_audioDataOutput setSampleBufferDelegate:self queue:_audioDataOutputQueue];
    
    
    //캡쳐 세션에 비디오 갭쳐 아웃풋 넣기(이 클래스가 부모 클래스 capturesession 상속 받아서 capturesession 에서 진행됨)
    [self addOutput:_videoDataOutput toCaptureSession:self.captureSession];
    //비디오 커넥션 초기화
    _videoConnection = [_videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
    
    
    //캡쳐 세션에 오디오 캡쳐 아웃풋 넣기
    [self addOutput:_audioDataOutput toCaptureSession:self.captureSession];
    //오디오 커넥터 초기화
    _audioConnection = [_audioDataOutput connectionWithMediaType:AVMediaTypeAudio];
    
    [self setCompressionSettings];
}



- (void)setupVideoPipelineWithInputFormatDescription:(CMFormatDescriptionRef)inputFormatDescription
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - setupVideoPipelineWithInputFormatDescription 진입 :");
    self.outputVideoFormatDescription = inputFormatDescription;
}

- (void)teardownVideoPipeline
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - teardownVideoPipeline 진입 :");
}


- (void)setCompressionSettings
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - setCompressionSettings 진입 :");
    _videoCompressionSettings = [_videoDataOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeQuickTimeMovie];
    _audioCompressionSettings = [_audioDataOutput recommendedAudioSettingsForAssetWriterWithOutputFileType:AVFileTypeQuickTimeMovie];
}

#pragma mark - SampleBufferDelegate methods

- (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){
                
                if(_recordingStatus == RecordingStatusRecording){
                    
                    NSLog(@"IDCaptureSessionAssetWriterCoordinator - [_assetWriterCoordinator appendAudioSampleBuffer:sampleBuffer]  호출 :");
                   // [_assetWriterCoordinator appendAudioSampleBuffer:sampleBuffer];
                }
                
            }
            
        }
        
        
    }else if(connection == _audioConnection){
        
       NSLog(@"IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection");
        self.outputAudioFormatDescription = formatDescription;
        @synchronized( self ) {
            if(_recordingStatus == RecordingStatusRecording){           //2 면
                [_assetWriterCoordinator appendAudioSampleBuffer:sampleBuffer];
            }
        }
        
    }
    
}

#pragma mark - IDAssetWriterCoordinatorDelegate methods

//녹화 시작 버튼 클릭했을때!!
- (void)writerCoordinatorDidFinishPreparing:(IDAssetWriterCoordinator *)coordinator
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator -  writerCoordinatorDidFinishPreparing 진입 :");
}

//오류발생 했을때!!
- (void)writerCoordinator:(IDAssetWriterCoordinator *)recorder didFailWithError:(NSError *)error
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator -  didFailWithError 진입 :");
}



// 정지 버튼 클릭되었을 때 호출
- (void)writerCoordinatorDidFinishRecording:(IDAssetWriterCoordinator *)coordinator
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator -  writerCoordinatorDidFinishRecording 진입 :");
}


#pragma mark - Recording State Machine
// call under @synchonized( self )
- (void)transitionToRecordingStatus:(RecordingStatus)newStatus error:(NSError *)error
{
    NSLog(@"IDCaptureSessionAssetWriterCoordinator - transitionToRecordingStatus 진입");
}



@end

IDAssetWriterCoordinator 


#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import <AVFoundation/AVFoundation.h>


@protocol IDAssetWriterCoordinatorDelegate;

@interface IDAssetWriterCoordinator : NSObject


- (instancetype)initWithURL:(NSURL *)URL;

- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)videoSettings;

- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings;

- (void)setDelegate:(id<IDAssetWriterCoordinatorDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue;

- (void)prepareToRecord;

- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)finishRecording;

@end


@protocol IDAssetWriterCoordinatorDelegate <NSObject>

- (void)writerCoordinatorDidFinishPreparing:(IDAssetWriterCoordinator *)coordinator;
- (void)writerCoordinator:(IDAssetWriterCoordinator *)coordinator didFailWithError:(NSError *)error;
- (void)writerCoordinatorDidFinishRecording:(IDAssetWriterCoordinator *)coordinator;

@end

#import "IDAssetWriterCoordinator.h"

typedef NS_ENUM(NSInteger, WriterStatus){
    WriterStatusIdle = 0,
    WriterStatusPreparingToRecord,
    WriterStatusRecording,
    WriterStatusFinishingRecordingPart1,             // waiting for inflight buffers to be appended
    WriterStatusFinishingRecordingPart2,             // calling finish writing on the asset writer
    WriterStatusFinished,                                        // terminal state
    WriterStatusFailed                                             // terminal state
};                                                                              // internal state machine


@interface IDAssetWriterCoordinator()

@property (nonatomic, assign) WriterStatus status;                                      //NS_ENUM

@property (nonatomic) dispatch_queue_t writingQueue;
@property (nonatomic) dispatch_queue_t delegateCallbackQueue;
@property (nonatomic) NSURL *URL;

@property (nonatomic) AVAssetWriter *assetWriter;
@property (nonatomic) BOOL haveStartedSession;


//오디오 설명, 셋팅, 인풋
@property (nonatomic) CMFormatDescriptionRef audioTrackSourceFormatDescription;
@property (nonatomic) NSDictionary *audioTrackSettings;
@property (nonatomic) AVAssetWriterInput *audioInput;

//비디오 설명, 셋팅, 인풋
@property (nonatomic) CMFormatDescriptionRef videoTrackSourceFormatDescription;
@property (nonatomic) CGAffineTransform videoTrackTransform;
@property (nonatomic) NSDictionary *videoTrackSettings;
@property (nonatomic) AVAssetWriterInput *videoInput;

@end



@implementation IDAssetWriterCoordinator


-(instancetype)initWithURL:(NSURL *)URL
{
      NSLog(@"IDAssetWriterCoordinator - initWithURL 진입");
    self = [super init];
    if (self) {
        
    }
    return self;
}

- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)videoSettings
{
     NSLog(@"IDAssetWriterCoordinator - addVideoTrackWithSourceFormatDescription 진입");
}

- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings
{
     NSLog(@"IDAssetWriterCoordinator - addAudioTrackWithSourceFormatDescription 진입");
}
- (void)setDelegate:(id<IDAssetWriterCoordinatorDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue
{
     NSLog(@"IDAssetWriterCoordinator - setDelegate <IDAssetWriterCoordinatorDelegate> 진입");
}

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

- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
     NSLog(@"IDAssetWriterCoordinator - appendVideoSampleBuffer 진입");
}

- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
     NSLog(@"IDAssetWriterCoordinator - appendAudioSampleBuffer 진입");
}

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



#pragma mark - Private methods -----------------------------------------------------------------------------
// audioInput 생성후, assetWriter에 넣어주기
- (BOOL)setupAssetWriterAudioInputWithSourceFormatDescription:(CMFormatDescriptionRef)audioFormatDescription settings:(NSDictionary *)audioSettings error:(NSError **)errorOut
{
    NSLog(@"IDAssetWriterCoordinator - setupAssetWriterAudioInputWithSourceFormatDescription 진입");
    
    return YES;
}



//video input 생성 , assetWriter에 videoInput 추가
- (BOOL)setupAssetWriterVideoInputWithSourceFormatDescription:(CMFormatDescriptionRef)videoFormatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings error:(NSError **)errorOut
{
    NSLog(@"IDAssetWriterCoordinator - setupAssetWriterVideoInputWithSourceFormatDescription 진입");
    return YES;
}



- (NSDictionary *)fallbackVideoSettingsForSourceFormatDescription:(CMFormatDescriptionRef)videoFormatDescription
{
    NSLog(@"IDAssetWriterCoordinator - fallbackVideoSettingsForSourceFormatDescription 진입");
    return  nil;
    
}



//최종적으로 버퍼 더하기
- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer ofMediaType:(NSString *)mediaType
{
    NSLog(@"IDAssetWriterCoordinator - appendSampleBuffer 진입");
}



// call under @synchonized( self )
- (void)transitionToStatus:(WriterStatus)newStatus error:(NSError *)error
{
    NSLog(@"IDAssetWriterCoordinator - transitionToStatus 진입");
}


- (NSError *)cannotSetupInputError
{
    NSLog(@"IDAssetWriterCoordinator - cannotSetupInputError 진입");
    
    return nil;
}

@end

 

카메라 실행시켰을 때 로그

...더보기

2019-04-16 13:49:01.701875+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionPipelineViewController setupWithPipelineMode IDCaptureSessionAssetWriterCoordinator 객체 생성시작!

2019-04-16 13:49:01.701993+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator init 진입

2019-04-16 13:49:01.702034+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator setupCaptureSession 진입 

2019-04-16 13:49:01.706344+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addDefaultCameraInputToCaptureSession 진입

 

2019-04-16 13:49:01.719701+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addInput toCaptureSession 진입

 

2019-04-16 13:49:01.722364+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addInput toCaptureSession - 세션에 캡쳐디바이스인풋 들어갔어요!

 

2019-04-16 13:49:01.722407+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addDefaultMicInputToCaptureSession 진입

 

2019-04-16 13:49:01.731848+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addInput toCaptureSession 진입

 

2019-04-16 13:49:01.733323+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addInput toCaptureSession - 세션에 캡쳐디바이스인풋 들어갔어요!

 

2019-04-16 13:49:01.733360+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionAssetWriterCoordinator - init 진입

 

2019-04-16 13:49:01.733400+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionAssetWriterCoordinator - addDataOutputsToCaptureSession 진입 : 비디오, 오디오 아웃풋 객체 생성 + 설정

2019-04-16 13:49:01.733780+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addOutput  toCaptureSession 진입

 

2019-04-16 13:49:01.735467+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator addOutput  toCaptureSession 진입

 

2019-04-16 13:49:01.736292+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionAssetWriterCoordinator - setCompressionSettings 진입 :

 

2019-04-16 13:49:01.737157+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator setDelegate 진입 - 전역변수에 IDCaptureSessionCoordinatorDelegate 할당, delegateCallbackQueue 할당

 

2019-04-16 13:49:01.737205+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionPipelineViewController - configureInterface 호출 -  AVCaptureVideoPreviewLayer 객체 생성

 

2019-04-16 13:49:01.737225+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator previewLayer 캡쳐 세션 넣어줌 초기화

 

2019-04-16 13:49:01.739046+0900 AssertWriterVideoRecorderNew[2591:505792] IDCaptureSessionCoordinator startRunning 진입 - dispatch_sync(세션큐, 캡쳐쎄션 스타트러닝!)

 

2019-04-16 13:49:02.041482+0900 AssertWriterVideoRecorderNew[2591:505846] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

 

2019-04-16 13:49:02.041546+0900 AssertWriterVideoRecorderNew[2591:505846] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

 

2019-04-16 13:49:02.041563+0900 AssertWriterVideoRecorderNew[2591:505846] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription == nil

 

2019-04-16 13:49:02.041582+0900 AssertWriterVideoRecorderNew[2591:505846] IDCaptureSessionAssetWriterCoordinator - setupVideoPipelineWithInputFormatDescription 진입 :

 

2019-04-16 13:49:02.042227+0900 AssertWriterVideoRecorderNew[2591:505847] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

 

2019-04-16 13:49:02.042250+0900 AssertWriterVideoRecorderNew[2591:505847] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

 

2019-04-16 13:49:02.042261+0900 AssertWriterVideoRecorderNew[2591:505847] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

 

2019-04-16 13:49:02.078642+0900 AssertWriterVideoRecorderNew[2591:505845] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

 

2019-04-16 13:49:02.078711+0900 AssertWriterVideoRecorderNew[2591:505845] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

 

2019-04-16 13:49:02.078726+0900 AssertWriterVideoRecorderNew[2591:505845] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

 

2019-04-16 13:49:02.109463+0900 AssertWriterVideoRecorderNew[2591:505847] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

 

2019-04-16 13:49:02.109554+0900 AssertWriterVideoRecorderNew[2591:505847] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

 

2019-04-16 13:49:02.111679+0900 AssertWriterVideoRecorderNew[2591:505844] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

 

2019-04-16 13:49:02.111712+0900 AssertWriterVideoRecorderNew[2591:505844] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  videoConnection

 

2019-04-16 13:49:02.111724+0900 AssertWriterVideoRecorderNew[2591:505844] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : outputVideoFormatDescription != nil

 

2019-04-16 13:49:02.161513+0900 AssertWriterVideoRecorderNew[2591:505844] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer 진입 :

 

2019-04-16 13:49:02.161828+0900 AssertWriterVideoRecorderNew[2591:505844] IDCaptureSessionAssetWriterCoordinator - didOutputSampleBuffer : connection =  audioConnection

 

 

예제파일

AssertWriterVideoRecorderNew.zip
0.09MB

 

Ios autoLayout 기초

장치가 세로일때 뷰 모양
장치가 가로일때 뷰모양

예제소스


#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    
    //크기 고정 뷰 - 화면 정중앙에 배치될 정사각형의 뷰 만들기
    UIView *oneView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    oneView.translatesAutoresizingMaskIntoConstraints = NO;     //필수!
    [oneView setBackgroundColor:[UIColor brownColor]];
    [self.view addSubview:oneView];
    
    
  
    //*설명
    //constraintWithItem : 제약을 걸 객체
    //attribute : 제약 속성
    //relatedBy : 상대 객체와의 관계
    //toItem: 상대 뷰객체
    //attribute상대뷰 객체의 제약 속성

      //가로 길이 제약 조건
    NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:oneView
                                                                       attribute:NSLayoutAttributeWidth
                                                                       relatedBy:NSLayoutRelationEqual
                                                                          toItem:nil
                                                                       attribute:NSLayoutAttributeNotAnAttribute
                                                                      multiplier:1.f
                                                                        constant:50];
    //높이 제약 조건
    NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:oneView
                                                                        attribute:NSLayoutAttributeHeight
                                                                        relatedBy:NSLayoutRelationEqual
                                                                           toItem:nil
                                                                        attribute:NSLayoutAttributeNotAnAttribute
                                                                       multiplier:1.0
                                                                         constant:50];
    
    
    
    //oneView를 view 기준으로 view의 x 값 중앙에 위치.
    NSLayoutConstraint *centerX = [NSLayoutConstraint constraintWithItem:oneView
                                                               attribute:NSLayoutAttributeCenterX
                                                               relatedBy:NSLayoutRelationEqual
                                                                  toItem:self.view
                                                               attribute:NSLayoutAttributeCenterX
                                                              multiplier:1.0
                                                                constant:0];
    
    
    
    //oneView를 view 기준으로 view의 y 값 중앙에 위치.
    NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:oneView
                                                               attribute:NSLayoutAttributeCenterY
                                                               relatedBy:NSLayoutRelationEqual
                                                                  toItem:self.view
                                                               attribute:NSLayoutAttributeCenterY
                                                              multiplier:1.0
                                                                constant:0];

    //크기 제약 조건 추가하기
    [oneView addConstraints:[NSArray arrayWithObjects:widthConstraint,heightConstraint,nil]];
    //위치 제약 조건 추가
    [self.view addConstraints:[NSArray arrayWithObjects:centerX,centerY,nil]];
    
    
    /*------------------------------------------------------------------------------------------------------------------*/
    
    //크기 유동 뷰 :  화면이 가로일때, 세로 일때 각각 다르게 변한다.
    UIView *twoView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    twoView.translatesAutoresizingMaskIntoConstraints = NO;
    [twoView setBackgroundColor:[UIColor yellowColor]];
    [self.view addSubview:twoView];
    
    
    //높이 50고정 : 높이는 변하지 않게 고정 ,  가로는 설정안해준다. 설정해주면 화면을 가로로했을때 가로길이가 바뀌지 않기 때문이다.
    NSLayoutConstraint *heightCons = [NSLayoutConstraint constraintWithItem:twoView
                                                                  attribute:NSLayoutAttributeHeight
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:nil
                                                                  attribute:NSLayoutAttributeNotAnAttribute
                                                                 multiplier:1.0
                                                                   constant:50];
    
    //twoView의 y 값은 view 중앙 y값
    NSLayoutConstraint *y = [NSLayoutConstraint constraintWithItem:twoView
                                                               attribute:NSLayoutAttributeTop
                                                               relatedBy:NSLayoutRelationEqual
                                                                  toItem:self.view
                                                               attribute:NSLayoutAttributeTop
                                                              multiplier:1.0
                                                                constant:30];
    
    
    
    
    //twoView의 왼쪽값은 view의 왼쪽에서 30 떨어지게
    NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:twoView
                                                            attribute:NSLayoutAttributeLeading
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:self.view
                                                            attribute:NSLayoutAttributeLeading
                                                           multiplier:1.0
                                                             constant:30];
    
     //twoView의 오른쪽값은 view의 오른쪽에서 30 떨어지게 (오른쪽은 self.view가 기준이 된다!!)
    NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:self.view
                                                             attribute:NSLayoutAttributeTrailing
                                                             relatedBy:NSLayoutRelationEqual
                                                                toItem:twoView
                                                             attribute:NSLayoutAttributeTrailing
                                                            multiplier:1.0
                                                              constant:30];
    
    //높이 제약 조건 넣어주기.
    [twoView addConstraints:[NSArray arrayWithObjects:heightCons,nil]];
    //view에 y좌표, 왼쪽, 오른쪽 제약조건 넣어주기.
    [self.view addConstraints:[NSArray arrayWithObjects:y,left,right,nil]];
    
    
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

 

예제파일

AutoLayoutSampleCode.zip
0.06MB

ios 화면보호기 & 터치 감지하기

목표

iOS UIWindow에서 터치를 감지한 후 일정시간 터치가 없으면 화면보호기 viewcontroller 띄워주는 기능 구현

 

이 화면에서 3초이상 터치가 없으면 화면보호기를 띄워줄 것이다.

순서

*공통상수클래스 제작- CommonValues

설명: 화면보호기 시간을 설정해준다.

#import <Foundation/Foundation.h>

@interface CommonValues : NSObject

@property(nonatomic,assign) int SAVER_TIME;         //화면보호기 시간
@end

#import "CommonValues.h"

@implementation CommonValues
@synthesize SAVER_TIME;

- (instancetype)init
{
    self = [super init];
    if (self) {
        SAVER_TIME = 3;                                //화면보호기 설정 시간 - 3초 ;
    }
   return self;
}

@end

 

*화면보호기 클래스 제작- SViewController

설명 : 화면보호기 화면을 xib 파일로 생성한다. 그후, ui객체들을 SViewController 연결시킨다.

필요한 변수들을 생성한다. 버튼 IBAction 함수를 만들어서 버튼 or 삭제 버튼이 눌러질때 이벤트 처리를 해준다.

초기 비번은 0000. 맞으면 화면을 닫아주고, 틀리면 알림창을 띄워준다.(비밀번호 담는 변수 초기화)

비밀번호 확인 로직은 주석 참고.

화면보호기 ui


#import <UIKit/UIKit.h>

@interface SViewController : UIViewController

@end

#import "SViewController.h"

@interface SViewController ()

//숫자~ 삭제 버튼 맴버 변수
@property (weak, nonatomic) IBOutlet UIButton *one;
@property (weak, nonatomic) IBOutlet UIButton *two;
@property (weak, nonatomic) IBOutlet UIButton *three;
@property (weak, nonatomic) IBOutlet UIButton *four;
@property (weak, nonatomic) IBOutlet UIButton *five;
@property (weak, nonatomic) IBOutlet UIButton *six;
@property (weak, nonatomic) IBOutlet UIButton *seven;
@property (weak, nonatomic) IBOutlet UIButton *eight;
@property (weak, nonatomic) IBOutlet UIButton *nine;
@property (weak, nonatomic) IBOutlet UIButton *zero;
@property (weak, nonatomic) IBOutlet UIButton *deleteButton;

//비번
@property (weak, nonatomic) IBOutlet UITextField *textfield;
@property(nonatomic,strong)NSString *secretKey;
@end

@implementation SViewController

//세로 화면만 허용 하기
- (NSUInteger) supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait; //세로 화면만 허용
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //비번 초기화
    _secretKey = [[NSString alloc]init];
}



// 숫자키 ~ del 눌렀을때
- (IBAction)numBtnAction:(id)sender {
    
    //삭제키 눌렀을때
    if([[sender titleLabel].text isEqualToString:@"del"]){
        //비밀번호 개수가 한개 이상일때
        if ([_secretKey length] > 0) {
            //하나씩 지워 주기
            _secretKey = [_secretKey substringToIndex:[_secretKey length] - 1];
            
            self.textfield.text = _secretKey;
        } else {
            return;
        }
    }else{
        
        //숫자키 눌렀을 때 숫자하나씩더하기
        _secretKey = [_secretKey stringByAppendingString:[sender titleLabel].text];
        self.textfield.text = _secretKey;

        //비밀번호 개수가 4개 일때
        if (_secretKey.length > 3) {
            //비번체크 : 비번을 0000 라고 해준다.
            if([_secretKey isEqualToString:@"0000"]){
                
                //화면보호기 창 내려주기
                [self dismissViewControllerAnimated:YES completion:nil];
            }else{
                
                //비밀번호 틀림창 띄워주기
                UIAlertController * view=   [UIAlertController
                                                             alertControllerWithTitle:@"알림"
                                                             message:@"비밀번호를 확인해주세요"
                                                             preferredStyle:UIAlertControllerStyleActionSheet];
                
                UIAlertAction* ok = [UIAlertAction
                                                 actionWithTitle:@"확인"
                                                 style:UIAlertActionStyleDefault
                                                 handler:^(UIAlertAction * action)
                                                 {
                                                     [view dismissViewControllerAnimated:YES completion:nil];
                                                 }];
                
                [view addAction:ok];
                [self presentViewController:view animated:YES completion:^{
                    self.textfield.text = @"";
                    self->_secretKey = [[NSString alloc]init];
                }];
                
            }//
        }//
    }
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end

*화면보호기를 띄울 클래스 제작- DetectTouchWindow

 화면 보호기를 컨트롤 하는 클래스는 UIWindow를 상속 받는다. NSTimer  객체변수와 UIWindow 객체 변수등 필요한 변수를 생성한다. 

idleTimerExceeded 함수: 타이머가 일정시간이 지났을때 호출되는 함수.화면보호기를 띄워준다.

resetIdleTimer 함수 : 터치가 인식되면 타이머를 계속 초기화 시켜준다. 터치가 안되면 타이머는 계속 진행된다.

sendEvent 함수: 터치 시작 or 터치 종료에 호출되는 함수


#import <UIKit/UIKit.h>
#import "SViewController.h"

@interface DetectTouchWindow : UIWindow                     //화면 보호기를 컨트롤 하는 클래스는 UIWindow를 상속 받는다.
@property (strong,nonatomic) NSTimer *idleTimer;         //타이머 객체
@property (strong, nonatomic) UIWindow *window;       //UIWindow 객체
@property (nonatomic,assign) int screenSaverTime;      //화면보호기가 나타날 시간(터치가 없고난 뒤 몇 시간뒤?)

@end
#import "DetectTouchWindow.h"
#import "CommonValues.h"
@implementation DetectTouchWindow
@synthesize idleTimer,screenSaverTime;



- (void)sendEvent:(UIEvent *)event {
    [super sendEvent:event];
    // 타이머 재설정 횟수를 줄이기 위해 시작 터치 또는 종료 터치에서만 타이머를 재설정.
    NSSet *allTouches = [event allTouches];
    
    // 눌렀을때 1, 누르고 손가락을 땔때 resetIdleTimer 를 호출한다.
    NSLog(@"sendEvent : %lu" , (unsigned long)[allTouches count]);
    if ([allTouches count] > 0) {
       
        //allTouches 수는 1로만 계산되므로 anyObject가 여기에서 작동합니다.
        UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
        if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded)
        [self resetIdleTimer];
    }
}

//터치가 되면 호출되는 함수 - 터치 중
- (void)resetIdleTimer {
    if (idleTimer) {
        NSLog(@"DetectTouchWindow - 터치가 감지되는 중...");
        [idleTimer invalidate]; //타이머 제거 = 타이머 초기화
    }
    
    //공통 상수 함수 - 화면보호기 시간
    CommonValues *cmVal = [[CommonValues alloc]init];
    screenSaverTime = cmVal.SAVER_TIME;
    //일정 시간뒤 화면보호기 창 띄우기 - 터치가 되면 계속 일정 시간으로 초기화 됨.
    idleTimer = [NSTimer scheduledTimerWithTimeInterval:screenSaverTime target:self selector:@selector(idleTimerExceeded) userInfo:nil repeats:NO] ;
}

- (void)idleTimerExceeded {
    NSLog(@"DetectTouchWindow - 화면 보호기 창을 띄웁니다.");
    
    //현재 띄워진 class 의 이름 - 특정 클래스 걸러내기
    UIViewController *curentViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
    NSString *currentSelectedCViewController = NSStringFromClass([[((UINavigationController *)curentViewController) visibleViewController] class]);
    
    //사진촬영 중일때, 녹음중일때 제외하고 화면보호기 띄우기 - !
    if([currentSelectedCViewController isEqualToString:@"CaptureMedia"]){
         [self resetIdleTimer];     //화면 보호기 시간 계속 초기화(터치중임)
    }else if([currentSelectedCViewController isEqualToString:@"AudioRecorder"]){
         [self resetIdleTimer];
    }else if([currentSelectedCViewController isEqualToString:@"MediaView"]){
        [self resetIdleTimer];
    }else{

       
        //위의 클래스들이 아닐때 화면보호기 띄우기
        NSString *s_key = @"1234";
        if (s_key) {
            NSLog(@"화면보호기를 띄웁니다 ₩~~~~~~~  %@ " , s_key);
            
            //일정시간 이상 지나면 커스텀으로 만든 화면 보호기 띄워주기
            SViewController *sv = [[SViewController alloc]init];
           [[[UIApplication.sharedApplication keyWindow]rootViewController] presentViewController:sv animated:YES completion:nil];
       
            
//            UIViewController *activeController = [UIApplication sharedApplication].keyWindow.rootViewController;
//            if ([activeController isKindOfClass:[UINavigationController class]])
//                activeController = [(UINavigationController*) activeController visibleViewController];
//            else if (activeController.presentedViewController)
//                activeController = activeController.presentedViewController;
//            [activeController presentViewController:sv animated:YES completion:nil];

        }else{
            NSLog(@"화면보호기를 띄우지 않습니다~~~~~~~~~ %@ " , s_key);
        }

    }
   
}


@end

*앱 댈리게이트에 설정해주기 제작- AppDelegate

설명: 위에서 만든 클래스들을 객체로 만들어서 셋팅해준다. main 컨트롤러에 터치가 시작되면 DetectTouchWindow에서 반응을 한 뒤 카운터를 시작한다. 터치가 또 있으면 DetectTouchWindow의 sendEvent, idleTimerExceeded 함수가 호출 되면서 카운터가 초기화 된다. 일정 시간 터치가 없으면 타이머가 완료(초기화 x 정상종료)되면서 화면보호기가 뜬다. 

#import "AppDelegate.h"
#import "CommonValues.h"
#import "DetectTouchWindow.h"
#import "ViewController.h"
@interface AppDelegate ()
@property (strong,nonatomic) ViewController *vc;
@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    _window = [[DetectTouchWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UIStoryboard *sboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    ViewController *vc = [sboard instantiateInitialViewController];
    
    UINavigationController *nc = [[UINavigationController alloc]initWithRootViewController:vc];
    
    [_window setBackgroundColor:[UIColor yellowColor]];
    [_window setRootViewController:nc];
    [_window makeKeyAndVisible];
    
    return YES;
}


@end

 

예제파일

DtectView.zip
0.08MB

날짜를 이용해서 파일 삭제하기

날짜를 이용해서 30일이 지난 폴더 또는 파일을 삭제하는 기능을 구현해 보자. 

먼저, 다큐먼트 폴더 안에 video, photo, audio 폴더가 각각있다고 가정하자. 위의 미디어 폴더 아래는 20190401, 20190402 와 같이 폴더 or 파일이름이 생성한 날짜인 파일들이 존재할 것이다.

참고로 다큐먼트 폴더는 사용자가 접근할 수 있는 최상위 폴더이다. 다큐먼트 이외의 폴더로는 애플 정책상(보안문제)접근이 어렵다.

필요한 데이터는 아래와 같다.

1.오늘날짜를 초를 변환한 값.

2.파일을 생성한 날짜를 초로 변환한 값

3.생성한 날짜에 30일(초값)을 더한 값. - 삭제예정일 (생성한 날짜를 초로 변환하고 30일을 초로 변환해서 계산 후 리턴 한다.)

위의 데이터를 구했으면 날짜값을 비교해서 파일을 삭제해준다. 

* 앱을 구동할때 날자를 계산해서 처리(AppDelegate.m)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    NSLog(@"application - 앱실행!");
    
    //일정 시간 지난 미디어 파일 지우기
    [self deleteOverDateMediaFile];
    
    return YES;
}

//일정 시간 지난 미디어 파일 지우기
-(void)deleteOverDateMediaFile{
    
    NSLog(@"일정 시간 지난 미디어 파일 지우기 deleteOverDateMediaFile - appDelegate");
    float todaySeconds = [self getTodaySeconds];
    // video 폴더 안의 파일 확인 후 삭제,  audio 폴더 안의 파일 확인 후 삭제,  photo 폴더 안의 파일 확인 후 삭제,
    [self startDeleteMediaFile:@"/video" deleteStandardSeconds:todaySeconds ];
    [self startDeleteMediaFile:@"/audio" deleteStandardSeconds:todaySeconds ];
    [self startDeleteMediaFile:@"/photo" deleteStandardSeconds:todaySeconds ];
}

//비디오, 오디오, 사진 파일 삭제 :(오늘날짜를 초로변환 한 값을 전달해준다!)
-(void) startDeleteMediaFile :(NSString *) fileName  deleteStandardSeconds:(float) todaySeconds{
    NSLog(@"비디오, 오디오, 사진 파일 삭제 startDeleteMediaFile - appDelegate");
    //파일 매니져
    NSFileManager *fm = [NSFileManager defaultManager];
    //최종 문서  폴더
    NSArray *DocumentDrectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    //다큐먼트 폴더에서 제일 상위 폴더(video, audio, photo) 바로 아래 존재하는 폴더들(20190101, 20190202....)
    NSString *bigDir = [DocumentDrectory[0] stringByAppendingFormat:@"%@",fileName];
    
    //디렉토리 존재여부 확인
    if ([fm isWritableFileAtPath:bigDir] ) {
        NSLog(@" %@ Dir가 존재합니다." , bigDir);
    }else{
        NSLog(@" %@ Dir가 존재하지 않습니다. ㅜnㅜ 확인해주세요..." ,bigDir);
    }
    
    //폴더 안에 있는 날짜 폴더들을 배열에 담는다.
    NSError *err;
    NSArray *smallDateFileArray = [fm contentsOfDirectoryAtPath:bigDir error:&err];
    //해당 경로가 존재하면
    if (err == nil) {
        //날짜 폴더 개수별로 꺼내기
        for (int i = 0 ; i < (unsigned long)[smallDateFileArray count]; i++) {
            
            //삭제할 폴더의 전체 경로 - 아래쪽에서 삭제해줄때 사용됨.
            NSString *folderPathToDelete = [bigDir stringByAppendingFormat:@"/%@/",[smallDateFileArray objectAtIndex:i] ];
            
            //String 폴더명 가져와서 날짜 타입으로 바꾸고 초로 바꿔주기
            NSString *dateFileNameString =   [smallDateFileArray objectAtIndex:i];  //폴더의이름(가공전)
            NSDateFormatter *fileDateFormatter = [[NSDateFormatter alloc] init];   //날짜포멧
            [fileDateFormatter setDateFormat:@"yyyyMMdd"];                                  //날짜포멧
            NSDate *fileNameDate = [fileDateFormatter dateFromString:dateFileNameString];   //폴더이름 가공(NSDate)
            NSString *fileNameDateString = [fileDateFormatter stringFromDate:fileNameDate]; //폴더이름 가공(NSString)
            NSLog(@"파일이름(날짜) : %@" , fileNameDateString);
            NSTimeInterval fileNameToSeconds = [fileNameDate timeIntervalSinceReferenceDate];//초로변환
            NSLog(@"파일이름(초) : %f" , fileNameToSeconds);  //57 5737200  4월 1일(촬영일)
            
                                                                                                //57 6000000  4월 2일(오늘)
            
            
            //위의 폴더의 초에서 30일에 해당하는 초값을 계산해서 리턴
            float afterLongDaySec = [self delDateString:fileNameDateString];
            NSLog(@"afterLongDaySec(30일 이후 날짜) 초 : %f" , afterLongDaySec);
            //57 8329216  4월 40일(삭제기준일)
            
            
            // 오늘날짜초 >= 삭제예정날짜초
            // 오늘날짜와 비교해서 해당폴더의 30일 이후의 날짜값이 오늘날짜값 보다 작으면 .
            if (todaySeconds >= afterLongDaySec) {
                
                NSError *deleteErr;
                //해당 폴더 삭제!!
                [[NSFileManager defaultManager] removeItemAtPath:folderPathToDelete error:&deleteErr];
                
                if (deleteErr != nil) {
                    NSLog(@"이전 파일 삭제 중 오류가 발생했습니다.");
                    return;
                }else{
                    NSLog(@"삭제 성공!");
                    NSLog(@"%@ 폴더는 특정시간이 지난 파일이기 때문에 삭제 했습니다." , dateFileNameString);
                    NSLog(@"삭제 경로 : %@ " , folderPathToDelete);
                }
            }
        } // for - end
    }    // if - end
}


//해당 날짜에  30일(초값)을 더해서  초값으로 리턴
- (float) delDateString:(NSString *)date{
    int milsec = 3600 * 24 * 30;                    //30일
    //String 폴더명 가져와서 날짜 타입으로 바꾸고 숫자로 바꿔주기
    NSDateFormatter *fileDateFormatter = [[NSDateFormatter alloc] init];
    [fileDateFormatter setDateFormat:@"yyyyMMdd"];
    NSDate *fileNameDate = [fileDateFormatter dateFromString:date];
    
    //30일 이후의 날짜
    NSDate *delDate = [fileNameDate dateByAddingTimeInterval:milsec];
    
    //날짜 포멧형식으로 변경
    NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
    [formatter setDateFormat:@"yyyyMMdd"];
    NSString *delDateString = [formatter stringFromDate:delDate];
    NSLog(@"파일의 30일 이후 날짜 : %@" , delDateString);
    NSDate *dateDate = [formatter dateFromString:delDateString];
    NSTimeInterval seconds = [dateDate timeIntervalSinceReferenceDate];
    NSLog(@"파일의 30일 이후 날짜 초 : %f" , seconds);
    return seconds;
}

//오늘 날짜 초값으로 리턴
-(float)getTodaySeconds{
    //오늘 날짜
    NSDate *today = [NSDate date];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyyMMdd";
    NSString *dateString = [dateFormatter stringFromDate:today];
    NSLog(@"오늘 날짜 : %@" , dateString);
    NSDate *dateDate = [dateFormatter dateFromString:dateString];
    NSTimeInterval seconds = [dateDate timeIntervalSinceReferenceDate];
    NSLog(@"오늘 날짜 초로 변환 : %f" , seconds);
    return seconds;
}

 

*파일

DeleteFileFromDate.zip
/ 0.06MB

날짜계산(특정시간 이후 날짜 구하기)

날짜와 시간을 위한 프로그래밍을 위해서는 기본적으로 NSDate를 사용한다.

NSDate는 2001년 1월 1일 자정을 기준으로 현재시간(특정)까지의 경과한 시간(초)을 저장하고 있는 객체이다.

두 시점의 과거 , 미래 시간을 비교하는 작업은 가능하나, 구체적인 날짜나 요일을 구하기는 어렵다.

(예 3월1일은 무슨요일인가 ?등)

날짜를 자유자재로 다룰려면 NSCalendar와 NSDateComponents 객체에 대해서 알아야 한다.

NSDateComponents는 몇 월, 몇 일 등 날짜 정보를 구성하는 달력에서 기본 단위들에 대한 정보를 얻는데 사용된다.

실제로 NSDate 객체와 NSDateComponents 객체를 NSCalendar 객체가 연결해준다.

*코드

    int milsec = 3600 * 24 * 30; //30일
    
    // 현재 시간을 저장한다.
    //NSDate *now = [[NSDate alloc] init];                                              // 생성
    NSDate *today = [NSDate date];                                                      //메소드를 이용해서 간단히 생성
    NSLog(@"오늘 날짜 '%@'.", today);
    //오늘 날짜 '2019-04-02 05:31:39 +0000'.

    // 현재 시간에  30일 시간 (3600 * 24 * 30) 더하기
    NSDate *afterDay = [today dateByAddingTimeInterval:milsec];
    NSLog(@"30일 이 후 날짜:  '%@'.", afterDay);
     //30일 이 후 날짜:  '2019-05-02 05:31:39 +0000'.
    
    //날짜 형식 지정
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMdd"];
    NSString *dateString = [formatter stringFromDate:afterDay];
    
    
    NSLog(@"30일 후 날짜 ?  : %@" ,dateString );
    //30일 후 날짜 ?  : 20190502
    
   //문자열 -> 날짜 형식으로
  // NSDate *someDay = [formatter dateFromString:dateString];
    

 

 

uuid 구하기(ios 기기 고유번호)

과거 iOS 기기의 고유 번호를 얻기 위해서 udid가 존재했었다.

하지만 개인정보 문제 등으로 ios5 부터는 udid가 사라지고 uuid(임의로 생성한 고유값)가 생겨났다.

애플도 uuid를 이용해서 기기 고유번호를 생성하라고 권고한다.

하지만 문제는 uuid도 앱을 삭제하면 새롭게 생성 된다.  고유값이라고   없다.

개인정보 문제로 부터 자유로워졌긴 하지만, 개발자들이  고유번호를 다루기가 까다로워졌다.

여러 방법이 있겠지만 uuid를 최초로 생성하고 keychain에 저장시켜주는 방법 찾아보았다. 

/*
     File: KeychainItemWrapper.m 
 Abstract: 
 Objective-C wrapper for accessing a single keychain item.
  
  Version: 1.2 - ARCified
  
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple 
 Inc. ("Apple") in consideration of your agreement to the following 
 terms, and your use, installation, modification or redistribution of 
 this Apple software constitutes acceptance of these terms.  If you do 
 not agree with these terms, please do not use, install, modify or 
 redistribute this Apple software. 
  
 In consideration of your agreement to abide by the following terms, and 
 subject to these terms, Apple grants you a personal, non-exclusive 
 license, under Apple's copyrights in this original Apple software (the 
 "Apple Software"), to use, reproduce, modify and redistribute the Apple 
 Software, with or without modifications, in source and/or binary forms; 
 provided that if you redistribute the Apple Software in its entirety and 
 without modifications, you must retain this notice and the following 
 text and disclaimers in all such redistributions of the Apple Software. 
 Neither the name, trademarks, service marks or logos of Apple Inc. may 
 be used to endorse or promote products derived from the Apple Software 
 without specific prior written permission from Apple.  Except as 
 expressly stated in this notice, no other rights or licenses, express or 
 implied, are granted by Apple herein, including but not limited to any 
 patent rights that may be infringed by your derivative works or by other 
 works in which the Apple Software may be incorporated. 
  
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE 
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 
  
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 
 POSSIBILITY OF SUCH DAMAGE. 
  
 Copyright (C) 2010 Apple Inc. All Rights Reserved. 
  
*/ 

#import "KeychainItemWrapper.h"
#import <Security/Security.h>

#if ! __has_feature(objc_arc)
#error THIS CODE MUST BE COMPILED WITH ARC ENABLED!
#endif

/*

These are the default constants and their respective types,
available for the kSecClassGenericPassword Keychain Item class:

kSecAttrAccessGroup			-		CFStringRef
kSecAttrCreationDate		-		CFDateRef
kSecAttrModificationDate    -		CFDateRef
kSecAttrDescription			-		CFStringRef
kSecAttrComment				-		CFStringRef
kSecAttrCreator				-		CFNumberRef
kSecAttrType                -		CFNumberRef
kSecAttrLabel				-		CFStringRef
kSecAttrIsInvisible			-		CFBooleanRef
kSecAttrIsNegative			-		CFBooleanRef
kSecAttrAccount				-		CFStringRef
kSecAttrService				-		CFStringRef
kSecAttrGeneric				-		CFDataRef
 
See the header file Security/SecItem.h for more details.

*/

@interface KeychainItemWrapper (PrivateMethods)
/*
The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was
to encapsulate the transition between what the detail view controller was expecting (NSString *) and what the
Keychain API expects as a validly constructed container class.
*/
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;

// Updates the item in the keychain, or adds it if it doesn't exist.
- (void)writeToKeychain;

@end

@implementation KeychainItemWrapper
{
    NSMutableDictionary *keychainItemData;		// The actual keychain item data backing store.
    NSMutableDictionary *genericPasswordQuery;	// A placeholder for the generic keychain item query used to locate the item.
}

- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
{
    if (self = [super init])
    {
        // Begin Keychain search setup. The genericPasswordQuery leverages the special user
        // defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain
        // items which may be included by the same application.
        genericPasswordQuery = [[NSMutableDictionary alloc] init];
        
		[genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        [genericPasswordQuery setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
		
		// The keychain access group attribute determines if this item can be shared
		// amongst multiple apps whose code signing entitlements contain the same keychain access group.
		if (accessGroup != nil)
		{
#if TARGET_IPHONE_SIMULATOR
			// Ignore the access group if running on the iPhone simulator.
			// 
			// Apps that are built for the simulator aren't signed, so there's no keychain access group
			// for the simulator to check. This means that all apps can see all keychain items when run
			// on the simulator.
			//
			// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
			// simulator will return -25243 (errSecNoAccessForItem).
#else			
			[genericPasswordQuery setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
		}
		
		// Use the proper search constants, return only the attributes of the first match.
        [genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
        [genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
        
        NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];
        
        CFMutableDictionaryRef outDictionary = NULL;
        
        if (!SecItemCopyMatching((__bridge CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
        {
            // Stick these default values into keychain item if nothing found.
            [self resetKeychainItem];
			
			// Add the generic attribute and the keychain access group.
			[keychainItemData setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
			if (accessGroup != nil)
			{
#if TARGET_IPHONE_SIMULATOR
				// Ignore the access group if running on the iPhone simulator.
				// 
				// Apps that are built for the simulator aren't signed, so there's no keychain access group
				// for the simulator to check. This means that all apps can see all keychain items when run
				// on the simulator.
				//
				// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
				// simulator will return -25243 (errSecNoAccessForItem).
#else			
				[keychainItemData setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
			}
		}
        else
        {
            // load the saved data from Keychain.
            keychainItemData = [self secItemFormatToDictionary:(__bridge NSDictionary *)outDictionary];
        }
		if(outDictionary) CFRelease(outDictionary);
    }
    
	return self;
}

- (void)setObject:(id)inObject forKey:(id)key 
{
    if (inObject == nil) return;
    id currentObject = [keychainItemData objectForKey:key];
    if (![currentObject isEqual:inObject])
    {
        [keychainItemData setObject:inObject forKey:key];
        [self writeToKeychain];
    }
}

- (id)objectForKey:(id)key
{
    return [keychainItemData objectForKey:key];
}

- (void)resetKeychainItem
{
	OSStatus junk = noErr;
    if (!keychainItemData) 
    {
        keychainItemData = [[NSMutableDictionary alloc] init];
    }
    else if (keychainItemData)
    {
        NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:keychainItemData];
		junk = SecItemDelete((__bridge CFDictionaryRef)tempDictionary);
        NSAssert( junk == noErr || junk == errSecItemNotFound, @"Problem deleting current dictionary." );
    }
    
    // Default attributes for keychain item.
    [keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrAccount];
    [keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrLabel];
    [keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrDescription];
    
	// Default data for keychain item.
    [keychainItemData setObject:@"" forKey:(__bridge id)kSecValueData];
}

- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
    // The assumption is that this method will be called with a properly populated dictionary
    // containing all the right key/value pairs for a SecItem.
    
    // Create a dictionary to return populated with the attributes and data.
    NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
    
    // Add the Generic Password keychain item class attribute.
    [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    
    // Convert the NSString to NSData to meet the requirements for the value type kSecValueData.
	// This is where to store sensitive data that should be encrypted.
    NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
    [returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
    
    return returnDictionary;
}

- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
    // The assumption is that this method will be called with a properly populated dictionary
    // containing all the right key/value pairs for the UI element.
    
    // Create a dictionary to return populated with the attributes and data.
    NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
    
    // Add the proper search key and class attribute.
    [returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
    [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    
    // Acquire the password data from the attributes.
    CFDataRef passwordData = NULL;
    if (SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary, (CFTypeRef *)&passwordData) == noErr)
    {
        // Remove the search, class, and identifier key/value, we don't need them anymore.
        [returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];
        
        // Add the password to the dictionary, converting from NSData to NSString.
        NSString *password = [[NSString alloc] initWithBytes:[(__bridge NSData *)passwordData bytes] length:[(__bridge NSData *)passwordData length] 
                                                     encoding:NSUTF8StringEncoding];
        [returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
    }
    else
    {
        // Don't do anything if nothing is found.
        NSAssert(NO, @"Serious error, no matching item found in the keychain.\n");
    }
	if(passwordData) CFRelease(passwordData);

	return returnDictionary;
}

- (void)writeToKeychain
{
    CFDictionaryRef attributes = NULL;
    NSMutableDictionary *updateItem = nil;
	OSStatus result;
    
    if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr)
    {
        // First we need the attributes from the Keychain.
        updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge NSDictionary *)attributes];
        // Second we need to add the appropriate search key/values.
        [updateItem setObject:[genericPasswordQuery objectForKey:(__bridge id)kSecClass] forKey:(__bridge id)kSecClass];
        
        // Lastly, we need to set up the updated attribute list being careful to remove the class.
        NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];
        [tempCheck removeObjectForKey:(__bridge id)kSecClass];
		
#if TARGET_IPHONE_SIMULATOR
		// Remove the access group if running on the iPhone simulator.
		// 
		// Apps that are built for the simulator aren't signed, so there's no keychain access group
		// for the simulator to check. This means that all apps can see all keychain items when run
		// on the simulator.
		//
		// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
		// simulator will return -25243 (errSecNoAccessForItem).
		//
		// The access group attribute will be included in items returned by SecItemCopyMatching,
		// which is why we need to remove it before updating the item.
		[tempCheck removeObjectForKey:(__bridge id)kSecAttrAccessGroup];
#endif
        
        // An implicit assumption is that you can only update a single item at a time.
		
        result = SecItemUpdate((__bridge CFDictionaryRef)updateItem, (__bridge CFDictionaryRef)tempCheck);
		NSAssert( result == noErr, @"Couldn't update the Keychain Item." );
    }
    else
    {
        // No previous item found; add the new one.
        result = SecItemAdd((__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);
		NSAssert( result == noErr, @"Couldn't add the Keychain Item." );
    }
	
	if(attributes) CFRelease(attributes);
}

@end

/*
 
 ios 기기의 고유 번호를 얻는 것은
 */




 

*KeychainItemWrapper

/*
     File: KeychainItemWrapper.h
 Abstract: 
 Objective-C wrapper for accessing a single keychain item.
 
  Version: 1.2 - ARCified
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2010 Apple Inc. All Rights Reserved.
 
*/

#import <UIKit/UIKit.h>

/*
    The KeychainItemWrapper class is an abstraction layer for the iPhone Keychain communication. It is merely a 
    simple wrapper to provide a distinct barrier between all the idiosyncracies involved with the Keychain
    CF/NS container objects.
*/
@interface KeychainItemWrapper : NSObject

// Designated initializer.
- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *)accessGroup;
- (void)setObject:(id)inObject forKey:(id)key;
- (id)objectForKey:(id)key;

// Initializes and resets the default generic keychain item data.
- (void)resetKeychainItem;

@end

 

*viewdidload 에서 호출해서 사용하기


#import "ViewController.h"
#import "KeychainItemWrapper.h"

@interface ViewController ()

@end

@implementation ViewController

//uuid 생성 및 리턴하는 함수
- (NSString*) getAPInfo
{
    //uuid 저장하기 위해서 키 체이닝 생성 및 초기화
    KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"UUID" accessGroup:nil];

    NSString *uuid = [wrapper objectForKey:(__bridge id)(kSecAttrAccount)];

    if( uuid == nil || uuid.length == 0)
    {
        //키체인에 uuid 없으면 만들어서 저장
        CFUUIDRef uuidRef = CFUUIDCreate(NULL);
        CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef);
        CFRelease(uuidRef);
        uuid = [NSString stringWithString:(__bridge NSString *) uuidStringRef];
        CFRelease(uuidStringRef);

        // 키체인에 uuid 저장
        [wrapper setObject:uuid forKey:(__bridge id)(kSecAttrAccount)];

    }

    return uuid;

}

//화면이 로딩될때 호출된다
- (void)viewDidLoad {
    [super viewDidLoad];
    //uuid 호출
    NSString *uuid = [self getAPInfo];
    NSLog(@"uuid 값: %@" , uuid);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

 

*결과

처음 설치했을때

3BE01CF3-E786-47DD-96B8-0B0895144B51

삭제후 재설치 했을때

3BE01CF3-E786-47DD-96B8-0B0895144B51

 

*파일

UUID.zip
0.07MB

 

*주의 : 키체인 쉐어링 꼭 체크를 해주자! ON !

 

 

카메라 줌 AVCaptureDevice Zoom

예전시간에 카메라 자동 회전 기능을 소개했다.

https://abc1211.tistory.com/615

그곳에 카메라 줌기능을 넣기 위해서 이것저것 찾아보고 시도해보았다.

처음에는 카메라 캡쳐화면을 실시간으로 보여주는 PreviewLayer에 pinchegesture를 걸어서

제스쳐 변화 값을 이용해서 zoom해주면 된다고 생각했는데 거의 오전 내내 삽질을 했다.

결론은 previewlayer에는 gesture 를 걸 수 없다. Avcapturedevice 를 이용해야 한다.

아래는 해당 소스이다. 주석을 달아 놓았다.

*제스쳐 함수

//self.view 에 pinches 제스쳐 이벤트를 걸었다. view 화면을 두 손가락으로 움직이면 AVCaptureDevice의 줌을 변경한다.
-(void) handlePinchToZoomRecognizer2:(UIPinchGestureRecognizer*) pinchRecognizer {
    
    static CGFloat zoomFactorBegin = 0.0;                                                                                                   //시작 줌 값
    
    //제스쳐를 처음 시작했을때
    if ( UIGestureRecognizerStateBegan == pinchRecognizer.state ) {
        zoomFactorBegin = self.inputDevice.videoZoomFactor;                                                                      //     1.0
         NSLog(@"제스쳐 시작- 줌 값 : %f" , zoomFactorBegin);                                                                          //     1.0
         NSLog(@"제스쳐 시작 맥스- 줌 값 : %f" , self.inputDevice.activeFormat.videoMaxZoomFactor);         //     16.0
        
    //제스쳐  변화중 상태 값
    } else if (UIGestureRecognizerStateChanged == pinchRecognizer.state) {
        NSError *error = nil;
        
        //*lockForConfiguration
        //*설명: 아이폰의 하드웨어 속성을 변경하기 전에 이 메서드를 호출해야 한다.
        // 이 메서드를 호출 한 후 잠금을 해제 하고, 설정을 변경 후 잠금을 해준다.
        if ([self.inputDevice lockForConfiguration:&error]) {
            
            NSLog(@"제스쳐 변화- zoomFactorBegin줌 값 1 : %f" , zoomFactorBegin);                                          //1.0
            NSLog(@"제스쳐 pinchRecognizer.scale-  값  : %f" , pinchRecognizer.scale);                                     //1.02334~ 1.43343
            
            CGFloat desiredZoomFactor = zoomFactorBegin * pinchRecognizer.scale;
            
            NSLog(@"제스쳐 desiredZoomFactor- 줌 값  : %f" , desiredZoomFactor);                                             //1.02334~ 1.43343
            NSLog(@"제스쳐 변화 맥스- 줌 값 : %f" , self.inputDevice.activeFormat.videoMaxZoomFactor);           //16
            
            //*activeFormat : 활성화된 캡쳐 장치의 미디어 데이터 형식. 이 속성을 사용하여 현재 활성된 장치의 형식을 가져오거나 설정한다.
            //*videoMaxZoomFactor :  최대 zoom 값을 가져왔다.
            
            
            //zoomFactor의 최소 값이 1.0 이상이도록 설정.
            CGFloat zoomFactor = MAX(1.0, MIN(desiredZoomFactor, self.inputDevice.activeFormat.videoMaxZoomFactor));
            
             NSLog(@"제스쳐 zoomFactor- 줌 값  : %f" , zoomFactor);                                                                   //1.02334~ 1.43343
            
            //현재 확대,축소 비율에서 변경된 확대,축소 비율로 부드럽게 전환해주는 메소드
            [self.inputDevice rampToVideoZoomFactor:zoomFactor withRate:3.0];
            
            //하드웨어 속성을 잠궈준다.
            [self.inputDevice unlockForConfiguration];
        } else {
            NSLog(@"error: %@", error);
        }
    }
}

 

*viewdidload 함수 안

    //카메라 줌 제스쳐
    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchToZoomRecognizer2:)];
    [self.view addGestureRecognizer:pinchGesture];

 

*파일

zoom.zip
0.08MB

core audioqueue player 에 대하여 2(추가설명)

 

지난번 글에서 오디오 큐로 오디오 플레이어를 구현하는 법을 포스팅 했다테스트를 하면서 확인해 본결과  오디오 파일을 읽을 읽을 패킷 값을 UInt32 numPackets = 10*1000;  이런식으로 고정값으로 해놓으니 런타임이 오디오에서는 콜백을 타지 않는 현상이 발견했다. 그래서  DeriveBufferSize 함수를 만들어서 버퍼 사이즈와 한번에 읽을 패킷 수를 유동적으로 구하기로 했다.

지난번에 로그를 확인하는 시간을 안가졌다오디오 큐가 오디오 파일을 어떻게 읽는지, 읽은 값은 어떻게 되는지. 오디오 파일을 읽은 오디오 큐가  콜백 함수를 어떻게 호출 하는지.. 콜백 함수 내부에서는 어떤일이 일어나는지 등등 로그를 통해서 알아보자.

6 크기의 caf 오디오 파일 사용했다.

[prepare 버튼눌렀을때]

아래 로그를 보면 오디오 큐가 읽을 오디오 파일을 열고 파일을 읽은 다음 전체 프레임 사이즈와 전체 프레임, 최대 패킷 크기, 버퍼의 사이즈, 읽을 패킷 수를 결정한다그리고 오디오에서 읽은 데이터를 버퍼에 3 할당해 준다 과정에서 콜백 함수에서, 오디오 파일의 데이터를 버퍼에 넣주기(버퍼에 읽은 데이터의 바이트 할당) -> 버퍼를 큐에 넣기 -> 큐는 버퍼의 오디오 데이터를 출력장치로 보냄.이런 과정이 발생한다. 

2019-03-29 16:59:35.750459+0900 ApplePlayer[1847:308425] open file file:///var/containers/Bundle/Application/45956846-0198-496B-BC44-9FA04D7E7650/ApplePlayer.app/01.caf success!

2019-03-29 16:59:35.750704+0900 ApplePlayer[1847:308425] outDataSize( 몇초??) : 6

2019-03-29 16:59:35.750885+0900 ApplePlayer[1847:308425] sizeOfTotalFrames : 8

2019-03-29 16:59:35.750906+0900 ApplePlayer[1847:308425] totalFrames : 264600

2019-03-29 16:59:35.752062+0900 ApplePlayer[1847:308425] AudioFileGetProperty - result : 0

2019-03-29 16:59:35.752092+0900 ApplePlayer[1847:308425] maxPacketSize : 4

2019-03-29 16:59:35.754947+0900 ApplePlayer[1847:308425] propertySize : 4

2019-03-29 16:59:35.754998+0900 ApplePlayer[1847:308425] pAqData.bufferByteSize : 88200

2019-03-29 16:59:35.755024+0900 ApplePlayer[1847:308425] pAqData.mNumPacketsToRead : 22050

 

2019-03-29 16:59:35.755384+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 16:59:35.755422+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 16:59:35.756181+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 16:59:35.756226+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 16:59:35.756258+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 0

2019-03-29 16:59:35.756288+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 16:59:35.756318+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 16:59:35.759313+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 22050

 

2019-03-29 16:59:35.759505+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 16:59:35.759539+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 16:59:35.760260+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 16:59:35.760307+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 16:59:35.760339+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 22050

2019-03-29 16:59:35.760369+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 16:59:35.760400+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 16:59:35.760630+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷킷수  -  : 22050

2019-03-29 16:59:35.760400+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 16:59:35.760630+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷\354\235 인덱스 -3  : 44100

 

2019-03-29 16:59:35.763228+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 16:59:35.763270+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 16:59:35.764145+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 16:59:35.764195+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 16:59:35.764219+0900 ApplePlayer[1847:308425] 킷수  -  : 22050

2019-03-29 16:59:35.760400+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 16:59:35.760630+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷\354\235 인덱스 -3  : 44100

2019-03-29 16:59:35.763228+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 16:59:35.763270+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 16:59:35.764145+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 16:59:35.764195+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 16:59:35.764219+0900 ApplePlayer[1847:308425] \354오디오 파일에서 읽을 패킷의 인덱스 -  : 44100

2019-03-29 16:59:35.764241+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 16:59:35.764265+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 16:59:35.764380+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 66150

======================================================================================================

[Play 버튼 눌렀을 때]

이곳에서는 값들의 변화를 자세히 살펴보자파일에서 읽은 바이트 값은 88200 동일하다패킷인덱스는 계속 증가한다. 얼만틈?   22050만큼 그럼 숫자는 어디서 나왔나pAqData.mNumPacketsToRead : 22050  오디오 큐가 파일 내용을 읽을 이미 위에서 계산되었다.  이런식으로 콜백함수는 파일에서 읽은 바이트 값을 버퍼에 넣고 패킷을 읽고 늘려가면서 버퍼의 데이터를 출력장치로 보낸다. ,

2019-03-29 17:00:02.458513+0900 ApplePlayer[1847:308425] 플레이 버튼 클릭

2019-03-29 17:00:03.096833+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:03.097001+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:03.099052+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:03.099208+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 17:00:03.099331+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 66150

2019-03-29 17:00:03.099404+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 17:00:03.099475+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 17:00:03.099984+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 88200

 

2019-03-29 17:00:03.587594+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:03.587771+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:03.590197+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:03.590332+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 17:00:03.590405+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 88200

2019-03-29 17:00:03.590473+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 17:00:03.590543+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 17:00:03.590899+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 110250

 

2019-03-29 17:00:04.098978+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:04.099032+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:04.100639+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:04.100678+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 17:00:04.100695+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 110250

2019-03-29 17:00:04.100717+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 17:00:04.100791+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 17:00:04.100948+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 132300

 

2019-03-29 17:00:04.590124+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:04.590293+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:04.592524+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:04.592640+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 17:00:04.592800+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 132300

2019-03-29 17:00:04.592935+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 17:00:04.593011+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 17:00:04.593414+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 154350

(생략)

2019-03-29 17:00:07.086121+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:07.086292+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:07.088785+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:07.088933+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 88200

2019-03-29 17:00:07.089010+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 242550

2019-03-29 17:00:07.089080+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 22050

2019-03-29 17:00:07.089152+0900 ApplePlayer[1847:308425]  오디오 버퍼에 읽은 데이터의 바이트 수를 할당한 : 88200

2019-03-29 17:00:07.089590+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -3  : 264600

 

(실제로 읽힌 패킷수 0) 세번

 

2019-03-29 17:00:07.598063+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:07.598231+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:07.598313+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:07.598385+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 0

2019-03-29 17:00:07.598453+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 264600

2019-03-29 17:00:07.598519+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 0

 

 

2019-03-29 17:00:08.090019+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:08.090202+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:08.090295+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:08.090367+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 0

2019-03-29 17:00:08.090435+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 264600

2019-03-29 17:00:08.090501+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 0

 

 

2019-03-29 17:00:08.624193+0900 ApplePlayer[1847:308425] HandleOutputBuffer 콜백함수 진입 - !

2019-03-29 17:00:08.624527+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -1 : 88200

2019-03-29 17:00:08.624633+0900 ApplePlayer[1847:308425] 오디오 파일의 바이트 데이터를 버퍼에 넣었다.

2019-03-29 17:00:08.624709+0900 ApplePlayer[1847:308425] 파일에서 읽은 바이트 -2 : 0

2019-03-29 17:00:08.624827+0900 ApplePlayer[1847:308425] 오디오 파일에서 읽을 패킷의 인덱스 -  : 264600

2019-03-29 17:00:08.624897+0900 ApplePlayer[1847:308425] 실제로 읽힌 패킷수  -  : 0

 

뷰화면

 

소스코드

/*
 - 순서
 1. 상태, 형식 및 경로 정보를 관리하는 사용자 지정구조를 정의한다.
 2. 실제 재생을 수행하는 오디오 큐 콜백 함수를 작성한다.
 3.오디오 큐 버퍼에 적합한 크기를 결정하는 코드를 작성한다.
 4.재생할 오디오 파일을 열고 오디오 데이터 형식을 결정한다.
 5. 재생오디오 큐를 생성하고 재생하기 위해서 셋팅한다.
 6. 오디오 큐 버퍼를 할당하고, 오디오 큐에 넣는다. 오디오 큐에 재생이 시작될 것을 알린다.
 재생이 완료되면 재생 콜백은 오디오 큐에 중지를 알린다.
 7. 오디오 큐 삭제, 리소스 해제하기
 */

#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreAudioKit/CoreAudioKit.h>

//step 1. 시작 전 오디오 형식 및 오디오 큐 상태 정보를 관리하는 데 사용할 사용자 지정 구조를 정의 해야 한다.

static const int kNumberBuffers = 3;                                               //1. 사용할 오디오 큐 버퍼 수 정하기

struct AQPlayerState {
    AudioStreamBasicDescription mDataFormat;                            // 2.파일의 오디오 데이터 형식을 나타냄, mQueue 필드에 지정된 오디오 큐에 사용된다.
    AudioQueueRef mQueue;                                                            // 3. app에서 생성하는 재생 오디오 큐
    AudioQueueBufferRef mBuffers [kNumberBuffers];                  // 4.오디오 큐가 관리하는 오디오 큐 버퍼의 포인터를 보관하는 배열
    AudioFileID mAudioFile;                                                               // 5. 프로그램에서 재생하는 오디오 파일을 나타내는 오디오 파일 객체.
    UInt32 bufferByteSize;                                                                 // 6.각 오디오 큐 버퍼의 크기 (바이트)
    SInt64 mCurrentPacket;                                                               // 7.오디오 파일에서 재생할 다음 패킷의 패킷 인덱스
    UInt32 mNumPacketsToRead;                                                      // 8.오디오 큐의 재생 콜백이 호출될때마다 읽을 패킷의 수
    AudioStreamPacketDescription * mPacketDescs;                      // 9. vbr일 경우 재생중인 파일에 대한 패킷 설명. cbr일 경우 null
    bool mIsRunning;                                                                          // 10. 오디오 큐가 실행중인지 여부를 나타내는 부울 값.
};

@interface ViewController : UIViewController


@end

 

#import "ViewController.h"

#define VErr(err, msg)  do {\
if(nil != err) {\
NSLog(@"[ERR]:%@--%@", (msg), [err localizedDescription]);\
return ;\
}\
} while(0)


#define VStatus(err, msg) do {\
if(noErr != err) {\
NSLog(@"[ERR-%d]:%@", err, (msg));\
return ;\
}\
} while(0)


#define VStatusBOOL(err, msg) do {\
if(noErr != err) {\
NSLog(@"[ERR-%d]:%@", err, (msg));\
return NO;\
}\
} while(0)



//step2. 재생 오디오 큐 콜백 함수 작성
// 기능
//* 오디오 파일에서 지정된 양의 데이터를 읽어서 오디오 버퍼에 삽입.
//* 오디오 버퍼를 큐에 넣는다.
//* 오디오 파일에서 읽을 데이터가 없을 때 오디오 대기열에 중지 할 것을 알린다.
static void HandleOutputBuffer (
                                                        void * aqData,                                   // 1. 오디오 큐에 대한 상태 정보
                                                        AudioQueueRef inAQ,                      // 2. 이 콜백을 소유한 오디오 큐
                                                        AudioQueueBufferRef inBuffer       // 3. 콜백이 오디오 파일에서 읽은 데이터를 넣을 버퍼.
){
    

    NSLog(@"HandleOutputBuffer 콜백함수 진입 - !");
   
    //step 2-1. 오디오 큐에 대한 상태 정보
      struct AQPlayerState *pAqData = (struct AQPlayerState *) aqData;
    
    
    //NSLog(@"pAqData->mIsRunning : %@" , pAqData->mIsRunning ? @"yes" : @"no"); //no 면 정지 됨
   // if (pAqData->mIsRunning == 0) return;                                               //. 오디오 큐가 중지되면 리턴.
    
    
    UInt32 numBytesReadFromFile  = pAqData->bufferByteSize;          //. 재생중인 파일에서 읽은 오디오 데이터의 바이트 값을 가지는 변수
    //UInt32 numPackets = 10*1000;
    UInt32 numPackets = pAqData->mNumPacketsToRead;                 //. 재생중인 파일에서 읽을 패킷수.  -
    
    NSLog(@"파일에서 읽은 바이트 값 -1 : %d" , (int)numBytesReadFromFile);
  
    //step 2-2. 오디오 파일의 데이터를 판독해서 버퍼에 넣는 역할 하는 함수 작성 (오디오 데이터 -> 버퍼)
    OSStatus stts =  AudioFileReadPacketData (
                                              pAqData->mAudioFile,                         //2. 읽을 오디오 파일
                                              NO,                                                        // 3. 함수가 읽을 때 데이터를 캐시 (x)
                                              &numBytesReadFromFile,                   // 4. 출력시 오디오 파일에서 읽은 오디오 데이터의 바이트 수.
                                              pAqData->mPacketDescs,                  // 5. 출력시 오디오 파일에서 읽은 데이터에 대한 패킷의 설명. cbr = null
                                              pAqData->mCurrentPacket,               // 6. 오디오 파일에서 읽을 첫번째 패킷의 인덱스.
                                              &numPackets,                                      // 7. 입력시 오디오 파일에서 읽을 패킷수. 출력시 실재로 읽힌 패킷수.
                                              inBuffer->mAudioData                        // 8. 출력시 오디오 파일에서 읽은 데이터가 들어 있는 버퍼.
                                                                            );
    
    VStatus(stts, @"AudioFileReadPacketData");
    NSLog(@"오디오 파일의 바이트 데이터를 버퍼에 넣었다.");
    NSLog(@"파일에서 읽은 바이트 값 -2 : %d" , (int)numBytesReadFromFile);
    NSLog(@"오디오 파일에서 읽을 패킷의 인덱스 -  : %d" , (int)pAqData->mCurrentPacket);
    NSLog(@"실제로 읽힌 패킷수  -  : %d" , (int)numPackets);
    
    if (numPackets> 0) {                                                                            //.파일로 부터 오디오 데이터를 읽었는지 여부. 읽었다면 버퍼를 큐에 할당. 못읽었다면 중지.
        
        inBuffer-> mAudioDataByteSize = numBytesReadFromFile;        //. 오디오 큐 버퍼에 읽은 데이터의 바이트 수를 할당.

        NSLog(@" 오디오 큐 버퍼에 읽은 데이터의 바이트 수를 할당한 값 : %d" ,  (int)inBuffer-> mAudioDataByteSize);
      
        //step 2-3. 오디오 파일 데이터가 읽혀서 오디오 큐 버퍼에 저정되어 있는 상태이다. (버퍼 -> 큐)
        //아래 함수 처럼, 콜백이 버퍼를 큐에 넣는다. 큐에 들어가면 버퍼의 오디오 데이터를 큐에서 출력장치로 보낼 수 있다.
        //버퍼를 큐에 추가하는 함수
        AudioQueueEnqueueBuffer (
                                 pAqData-> mQueue,                                                // 2. 버퍼가 담긴 큐를 소유하고 있는 오디오 큐
                                 inBuffer,                                                                    // 3. 큐에 넣을 오디오 버퍼.
                                 (pAqData-> mPacketDescs? numPackets : 0),      // 4. 오디오 큐 버퍼의 데이터  패킷의 수. cbr = 0
                                 pAqData-> mPacketDescs                                       // 5
                                 );
        
        
        pAqData-> mCurrentPacket += numPackets;                                 //.읽은 패킷 수에 따라 패킷 인덱스 증가.
        
        NSLog(@"오디오 파일에서 읽을 패킷의 인덱스 -3  : %d" , (int)pAqData->mCurrentPacket);
     
    } else {
        
        //콜백이 하는 마지막 작업은 재생중인 오디오가 더 이상 읽을 데이터가 없는지 확인하는 것이다.
        //파일의 끝을 발견하면 콜백은 재생 오디오 큐에 멈추라고 이야기 한다.
        //AudioFileReadPackets함수에 의해 읽힌 패킷의 수가 콜백에 의해 이전에 호출 되었는지를 검사한다  패킷이 0 이면 읽을 파일 x.
        AudioQueueStop (                                                    // 2.오디오 큐를 정지하는 함수
                                        pAqData-> mQueue,               // 3.중지할 오디오 큐
                                        false                                         //  4.큐에 저장된 모든 버퍼가 재생되면 오디오 큐를 비동기 적으로 중지한다.
                                    );
        
        pAqData-> mIsRunning = false;                           // 5. 재생이 완료되었음을 나타낸다.
    }
}

//오디오 데이터를 오디오 큐에 전달하기 위해서는 AudioQueueBuffer 라는 객체를 이용한다.
//이 버퍼를 생성하기 전에 오디오 데이터를 분석한다.
//그 후, 어떤 사이즈의 버퍼를 사용할지 결정한다.
// 또, 한번에 몇개의 packet을 읽을지 결정해야 한다.
//아래 함수는 사용할 버퍼의 사이즈를 outBufferSize에 저장하고, 읽을 패킷의 개수를 outNumPacketsToRead에 저장한다.
void DeriveBufferSize (
                       AudioStreamBasicDescription ASBDesc,
                       UInt32                      maxPacketSize,
                       Float64                     seconds,
                       UInt32                      *outBufferSize,
                       UInt32                      *outNumPacketsToRead
                       )
{
    static const int maxBufferSize = 0x50000;                                                                   // 320KB
    static const int minBufferSize = 0x4000;                                                                     //  16KB
    
    if (ASBDesc.mFramesPerPacket != 0) {                                                                        // 1 . 오디오 데이터의 ASBD의 패킷당 프레임 개수가 일정하다는 뜻.
        
        //주어진 시간에 처리할 수 있는 패킷의 개수(numPacketsForTime)를 구할 수 있다.
        //주어진 시간에 필요한 버퍼의 크기(outBufferSize)를 계산할 수 있다.
        Float64 numPacketsForTime = ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;
        *outBufferSize = numPacketsForTime * maxPacketSize;
        
    } else {                                                                                                                             // 2. 패킷당 프레임 개수가 다르다면
       
        *outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;  //maxPacketSize는 (AudioFileGetProperty 함수로 구함.)
    }
    
    if (*outBufferSize > maxBufferSize &&                                                                       // 3.얻은값이 너무 크다면 maxBufferSize에 맞춰준다.
        *outBufferSize > maxPacketSize
        )
        *outBufferSize = maxBufferSize;
    
    else {                                                                                                                              // 4. 얻은 값이 너무 작다면 minBufferSize에 맞춰 준다.
        if (*outBufferSize < minBufferSize){
            *outBufferSize = minBufferSize;
        }
    }
    
    *outNumPacketsToRead = *outBufferSize / maxPacketSize;                                    // 5.버퍼의 크기와 최대 패킷 크기를 알면 콜백함수가 한번에 읽을 수 있는 패킷 개수를 구할 수 있다.
                                                                                                                                          //maxPacketSize는 (AudioFileGetProperty 함수로 구함.).  4,   1052
}

@interface ViewController (){
    struct AQPlayerState pAqData;
}
@property (weak, nonatomic) IBOutlet UILabel *sizeOfTotalFrame;     //전체 프레임 크기
@property (weak, nonatomic) IBOutlet UILabel *totalFrame;               //전체 프레임
@property (weak, nonatomic) IBOutlet UILabel *maxPacketSize;       // 최대 패킷 사이즈
@property (weak, nonatomic) IBOutlet UILabel *bufferByteSize;       //버퍼의 크기
@property (weak, nonatomic) IBOutlet UILabel *packetToRead;         //읽을 패킷
@property (weak, nonatomic) IBOutlet UITextView *logs;

@property (weak, nonatomic) IBOutlet UIButton *play;
@property (weak, nonatomic) IBOutlet UIButton *stop;
@property (weak, nonatomic) IBOutlet UIButton *prepare;
@property (weak, nonatomic) IBOutlet UILabel *totalDuration;        //전체 길이 (초)

@property (strong, nonatomic) NSURL *musicURL;                          //오디오 파일 경로
@end

@implementation ViewController

//step3. AVAudioSession 설정 -AVAudioSessionCategorySoloAmbient 로 설정.
- (void) setAudioSession {
    
    NSError *error;
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setActive:YES error:&error];
   
    if (nil != error) {
        NSLog(@"AudioSession setActive 에러 발생:%@", error.localizedDescription);
        return;
    }
    
    error = nil;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:&error];
    if (nil != error) {
        NSLog(@"AudioSession setCategory(AVAudioSessionCategoryPlayAndRecord) 에러 발생:%@", error.localizedDescription);
        return;
    }
}


- (void)viewDidLoad {
    [super viewDidLoad];
    _logs.editable = NO; //텍스트 뷰  readonly
}

//오디오 파일 준비
- (BOOL) prepareAudioFile {
    //step 3-1 읽을 오디오 파일 셋팅 및 초기화
    [self setAudioSession];
    
    //step4.오디오 파일에 대한 CFURL 객체 가져 오기
    CFURLRef audioFileURL = (__bridge CFURLRef) _musicURL;
    //nill 이면 로컬에 있는 파일 재생하기
    if (nil == audioFileURL) {
        audioFileURL = (__bridge CFURLRef) [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"01" ofType:@"caf"]];
    }
    
    
    //step5. 재생을 위해 오디오 파일열기
    OSStatus result =
    AudioFileOpenURL (                                                                                  // 2. 재생할 파일을 여는 함수
                      audioFileURL,                                                                          // 3.  재생할 파일 참조
                      kAudioFileReadPermission,                                                     //4.  재생 중인 파일에 사용할 파일 권한. File Access Permission Constants열거 형에 정의되어있음.
                      0,                                                                                               //5. 파일 타입 관련 옵션. 0은 이 기능을 사용하지 않겠다.
                      &pAqData.mAudioFile                                                              //6. 출력시 오디오 파일에 대한 참조가 사용자 정의 구조 mAudioFile 필드에 배치 된다.
                      );
    
    //CFRelease (audioFileURL);                                                                     //7. 위에서 만든 CFURL 개체 해제.
    VStatusBOOL(result, @"AudioFileOpenURL");
    NSLog(@"open file %@ success!", audioFileURL);
    
    //step6. 파일의 오디오 데이터 형식 얻기
    UInt32 dataFormatSize = sizeof (pAqData.mDataFormat);                                // 1. 오디오 파일의 오디오 데이터 형식을 쿼리 할 때 사용할 예상 속성 값 크기를 가져온다.
    NSLog(@"dataFormatSize : %u" , (unsigned int)dataFormatSize);
    result = AudioFileGetProperty (                                                                             //2. 오디오 파일의 지정된 속성의 값을 획득
                                   pAqData.mAudioFile,                                                               //3. AudioFileID 오디오 데이터 형식을 가져올 파일을 나타내는 오디오 파일 객체
                                   kAudioFilePropertyDataFormat,                                             //4. 오디오 파일의 데이터 형식 값을 얻기 위한 속성 ID.
                                   &dataFormatSize,                                                                    //5. 입력시 AudioStreamBasicDescription오디오 파일의 데이터 형식을 설명하는 구조체의 예상 크기. 출력시 사용 x
                                   &pAqData.mDataFormat                                                          //6. 출력시 AudioStreamBasicDescription 오디오 파일에서 가져온 전체 오디오 데이터 형식을 구조체 형식으로 출력. 파일의 오디오 데이터 형식을 오디오 큐의 사용자 지정 구조에 저장하여 오디오 대기열에 적용
                                   );
    VStatusBOOL(result, @"AudioFileGetProperty - kAudioFilePropertyDataFormat");
    NSLog(@"result : %d" , (int)result);
    NSLog(@"dataFormatSize : %u" , (unsigned int)dataFormatSize);
    
    
    
    //step6-2. 파일의 오디오 길이 얻기
    Float64 outDataSize = 0;
    UInt32 thePropSize = sizeof(Float64);
    result = AudioFileGetProperty(
                                                      pAqData.mAudioFile,
                                                      kAudioFilePropertyEstimatedDuration,
                                                      &thePropSize,
                                                      &outDataSize
                                                    );
    
    VStatusBOOL(result, @"AudioFileGetProperty - duration");
    NSLog(@"result : %d" , (int)result);
    NSLog(@"thePropSize : %u" , (unsigned int)thePropSize);
    NSLog(@"outDataSize( 몇초??) : %d" , (int)outDataSize);
    NSString *myOutDataSizeString =  [[NSNumber numberWithInt: (int)outDataSize] stringValue];
    [_totalDuration setText:myOutDataSizeString];                                                       //전체 길이에 셋팅해주기
    
    
     //step6-3. 파일의 전체 오디오 데이터 패킷
    UInt64 totalFrames;     //전체 패킷
    UInt32 sizeOfTotalFrames = sizeof(UInt64);
    AudioFileGetProperty (
                                          pAqData.mAudioFile,
                                          kAudioFilePropertyAudioDataPacketCount,
                                          &sizeOfTotalFrames,
                                          &totalFrames
                                        );
    VStatusBOOL(result, @"kAudioFilePropertyAudioDataPacketCount");
    NSLog(@"result : %d" , (int)result);
    NSLog(@"sizeOfTotalFrames : %d" , ( int)sizeOfTotalFrames);                         //
    NSLog(@"totalFrames : %d" , (int)totalFrames);                                                 //
    
    NSString *mySizeOfTotalFrames =  [[NSNumber numberWithInt:(int)sizeOfTotalFrames] stringValue];
    NSString *myTotalFrames =  [[NSNumber numberWithInt: (int)totalFrames] stringValue];
    [_sizeOfTotalFrame setText:mySizeOfTotalFrames];   //전체 프래레임 사이즈 셋팅
    [_totalFrame setText:myTotalFrames];                        //전체 프레임 셋팅
    
    
    //step7. 재생오디오 큐 만들기: 이전 단계에서 구성한 사용자 지정 구조 및 콜백과 재생할 파일의 오디오 데이터 형식을 사용한다.
    result = AudioQueueNewOutput (                                                           // 1
                                  &pAqData.mDataFormat,                                         // 2. 오디오 큐가 재생되도록 설정된 파일의 오디오 데이터 형식이다.
                                  HandleOutputBuffer,                                                // 3. 재생 오디오 큐와 함께 사용할 콜백 함수.
                                  &pAqData,                                                                // 4. 재생 오디오 큐의 사용자 지정 데이터 구조.
                                  CFRunLoopGetCurrent (),                                      // 5. 현재 실행중인 루프와 오디오 큐 재생 콜백이 호출되는 루프.
                                  kCFRunLoopCommonModes,                                // 6. 콜백을 호출 할 수 있는 실행 루프 모드.
                                  0,                                                                             // 7.
                                  &pAqData.mQueue                                                // 8. 출력시 새로 할당 된 재생 오디오 큐.
                                  );
    VStatusBOOL(result, @"AudioQueueNewOutput");
    

    //step 8. 재생 오디오 큐의 크기 설정 : 오디오 큐에 버퍼를 할당할때와 오디오 파일을 읽기 시작하기 전에 사용
    //설정방법: 오디오 큐의 버퍼 크기, 재생 오디오 큐 콜백 호출 마다 읽을 패킷 수, 버퍼 하나의 오디오 데이터 패킷 설명을 보관 유지 하기 위한 배열 사이즈
    //읽을 버퍼 크기 및 패킷 수 설정 : 여기에서 각 오디오 큐 버퍼의 크기를 설정하고, 재생 오디오 큐 콜백을 호출할때 마다 읽을 패킷 수를 결정한다.
    UInt32 maxPacketSize;
    UInt32 propertySize = sizeof (maxPacketSize);
    result = AudioFileGetProperty (                                                                                                // 1.  재생하려는 파일의 오디오 데이터 패킷 크기에 대한 max를 바이트 단위로 구하기
                                                       pAqData.mAudioFile,                                                             //2. 재생할 파일을 나타내는 오디오 파일 객체.
                                                       kAudioFilePropertyPacketSizeUpperBound,                       //3.오디오 파일 패킷 사이즈를 얻기위한 프로퍼티 id
                                                       &propertySize,                                                                    // 4. 출력시 kAudioFilePropertyPacketSizeUpperBound 속성의 크기
                                                       &maxPacketSize                                                                 // 5. 출력시 재생할 파일의 패킷 크기에 대한 max 값
                                   );
    VStatusBOOL(result, @"AudioFileGetProperty");
    
    
    //bufferByteSize 와 mNumberPacketToRead을 구한다.
    DeriveBufferSize (
                                  pAqData.mDataFormat,
                                  maxPacketSize,
                                  0.5,
                                  &pAqData.bufferByteSize,
                                  &pAqData.mNumPacketsToRead
                                );
    
    
    // kNumberPackages = 10*1000,
    //pAqData.bufferByteSize = (10*1000) * maxPacketSize;
    //pAqData.mPacketDescs =(AudioStreamPacketDescription *) malloc((10*1000) * sizeof(AudioStreamPacketDescription));
    
    NSLog(@"AudioFileGetProperty - result : %d" , (int)result);
    NSLog(@"maxPacketSize : %d" , (int)maxPacketSize);
    NSLog(@"propertySize : %d" , (int)maxPacketSize);
    NSLog(@"pAqData.bufferByteSize : %d" ,(int)pAqData.bufferByteSize);
     NSLog(@"pAqData.mNumPacketsToRead : %d" ,(int)pAqData.mNumPacketsToRead);
    
    NSString *myMaxPacketSize =  [[NSNumber numberWithInt:(int)maxPacketSize] stringValue];
    NSString *myBufferByteSize =  [[NSNumber numberWithInt:(int)pAqData.bufferByteSize] stringValue];
    NSString *myNumPacketsToRead =  [[NSNumber numberWithInt: (int)pAqData.mNumPacketsToRead] stringValue];
    [_maxPacketSize setText:myMaxPacketSize];                        //최대 패킷 사이즈
    [_bufferByteSize setText:myBufferByteSize];                        //버퍼 사이즈
     [_packetToRead setText:myNumPacketsToRead];                //읽을 패킷 수
    
    
        //step 9. 패킷 설명 배열에 메모리 할당
        //하나의 버퍼에 들어있는 오디오 데이터에 대한 패킷 설명을 보관할 배열의 메모리를 할당한다.
        //오디오 파일의 데이터 형식이 vbr인지 cbr인지 결정.  vbr 데이터에서 패킷당 바이트 수 or 패킷당 프레임 수 중 하나 또는 모두가 가변적이므로 0 이다.
        bool isFormatVBR = (pAqData.mDataFormat.mBytesPerPacket == 0 || pAqData.mDataFormat.mFramesPerPacket == 0);
    
        //vbr데이터가 포함된 오디오 파일의 경우 패킷 설명 배열에 메모리를 할당한다.
        if (isFormatVBR) {
            //재생 콜백을 호출 할 때마다 읽을 오디오 데이터 패킷 수에 따라 필요한 메모리를 계산한다.
            //pAqData.mPacketDescs =(AudioStreamPacketDescription *) malloc((10*1000) * sizeof(AudioStreamPacketDescription));
            pAqData.mPacketDescs = (AudioStreamPacketDescription *) malloc (pAqData.mNumPacketsToRead * sizeof (AudioStreamPacketDescription)); //apple 문서
        } else {
            pAqData.mPacketDescs = NULL;  //선형 pcm과 같은 cbr 데이터가 포함된 오디오 파일의 경우 오디오 대기열은 패킷 설명 배열을 사용하지 않는다.
        }
    
    
    //step 10. 매직 쿠키 설정
    //mpeg 4 aac 와 같은 일부 압축 오디오 포맷은 구조를 사용해서 오디오 메타 데이터를 포함한다. (이 구조를 매직 쿠키라고 함)
    //오디오 큐 서비스를 사용해서 이러한 형식의 파일을 재생할 때 재생을 시작하기 전에 오디오 파일에서 매직쿠키를 가져와서 오디오 큐에 추가.
    //아래는 매직 쿠키를 얻어 오디오 큐에 적용하는 방법. 재생을 시작하기 전에 이 함수를 호출 한다.
    
    UInt32 cookieSize = sizeof (UInt32);                                                                  //1. 매직 쿠키 데이터의 예상 크기 설정
    
    bool couldNotGetProperty =                                                                                //2. AudioFileGetPropertyInfo 함수의 결과를 캡쳐한다. 성공하면 false 반환.
    AudioFileGetPropertyInfo (                                                                                   //3.
                                              pAqData.mAudioFile,                                                   //4.재생할 오디오 파일을 나타내는 오디오 파일 객체.
                                              kAudioFilePropertyMagicCookieData,                        //5.오디오 파일의 매직 쿠키 데이터를 나타내는 속성 ID
                                              &cookieSize,                                                                 //6.입력시 마법 쿠키 데이터의 예상 크기, 출력시 실제 크기.
                                              NULL                                                                             //7.
                              );
    
    if (!couldNotGetProperty && cookieSize) {                                                             //8. 오디오 파일에 매직쿠키가 포함되어 있으면 이를 보관할 메모리 할당.
        
        char* magicCookie = (char *) malloc (cookieSize);
        
        AudioFileGetProperty (                                                                                          // 9. 파일의 매직 쿠키를 가져온다.
                                          pAqData.mAudioFile,                                                       // 10.재생할 오디오 파일을 나타내는 오디오 파일 객체.
                                          kAudioFilePropertyMagicCookieData,                             //11.오디오 파일의 마법 쿠키 데이터를 나타내는 속성 id
                                          &cookieSize,                                                                      //12.입력시 함수 매직 쿠키를 사용하여 얻은 변수의 크기, 출력시 매직 쿠키 변수에 기록된 바이트 크기.
                                          magicCookie                                                                       // 13. 출력시 오디오 파일의 매직쿠키.
                              );
        
        AudioQueueSetProperty (                                                                                // 14. 오디오 큐 속성 설정. 오디오 큐의 매직 쿠키를 설정하여 재생할 오디오 파일의 마법 쿠키와 일치 시킨다.
                               pAqData.mQueue,                                                                // 15. 매직 쿠키를 설정하려는 오디오 큐.
                               kAudioQueueProperty_MagicCookie,                                 // 16.오디오 큐의 매직 쿠키를 나타내는 속성 id
                               magicCookie,                                                                       // 17. 재생할 오디오 파일의 매직 쿠키
                               cookieSize                                                                            // 18. 매직 쿠키의 크기(바이트)
                               );
        
        free (magicCookie);                                                                                    // 19.매직 쿠키에 할당 된 메모리를 해제.
    }
    
    pAqData.mCurrentPacket = 0;                                                                   // 1. 오디오 큐 콜백이 버퍼를 채우기 시작할때, 오디오 파일의 시작 부분에서 시작되도록 패킷 인덱스를 0으로 설정.
    
    for (int i = 0; i < kNumberBuffers; ++i) {                                                   // 2. 오디오 큐 버퍼 세트를 할당 및 준비
        
        AudioQueueAllocateBuffer (                                                                  // 3. 이 함수는 메모리를 할당하여 오디오 큐 버퍼를 만듭니다.
                                                      pAqData.mQueue,                                    // 4. 오디오 큐 버퍼를 할당하고 있는 오디오 큐.
                                                      pAqData.bufferByteSize,                         // 5. 새로운 오디오 큐 버퍼의 사이즈.(바이트)
                                                      &pAqData.mBuffers[i]                             // 6. 출력시 mBuffers 사용자 지정 구조의 배열에 새 오디오 큐 버퍼를 추가한다.
                                                    );


        HandleOutputBuffer (                                                                                            // 7. 재생 오디오 큐 콜백.
                                            &pAqData,                                                                          // 8. 오디오 큐의 사용자 지정 데이터 구조
                                            pAqData.mQueue,                                                             // 9. 콜백하고 있는 오디오 큐
                                            pAqData.mBuffers[i]                                                         // 10. 오디오 큐 콜백에 전달할 오디오 큐 버퍼.
                                            );
    }
    
    Float32 gain = 1.0;                                                                             // 1. , 0 ~ 1.0 (무음~크게)
    
    // 선택적으로 사용자가 설정을 재정의 할 수 있음
    AudioQueueSetParameter (                                                                //2. 오디오 큐에 대한 매개 변수 값을 설정.
                            pAqData.mQueue,                                                      //3.  매개 변수를 설정하려는 오디오 큐.
                            kAudioQueueParam_Volume,                                    // 4. 설정된 매개 변수의 ID
                            gain                                                                             // 5. 오디오 대기열에 적용 할 gain 설정
                            );
    
    return YES;
}


- (IBAction)playBtn:(id)sender {
    NSLog(@"플레이 버튼 클릭");
    pAqData.mIsRunning = true;                                      //1. 큐가 실행중
    
    OSStatus stts = AudioQueueStart (                          // 2. 자체 스레드에서 오디오 큐 시작.
                     pAqData.mQueue,                                      // 3. 시작할 오디오 대기열.
                     NULL                                                           // 4.큐가 즉시 시작해야 함.
                     );
    
    do {                                                                                 // 5. mIsRunning을 정기적으로 체크해서 오디오 큐가 중지되었는지 확인.
        CFRunLoopRunInMode (                                            // 6. 오디오 큐의 스레드가 포함 된 실행 루프를 실행.
                            kCFRunLoopDefaultMode,                     // 7.실행 루프의 기본 모드 사용.
                            0.25,                                                       // 8. 실행 루프의 시간을 0.25초 단위로 설정
                            false                                                       // 9.실행루프가 지정된 전체 시간 동안 계속되어야 함.
                            );
    } while (pAqData.mIsRunning);
    
    CFRunLoopRunInMode (                                             // 10.오디오 큐가 중지된 후 실행 루프를 조금 더 오래 실행해서 현재 재생중인 오디오 대기열 버퍼에 완료 시간이 있는 지 확인.
                        kCFRunLoopDefaultMode,
                        1,
                        false
                        );
     VStatus(stts, @"AudioQueueStart");
}

//큐 정지
- (IBAction)stopBtn:(id)sender {
    OSStatus stts = AudioQueuePause(pAqData.mQueue);
    VStatus(stts, @"AudioQueuePause");
}

//준비 or 해제
- (IBAction)preparebtn:(id)sender {
    
    static BOOL once = NO;
    if (! once ) {
        if (! [self prepareAudioFile]) {
            NSLog(@"dipose error!");
            return ;
        }
        [_prepare setTitle:@"Dispose" forState:UIControlStateNormal];
        once = YES;
    } else {
        if (! [self dipose]) {
            NSLog(@"dipose error!");
            return ;
        }
        [_prepare setTitle:@"Prepare" forState:UIControlStateNormal];
        once = NO;
    }
    
}


- (BOOL) dipose {
    if (pAqData.mQueue) {
        AudioQueueDispose (                                          // 1. 오디오 큐와 해당 버퍼를 포함하여 모든 리소스 삭제.
                           pAqData.mQueue,                             // 2. 삭제할 큐
                           true                                                     // 3. 오디오 큐를 동기적으로 처리
                           );
    }
    
    if (pAqData.mAudioFile) {
        AudioFileClose (pAqData.mAudioFile);                 // 4. 재생 된 오디오 파일을 닫는다. * !주의 CFRelease 하면 이곳에서 에러남.
    }
    
    if (pAqData.mPacketDescs) {
        free (pAqData.mPacketDescs);                            // 5. 패킷 설명을 유지하는데 사용된 메모리 해제.
        pAqData.mPacketDescs = NULL;
    }
    
    return YES;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

 

예제파일

+ Recent posts