//
//  SCManagedVideoCapturer.m
//  Snapchat
//
//  Created by Liu Liu on 5/1/15.
//  Copyright (c) 2015 Liu Liu. All rights reserved.
//

#import "SCManagedVideoCapturer.h"

#import "NSURL+Asset.h"
#import "SCAudioCaptureSession.h"
#import "SCCameraTweaks.h"
#import "SCCapturerBufferedVideoWriter.h"
#import "SCCoreCameraLogger.h"
#import "SCLogger+Camera.h"
#import "SCManagedCapturer.h"
#import "SCManagedFrameHealthChecker.h"
#import "SCManagedVideoCapturerLogger.h"
#import "SCManagedVideoCapturerTimeObserver.h"

#import <SCAudio/SCAudioSession.h>
#import <SCAudio/SCMutableAudioSession.h>
#import <SCBase/SCMacros.h>
#import <SCCameraFoundation/SCManagedAudioDataSourceListenerAnnouncer.h>
#import <SCFoundation/SCAssertWrapper.h>
#import <SCFoundation/SCCoreGraphicsUtils.h>
#import <SCFoundation/SCDeviceName.h>
#import <SCFoundation/SCFuture.h>
#import <SCFoundation/SCLog.h>
#import <SCFoundation/SCQueuePerformer.h>
#import <SCFoundation/SCTrace.h>
#import <SCFoundation/UIImage+CVPixelBufferRef.h>
#import <SCImageProcess/SCSnapVideoFrameRawData.h>
#import <SCImageProcess/SCVideoFrameRawDataCollector.h>
#import <SCImageProcess/SnapVideoMetadata.h>
#import <SCLogger/SCCameraMetrics.h>
#import <SCLogger/SCLogger+Performance.h>
#import <SCLogger/SCLogger.h>

#import <SCAudioScope/SCAudioSessionExperimentAdapter.h>

@import CoreMedia;
@import ImageIO;

static NSString *const kSCAudioCaptureAudioSessionLabel = @"CAMERA";

// wild card audio queue error code
static NSInteger const kSCAudioQueueErrorWildCard = -50;
// kAudioHardwareIllegalOperationError, it means hardware failure
static NSInteger const kSCAudioQueueErrorHardware = 1852797029;

typedef NS_ENUM(NSUInteger, SCManagedVideoCapturerStatus) {
    SCManagedVideoCapturerStatusUnknown,
    SCManagedVideoCapturerStatusIdle,
    SCManagedVideoCapturerStatusPrepareToRecord,
    SCManagedVideoCapturerStatusReadyForRecording,
    SCManagedVideoCapturerStatusRecording,
    SCManagedVideoCapturerStatusError,
};

#define SCLogVideoCapturerInfo(fmt, ...) SCLogCoreCameraInfo(@"[SCManagedVideoCapturer] " fmt, ##__VA_ARGS__)
#define SCLogVideoCapturerWarning(fmt, ...) SCLogCoreCameraWarning(@"[SCManagedVideoCapturer] " fmt, ##__VA_ARGS__)
#define SCLogVideoCapturerError(fmt, ...) SCLogCoreCameraError(@"[SCManagedVideoCapturer] " fmt, ##__VA_ARGS__)

@interface SCManagedVideoCapturer () <SCAudioCaptureSessionDelegate>
// This value has to be atomic because it is read on a different thread (write
// on output queue, as always)
@property (atomic, assign, readwrite) SCManagedVideoCapturerStatus status;

@property (nonatomic, assign) CMTime firstWrittenAudioBufferDelay;

@end

static char *const kSCManagedVideoCapturerQueueLabel = "com.snapchat.managed-video-capturer-queue";
static char *const kSCManagedVideoCapturerPromiseQueueLabel = "com.snapchat.video-capture-promise";

static NSString *const kSCManagedVideoCapturerErrorDomain = @"kSCManagedVideoCapturerErrorDomain";

static NSInteger const kSCManagedVideoCapturerCannotAddAudioVideoInput = 1001;
static NSInteger const kSCManagedVideoCapturerEmptyFrame = 1002;
static NSInteger const kSCManagedVideoCapturerStopBeforeStart = 1003;
static NSInteger const kSCManagedVideoCapturerStopWithoutStart = 1004;
static NSInteger const kSCManagedVideoCapturerZeroVideoSize = -111;

static NSUInteger const kSCVideoContentComplexitySamplingRate = 90;

// This is the maximum time we will wait for the Recording Capturer pipeline to drain
// When video stabilization is turned on the extra frame delay is around 20 frames.
// @30 fps this is 0.66 seconds
static NSTimeInterval const kSCManagedVideoCapturerStopRecordingDeadline = 1.0;

static const char *SCPlaceholderImageGenerationQueueLabel = "com.snapchat.video-capturer-placeholder-queue";

static const char *SCVideoRecordingPreparationQueueLabel = "com.snapchat.video-recording-preparation-queue";

static dispatch_queue_t SCPlaceholderImageGenerationQueue(void)
{
    static dispatch_queue_t queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create(SCPlaceholderImageGenerationQueueLabel, DISPATCH_QUEUE_SERIAL);
    });
    return queue;
}

@interface SCManagedVideoCapturer () <SCCapturerBufferedVideoWriterDelegate>

@end

@implementation SCManagedVideoCapturer {
    NSTimeInterval _maxDuration;
    NSTimeInterval _recordStartTime;

    SCCapturerBufferedVideoWriter *_videoWriter;

    BOOL _hasWritten;
    SCQueuePerformer *_performer;
    SCQueuePerformer *_videoPreparationPerformer;
    SCAudioCaptureSession *_audioCaptureSession;
    NSError *_lastError;
    UIImage *_placeholderImage;

    // For logging purpose
    BOOL _isVideoSnap;
    NSDictionary *_videoOutputSettings;

    // The following value is used to control the encoder shutdown following a stop recording message.
    // When a shutdown is requested this value will be the timestamp of the last captured frame.
    CFTimeInterval _stopTime;
    NSInteger _stopSession;
    SCAudioConfigurationToken *_preparedAudioConfiguration;
    SCAudioConfigurationToken *_audioConfiguration;

    dispatch_semaphore_t _startRecordingSemaphore;

    // For store the raw frame datas
    NSInteger _rawDataFrameNum;
    NSURL *_rawDataURL;
    SCVideoFrameRawDataCollector *_videoFrameRawDataCollector;

    CMTime _startSessionTime;
    // Indicates how actual processing time of first frame. Also used for camera timer animation start offset.
    NSTimeInterval _startSessionRealTime;
    CMTime _endSessionTime;
    sc_managed_capturer_recording_session_t _sessionId;

    SCManagedVideoCapturerTimeObserver *_timeObserver;
    SCManagedVideoCapturerLogger *_capturerLogger;

    CGSize _outputSize;
    BOOL _isFrontFacingCamera;
    SCPromise<id<SCManagedRecordedVideo>> *_recordedVideoPromise;
    SCManagedAudioDataSourceListenerAnnouncer *_announcer;

    NSString *_captureSessionID;
    CIContext *_ciContext;
}

@synthesize performer = _performer;

- (instancetype)init
{
    SCTraceStart();
    return [self initWithQueuePerformer:[[SCQueuePerformer alloc] initWithLabel:kSCManagedVideoCapturerQueueLabel
                                                               qualityOfService:QOS_CLASS_USER_INTERACTIVE
                                                                      queueType:DISPATCH_QUEUE_SERIAL
                                                                        context:SCQueuePerformerContextCamera]];
}

- (instancetype)initWithQueuePerformer:(SCQueuePerformer *)queuePerformer
{
    SCTraceStart();
    self = [super init];
    if (self) {
        _performer = queuePerformer;
        _audioCaptureSession = [[SCAudioCaptureSession alloc] init];
        _audioCaptureSession.delegate = self;
        _announcer = [SCManagedAudioDataSourceListenerAnnouncer new];
        self.status = SCManagedVideoCapturerStatusIdle;
        _capturerLogger = [[SCManagedVideoCapturerLogger alloc] init];
        _startRecordingSemaphore = dispatch_semaphore_create(0);
    }
    return self;
}

- (void)dealloc
{
    SCLogVideoCapturerInfo(@"SCVideoCaptureSessionInfo before dealloc: %@",
                           SCVideoCaptureSessionInfoGetDebugDescription(self.activeSession));
}

- (SCVideoCaptureSessionInfo)activeSession
{
    return SCVideoCaptureSessionInfoMake(_startSessionTime, _endSessionTime, _sessionId);
}

- (CGSize)defaultSizeForDeviceFormat:(AVCaptureDeviceFormat *)format
{
    SCTraceStart();
    // if there is no device, and no format
    if (format == nil) {
        // hard code 720p
        return CGSizeMake(kSCManagedCapturerDefaultVideoActiveFormatWidth,
                          kSCManagedCapturerDefaultVideoActiveFormatHeight);
    }
    CMVideoDimensions videoDimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
    CGSize size = CGSizeMake(videoDimensions.width, videoDimensions.height);
    if (videoDimensions.width > kSCManagedCapturerDefaultVideoActiveFormatWidth &&
        videoDimensions.height > kSCManagedCapturerDefaultVideoActiveFormatHeight) {
        CGFloat scaleFactor = MAX((kSCManagedCapturerDefaultVideoActiveFormatWidth / videoDimensions.width),
                                  (kSCManagedCapturerDefaultVideoActiveFormatHeight / videoDimensions.height));
        size = SCSizeMakeAlignTo(SCSizeApplyScale(size, scaleFactor), 2);
    }
    if ([SCDeviceName isIphoneX]) {
        size = SCSizeApplyScale(size, kSCIPhoneXCapturedImageVideoCropRatio);
    }
    return size;
}

- (CGSize)cropSize:(CGSize)size toAspectRatio:(CGFloat)aspectRatio
{
    if (aspectRatio == kSCManagedCapturerAspectRatioUnspecified) {
        return size;
    }
    // video input is always in landscape mode
    aspectRatio = 1.0 / aspectRatio;
    if (size.width > size.height * aspectRatio) {
        size.width = size.height * aspectRatio;
    } else {
        size.height = size.width / aspectRatio;
    }
    return CGSizeMake(roundf(size.width / 2) * 2, roundf(size.height / 2) * 2);
}

- (SCManagedVideoCapturerOutputSettings *)defaultRecordingOutputSettingsWithDeviceFormat:
    (AVCaptureDeviceFormat *)deviceFormat
{
    SCTraceStart();
    CGFloat aspectRatio = SCManagedCapturedImageAndVideoAspectRatio();
    CGSize outputSize = [self defaultSizeForDeviceFormat:deviceFormat];
    outputSize = [self cropSize:outputSize toAspectRatio:aspectRatio];

    // [TODO](Chao): remove the dependency of SCManagedVideoCapturer on SnapVideoMetaData
    NSInteger videoBitRate = [SnapVideoMetadata averageTranscodingBitRate:outputSize
                                                              isRecording:YES
                                                              highQuality:YES
                                                                 duration:0
                                                               iFrameOnly:NO
                                                     originalVideoBitRate:0
                                                 overlayImageFileSizeBits:0
                                                        videoPlaybackRate:1
                                                            isLagunaVideo:NO
                                                        hasOverlayToBlend:NO
                                                               sourceType:SCSnapVideoFilterSourceTypeUndefined];
    SCTraceSignal(@"Setup transcoding video bitrate");
    [_capturerLogger logStartingStep:kSCCapturerStartingStepTranscodeingVideoBitrate];

    SCManagedVideoCapturerOutputSettings *outputSettings =
        [[SCManagedVideoCapturerOutputSettings alloc] initWithWidth:outputSize.width
                                                             height:outputSize.height
                                                       videoBitRate:videoBitRate
                                                       audioBitRate:64000.0
                                                   keyFrameInterval:15
                                                         outputType:SCManagedVideoCapturerOutputTypeVideoSnap];

    return outputSettings;
}

- (SCQueuePerformer *)_getVideoPreparationPerformer
{
    SCAssert([_performer isCurrentPerformer], @"must run on _performer");
    if (!_videoPreparationPerformer) {
        _videoPreparationPerformer = [[SCQueuePerformer alloc] initWithLabel:SCVideoRecordingPreparationQueueLabel
                                                            qualityOfService:QOS_CLASS_USER_INTERACTIVE
                                                                   queueType:DISPATCH_QUEUE_SERIAL
                                                                     context:SCQueuePerformerContextCamera];
    }
    return _videoPreparationPerformer;
}

- (void)prepareForRecordingWithAudioConfiguration:(SCAudioConfiguration *)configuration
{
    SCTraceStart();
    [_performer performImmediatelyIfCurrentPerformer:^{
        SCTraceStart();
        self.status = SCManagedVideoCapturerStatusPrepareToRecord;
        if (_audioConfiguration) {
            [SCAudioSessionExperimentAdapter relinquishConfiguration:_audioConfiguration performer:nil completion:nil];
        }
        __block NSError *audioSessionError = nil;
        _preparedAudioConfiguration = _audioConfiguration =
            [SCAudioSessionExperimentAdapter configureWith:configuration
                                                 performer:[self _getVideoPreparationPerformer]
                                                completion:^(NSError *error) {
                                                    audioSessionError = error;
                                                    if (self.status == SCManagedVideoCapturerStatusPrepareToRecord) {
                                                        dispatch_semaphore_signal(_startRecordingSemaphore);
                                                    }
                                                }];

        // Wait until preparation for recording is done
        dispatch_semaphore_wait(_startRecordingSemaphore, DISPATCH_TIME_FOREVER);
        [_delegate managedVideoCapturer:self
                            didGetError:audioSessionError
                                forType:SCManagedVideoCapturerInfoAudioSessionError
                                session:self.activeSession];
    }];
}

- (SCVideoCaptureSessionInfo)startRecordingAsynchronouslyWithOutputSettings:
                                 (SCManagedVideoCapturerOutputSettings *)outputSettings
                                                         audioConfiguration:(SCAudioConfiguration *)audioConfiguration
                                                                maxDuration:(NSTimeInterval)maxDuration
                                                                      toURL:(NSURL *)URL
                                                               deviceFormat:(AVCaptureDeviceFormat *)deviceFormat
                                                                orientation:(AVCaptureVideoOrientation)videoOrientation
                                                           captureSessionID:(NSString *)captureSessionID
{
    SCTraceStart();
    _captureSessionID = [captureSessionID copy];
    [_capturerLogger prepareForStartingLog];

    [[SCLogger sharedInstance] logTimedEventStart:kSCCameraMetricsAudioDelay
                                         uniqueId:_captureSessionID
                                    isUniqueEvent:NO];

    NSTimeInterval startTime = CACurrentMediaTime();
    [[SCLogger sharedInstance] logPreCaptureOperationRequestedAt:startTime];
    [[SCCoreCameraLogger sharedInstance] logCameraCreationDelaySplitPointPreCaptureOperationRequested];
    _sessionId = arc4random();

    // Set a invalid time so that we don't process videos when no frame available
    _startSessionTime = kCMTimeInvalid;
    _endSessionTime = kCMTimeInvalid;
    _firstWrittenAudioBufferDelay = kCMTimeInvalid;
    _audioQueueStarted = NO;

    SCLogVideoCapturerInfo(@"SCVideoCaptureSessionInfo at start of recording: %@",
                           SCVideoCaptureSessionInfoGetDebugDescription(self.activeSession));

    SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
    [_performer performImmediatelyIfCurrentPerformer:^{
        _maxDuration = maxDuration;
        dispatch_block_t startRecordingBlock = ^{
            _rawDataFrameNum = 0;
            // Begin audio recording asynchronously, first, need to have correct audio session.
            SCTraceStart();
            SCLogVideoCapturerInfo(@"Dequeue begin recording with audio session change delay: %lf seconds",
                                   CACurrentMediaTime() - startTime);
            if (self.status != SCManagedVideoCapturerStatusReadyForRecording) {
                SCLogVideoCapturerInfo(@"SCManagedVideoCapturer status: %lu", (unsigned long)self.status);
                // We may already released, but this should be OK.
                [SCAudioSessionExperimentAdapter relinquishConfiguration:_preparedAudioConfiguration
                                                               performer:nil
                                                              completion:nil];
                return;
            }
            if (_preparedAudioConfiguration != _audioConfiguration) {
                SCLogVideoCapturerInfo(
                    @"SCManagedVideoCapturer has mismatched audio session token, prepared: %@, have: %@",
                    _preparedAudioConfiguration.token, _audioConfiguration.token);
                // We are on a different audio session token already.
                [SCAudioSessionExperimentAdapter relinquishConfiguration:_preparedAudioConfiguration
                                                               performer:nil
                                                              completion:nil];
                return;
            }

            // Divide start recording workflow into different steps to log delay time.
            // And checkpoint is the end of a step
            [_capturerLogger logStartingStep:kSCCapturerStartingStepAudioSession];
            [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                             uniqueId:_captureSessionID
                                             stepName:@"audio_session_start_end"];

            SCLogVideoCapturerInfo(@"Prepare to begin recording");
            _lastError = nil;

            // initialize stopTime to a number much larger than the CACurrentMediaTime() which is the time from Jan 1,
            // 2001
            _stopTime = kCFAbsoluteTimeIntervalSince1970;

            // Restart everything
            _hasWritten = NO;

            SCManagedVideoCapturerOutputSettings *finalOutputSettings =
                outputSettings ? outputSettings : [self defaultRecordingOutputSettingsWithDeviceFormat:deviceFormat];
            _isVideoSnap = finalOutputSettings.outputType == SCManagedVideoCapturerOutputTypeVideoSnap;
            _outputSize = CGSizeMake(finalOutputSettings.height, finalOutputSettings.width);
            [[SCLogger sharedInstance] logEvent:kSCCameraMetricsVideoRecordingStart
                                     parameters:@{
                                         @"video_width" : @(finalOutputSettings.width),
                                         @"video_height" : @(finalOutputSettings.height),
                                         @"bit_rate" : @(finalOutputSettings.videoBitRate),
                                         @"is_video_snap" : @(_isVideoSnap),
                                     }];

            _outputURL = [URL copy];
            _rawDataURL = [_outputURL URLByAppendingPathExtension:@"dat"];
            [_capturerLogger logStartingStep:kSCCapturerStartingStepOutputSettings];

            // Make sure the raw frame data file is gone
            SCTraceSignal(@"Setup video frame raw data");
            [[NSFileManager defaultManager] removeItemAtURL:_rawDataURL error:NULL];
            if ([SnapVideoMetadata deviceMeetsRequirementsForContentAdaptiveVideoEncoding]) {
                if (!_videoFrameRawDataCollector) {
                    _videoFrameRawDataCollector = [[SCVideoFrameRawDataCollector alloc] initWithPerformer:_performer];
                }
                [_videoFrameRawDataCollector prepareForCollectingVideoFrameRawDataWithRawDataURL:_rawDataURL];
            }
            [_capturerLogger logStartingStep:kSCCapturerStartingStepVideoFrameRawData];

            SCLogVideoCapturerInfo(@"Prepare to begin audio recording");

            [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                             uniqueId:_captureSessionID
                                             stepName:@"audio_queue_start_begin"];
            [self _beginAudioQueueRecordingWithCompleteHandler:^(NSError *error) {
                [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                                 uniqueId:_captureSessionID
                                                 stepName:@"audio_queue_start_end"];
                if (error) {
                    [_delegate managedVideoCapturer:self
                                        didGetError:error
                                            forType:SCManagedVideoCapturerInfoAudioQueueError
                                            session:sessionInfo];
                } else {
                    _audioQueueStarted = YES;
                }
                if (self.status == SCManagedVideoCapturerStatusRecording) {
                    [_delegate managedVideoCapturer:self didBeginAudioRecording:sessionInfo];
                }
            }];

            // Call this delegate first so that we have proper state transition from begin recording to finish / error
            [_delegate managedVideoCapturer:self didBeginVideoRecording:sessionInfo];

            // We need to start with a fresh recording file, make sure it's gone
            [[NSFileManager defaultManager] removeItemAtURL:_outputURL error:NULL];
            [_capturerLogger logStartingStep:kSCCapturerStartingStepAudioRecording];

            SCTraceSignal(@"Setup asset writer");

            NSError *error = nil;
            _videoWriter = [[SCCapturerBufferedVideoWriter alloc] initWithPerformer:_performer
                                                                          outputURL:self.outputURL
                                                                           delegate:self
                                                                              error:&error];
            if (error) {
                self.status = SCManagedVideoCapturerStatusError;
                _lastError = error;
                _placeholderImage = nil;
                [_delegate managedVideoCapturer:self
                                    didGetError:error
                                        forType:SCManagedVideoCapturerInfoAssetWriterError
                                        session:sessionInfo];
                [_delegate managedVideoCapturer:self didFailWithError:_lastError session:sessionInfo];
                return;
            }

            [_capturerLogger logStartingStep:kSCCapturerStartingStepAssetWriterConfiguration];
            if (![_videoWriter prepareWritingWithOutputSettings:finalOutputSettings]) {
                _lastError = [NSError errorWithDomain:kSCManagedVideoCapturerErrorDomain
                                                 code:kSCManagedVideoCapturerCannotAddAudioVideoInput
                                             userInfo:nil];
                _placeholderImage = nil;
                [_delegate managedVideoCapturer:self didFailWithError:_lastError session:sessionInfo];
                return;
            }
            SCTraceSignal(@"Observe asset writer status change");
            SCCAssert(_placeholderImage == nil, @"placeholderImage should be nil");
            self.status = SCManagedVideoCapturerStatusRecording;
            // Only log the recording delay event from camera view (excluding video note recording)
            if (_isVideoSnap) {
                [[SCLogger sharedInstance] logTimedEventEnd:kSCCameraMetricsRecordingDelay
                                                   uniqueId:@"VIDEO"
                                                 parameters:@{
                                                     @"type" : @"video"
                                                 }];
            }
            _recordStartTime = CACurrentMediaTime();
        };

        [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                         uniqueId:_captureSessionID
                                         stepName:@"audio_session_start_begin"];

        if (self.status == SCManagedVideoCapturerStatusPrepareToRecord) {
            self.status = SCManagedVideoCapturerStatusReadyForRecording;
            startRecordingBlock();
        } else {
            self.status = SCManagedVideoCapturerStatusReadyForRecording;
            if (_audioConfiguration) {
                [SCAudioSessionExperimentAdapter relinquishConfiguration:_audioConfiguration
                                                               performer:nil
                                                              completion:nil];
            }
            _preparedAudioConfiguration = _audioConfiguration = [SCAudioSessionExperimentAdapter
                configureWith:audioConfiguration
                    performer:_performer
                   completion:^(NSError *error) {
                       if (error) {
                           [_delegate managedVideoCapturer:self
                                               didGetError:error
                                                   forType:SCManagedVideoCapturerInfoAudioSessionError
                                                   session:sessionInfo];
                       }
                       startRecordingBlock();
                   }];
        }
    }];
    return sessionInfo;
}

- (NSError *)_handleRetryBeginAudioRecordingErrorCode:(NSInteger)errorCode
                                                error:(NSError *)error
                                            micResult:(NSDictionary *)resultInfo
{
    SCTraceStart();
    NSString *resultStr = SC_CAST_TO_CLASS_OR_NIL(resultInfo[SCAudioSessionRetryDataSourceInfoKey], NSString);
    BOOL changeMicSuccess = [resultInfo[SCAudioSessionRetryDataSourceResultKey] boolValue];
    if (!error) {
        SCManagedVideoCapturerInfoType type = SCManagedVideoCapturerInfoAudioQueueRetrySuccess;
        if (changeMicSuccess) {
            if (errorCode == kSCAudioQueueErrorWildCard) {
                type = SCManagedVideoCapturerInfoAudioQueueRetryDataSourceSuccess_audioQueue;
            } else if (errorCode == kSCAudioQueueErrorHardware) {
                type = SCManagedVideoCapturerInfoAudioQueueRetryDataSourceSuccess_hardware;
            }
        }
        [_delegate managedVideoCapturer:self didGetError:nil forType:type session:self.activeSession];
    } else {
        error = [self _appendInfo:resultStr forInfoKey:@"retry_datasource_result" toError:error];
        SCLogVideoCapturerError(@"Retry setting audio session failed with error:%@", error);
    }
    return error;
}

- (BOOL)_isBottomMicBrokenCode:(NSInteger)errorCode
{
    // we consider both -50 and 1852797029 as a broken microphone case
    return (errorCode == kSCAudioQueueErrorWildCard || errorCode == kSCAudioQueueErrorHardware);
}

- (void)_beginAudioQueueRecordingWithCompleteHandler:(audio_capture_session_block)block
{
    SCTraceStart();
    SCAssert(block, @"block can not be nil");
    @weakify(self);
    void (^beginAudioBlock)(NSError *error) = ^(NSError *error) {
        @strongify(self);
        SC_GUARD_ELSE_RETURN(self);
        [_performer performImmediatelyIfCurrentPerformer:^{

            SCTraceStart();
            NSInteger errorCode = error.code;
            if ([self _isBottomMicBrokenCode:errorCode] &&
                (self.status == SCManagedVideoCapturerStatusReadyForRecording ||
                 self.status == SCManagedVideoCapturerStatusRecording)) {

                SCLogVideoCapturerError(@"Start to retry begin audio queue (error code: %@)", @(errorCode));

                // use front microphone to retry
                NSDictionary *resultInfo = [[SCAudioSession sharedInstance] tryUseFrontMicWithErrorCode:errorCode];
                [self _retryRequestRecordingWithCompleteHandler:^(NSError *error) {
                    // then retry audio queue again
                    [_audioCaptureSession
                        beginAudioRecordingAsynchronouslyWithSampleRate:kSCAudioCaptureSessionDefaultSampleRate
                                                      completionHandler:^(NSError *innerError) {
                                                          NSError *modifyError = [self
                                                              _handleRetryBeginAudioRecordingErrorCode:errorCode
                                                                                                 error:innerError
                                                                                             micResult:resultInfo];
                                                          block(modifyError);
                                                      }];
                }];

            } else {
                block(error);
            }
        }];
    };
    [_audioCaptureSession beginAudioRecordingAsynchronouslyWithSampleRate:kSCAudioCaptureSessionDefaultSampleRate
                                                        completionHandler:^(NSError *error) {
                                                            beginAudioBlock(error);
                                                        }];
}

// This method must not change nullability of error, it should only either append info into userInfo,
// or return the NSError as it is.
- (NSError *)_appendInfo:(NSString *)infoStr forInfoKey:(NSString *)infoKey toError:(NSError *)error
{
    if (!error || infoStr.length == 0 || infoKey.length == 0 || error.domain.length == 0) {
        return error;
    }
    NSMutableDictionary *errorInfo = [[error userInfo] mutableCopy];
    errorInfo[infoKey] = infoStr.length > 0 ? infoStr : @"(null)";

    return [NSError errorWithDomain:error.domain code:error.code userInfo:errorInfo];
}

- (void)_retryRequestRecordingWithCompleteHandler:(audio_capture_session_block)block
{
    SCTraceStart();
    if (_audioConfiguration) {
        [SCAudioSessionExperimentAdapter relinquishConfiguration:_audioConfiguration performer:nil completion:nil];
    }
    SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
    _preparedAudioConfiguration = _audioConfiguration = [SCAudioSessionExperimentAdapter
        configureWith:_audioConfiguration.configuration
            performer:_performer
           completion:^(NSError *error) {
               if (error) {
                   [_delegate managedVideoCapturer:self
                                       didGetError:error
                                           forType:SCManagedVideoCapturerInfoAudioSessionError
                                           session:sessionInfo];
               }
               if (block) {
                   block(error);
               }
           }];
}

#pragma SCCapturerBufferedVideoWriterDelegate

- (void)videoWriterDidFailWritingWithError:(NSError *)error
{
    // If it failed, we call the delegate method, release everything else we
    // have, well, on the output queue obviously
    SCTraceStart();
    [_performer performImmediatelyIfCurrentPerformer:^{
        SCTraceStart();
        SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
        [_outputURL reloadAssetKeys];
        [self _cleanup];
        [self _disposeAudioRecording];
        self.status = SCManagedVideoCapturerStatusError;
        _lastError = error;
        _placeholderImage = nil;
        [_delegate managedVideoCapturer:self
                            didGetError:error
                                forType:SCManagedVideoCapturerInfoAssetWriterError
                                session:sessionInfo];
        [_delegate managedVideoCapturer:self didFailWithError:_lastError session:sessionInfo];
    }];
}

- (void)_willStopRecording
{
    if (self.status == SCManagedVideoCapturerStatusRecording) {
        // To notify UI continue the preview processing
        SCQueuePerformer *promisePerformer =
            [[SCQueuePerformer alloc] initWithLabel:kSCManagedVideoCapturerPromiseQueueLabel
                                   qualityOfService:QOS_CLASS_USER_INTERACTIVE
                                          queueType:DISPATCH_QUEUE_SERIAL
                                            context:SCQueuePerformerContextCamera];
        _recordedVideoPromise = [[SCPromise alloc] initWithPerformer:promisePerformer];
        [_delegate managedVideoCapturer:self
            willStopWithRecordedVideoFuture:_recordedVideoPromise.future
                                  videoSize:_outputSize
                           placeholderImage:_placeholderImage
                                    session:self.activeSession];
    }
}

- (void)_stopRecording
{
    SCTraceStart();
    SCAssert([_performer isCurrentPerformer], @"Needs to be on the performing queue");
    // Reset stop session as well as stop time.
    ++_stopSession;
    _stopTime = kCFAbsoluteTimeIntervalSince1970;
    SCPromise<id<SCManagedRecordedVideo>> *recordedVideoPromise = _recordedVideoPromise;
    _recordedVideoPromise = nil;
    sc_managed_capturer_recording_session_t sessionId = _sessionId;
    if (self.status == SCManagedVideoCapturerStatusRecording) {
        self.status = SCManagedVideoCapturerStatusIdle;
        if (CMTIME_IS_VALID(_endSessionTime)) {
            [_videoWriter
                finishWritingAtSourceTime:_endSessionTime
                    withCompletionHanlder:^{
                        // actually, make sure everything happens on outputQueue
                        [_performer performImmediatelyIfCurrentPerformer:^{
                            if (sessionId != _sessionId) {
                                SCLogVideoCapturerError(@"SessionId mismatch: before: %@, after: %@", @(sessionId),
                                                        @(_sessionId));
                                return;
                            }
                            [self _disposeAudioRecording];
                            // Log the video snap recording success event w/ parameters, not including video
                            // note
                            if (_isVideoSnap) {
                                [SnapVideoMetadata logVideoEvent:kSCCameraMetricsVideoRecordingSuccess
                                                   videoSettings:_videoOutputSettings
                                                          isSave:NO];
                            }
                            void (^stopRecordingCompletionBlock)(NSURL *) = ^(NSURL *rawDataURL) {
                                SCAssert([_performer isCurrentPerformer], @"Needs to be on the performing queue");
                                SCVideoCaptureSessionInfo sessionInfo = self.activeSession;

                                [self _cleanup];

                                [[SCLogger sharedInstance] logTimedEventStart:@"SNAP_VIDEO_SIZE_LOADING"
                                                                     uniqueId:@""
                                                                isUniqueEvent:NO];
                                CGSize videoSize =
                                    [SnapVideoMetadata videoSizeForURL:_outputURL waitWhileLoadingTracksIfNeeded:YES];
                                [[SCLogger sharedInstance] logTimedEventEnd:@"SNAP_VIDEO_SIZE_LOADING"
                                                                   uniqueId:@""
                                                                 parameters:nil];
                                // Log error if video file is not really ready
                                if (videoSize.width == 0.0 || videoSize.height == 0.0) {
                                    _lastError = [NSError errorWithDomain:kSCManagedVideoCapturerErrorDomain
                                                                     code:kSCManagedVideoCapturerZeroVideoSize
                                                                 userInfo:nil];
                                    [recordedVideoPromise completeWithError:_lastError];
                                    [_delegate managedVideoCapturer:self
                                                   didFailWithError:_lastError
                                                            session:sessionInfo];
                                    _placeholderImage = nil;
                                    return;
                                }
                                // If the video duration is too short, the future object will complete
                                // with error as well
                                SCManagedRecordedVideo *recordedVideo =
                                    [[SCManagedRecordedVideo alloc] initWithVideoURL:_outputURL
                                                                 rawVideoDataFileURL:_rawDataURL
                                                                    placeholderImage:_placeholderImage
                                                                 isFrontFacingCamera:_isFrontFacingCamera];
                                [recordedVideoPromise completeWithValue:recordedVideo];
                                [_delegate managedVideoCapturer:self
                                    didSucceedWithRecordedVideo:recordedVideo
                                                        session:sessionInfo];
                                _placeholderImage = nil;
                            };

                            if (_videoFrameRawDataCollector) {
                                [_videoFrameRawDataCollector
                                    drainFrameDataCollectionWithCompletionHandler:^(NSURL *rawDataURL) {
                                        stopRecordingCompletionBlock(rawDataURL);
                                    }];
                            } else {
                                stopRecordingCompletionBlock(nil);
                            }
                        }];
                    }];

        } else {
            [self _disposeAudioRecording];
            SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
            [self _cleanup];
            self.status = SCManagedVideoCapturerStatusError;
            _lastError = [NSError errorWithDomain:kSCManagedVideoCapturerErrorDomain
                                             code:kSCManagedVideoCapturerEmptyFrame
                                         userInfo:nil];
            _placeholderImage = nil;
            [recordedVideoPromise completeWithError:_lastError];
            [_delegate managedVideoCapturer:self didFailWithError:_lastError session:sessionInfo];
        }
    } else {
        if (self.status == SCManagedVideoCapturerStatusPrepareToRecord ||
            self.status == SCManagedVideoCapturerStatusReadyForRecording) {
            _lastError = [NSError errorWithDomain:kSCManagedVideoCapturerErrorDomain
                                             code:kSCManagedVideoCapturerStopBeforeStart
                                         userInfo:nil];
        } else {
            _lastError = [NSError errorWithDomain:kSCManagedVideoCapturerErrorDomain
                                             code:kSCManagedVideoCapturerStopWithoutStart
                                         userInfo:nil];
        }
        SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
        [self _cleanup];
        _placeholderImage = nil;
        if (_audioConfiguration) {
            [SCAudioSessionExperimentAdapter relinquishConfiguration:_audioConfiguration performer:nil completion:nil];
            _audioConfiguration = nil;
        }
        [recordedVideoPromise completeWithError:_lastError];
        [_delegate managedVideoCapturer:self didFailWithError:_lastError session:sessionInfo];
        self.status = SCManagedVideoCapturerStatusIdle;
        [_capturerLogger logEventIfStartingTooSlow];
    }
}

- (void)stopRecordingAsynchronously
{
    SCTraceStart();
    NSTimeInterval stopTime = CACurrentMediaTime();
    [_performer performImmediatelyIfCurrentPerformer:^{
        _stopTime = stopTime;
        NSInteger stopSession = _stopSession;
        [self _willStopRecording];
        [_performer perform:^{
            // If we haven't stopped yet, call the stop now nevertheless.
            if (stopSession == _stopSession) {
                [self _stopRecording];
            }
        }
                      after:kSCManagedVideoCapturerStopRecordingDeadline];
    }];
}

- (void)cancelRecordingAsynchronously
{
    SCTraceStart();
    [_performer performImmediatelyIfCurrentPerformer:^{
        SCTraceStart();
        SCLogVideoCapturerInfo(@"Cancel recording. status: %lu", (unsigned long)self.status);
        if (self.status == SCManagedVideoCapturerStatusRecording) {
            self.status = SCManagedVideoCapturerStatusIdle;
            [self _disposeAudioRecording];
            [_videoWriter cancelWriting];
            SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
            [self _cleanup];
            _placeholderImage = nil;
            [_delegate managedVideoCapturer:self didCancelVideoRecording:sessionInfo];
        } else if ((self.status == SCManagedVideoCapturerStatusPrepareToRecord) ||
                   (self.status == SCManagedVideoCapturerStatusReadyForRecording)) {
            SCVideoCaptureSessionInfo sessionInfo = self.activeSession;
            [self _cleanup];
            self.status = SCManagedVideoCapturerStatusIdle;
            _placeholderImage = nil;
            if (_audioConfiguration) {
                [SCAudioSessionExperimentAdapter relinquishConfiguration:_audioConfiguration
                                                               performer:nil
                                                              completion:nil];
                _audioConfiguration = nil;
            }
            [_delegate managedVideoCapturer:self didCancelVideoRecording:sessionInfo];
        }
        [_capturerLogger logEventIfStartingTooSlow];
    }];
}

- (void)addTimedTask:(SCTimedTask *)task
{
    [_performer performImmediatelyIfCurrentPerformer:^{
        // Only allow to add observers when we are not recording.
        if (!self->_timeObserver) {
            self->_timeObserver = [SCManagedVideoCapturerTimeObserver new];
        }
        [self->_timeObserver addTimedTask:task];
        SCLogVideoCapturerInfo(@"Added timetask: %@", task);
    }];
}

- (void)clearTimedTasks
{
    // _timeObserver will be initialized lazily when adding timed tasks
    SCLogVideoCapturerInfo(@"Clearing time observer");
    [_performer performImmediatelyIfCurrentPerformer:^{
        if (self->_timeObserver) {
            self->_timeObserver = nil;
        }
    }];
}

- (void)_cleanup
{
    [_videoWriter cleanUp];
    _timeObserver = nil;

    SCLogVideoCapturerInfo(@"SCVideoCaptureSessionInfo before cleanup: %@",
                           SCVideoCaptureSessionInfoGetDebugDescription(self.activeSession));

    _startSessionTime = kCMTimeInvalid;
    _endSessionTime = kCMTimeInvalid;
    _firstWrittenAudioBufferDelay = kCMTimeInvalid;
    _sessionId = 0;
    _captureSessionID = nil;
    _audioQueueStarted = NO;
}

- (void)_disposeAudioRecording
{
    SCLogVideoCapturerInfo(@"Disposing audio recording");
    SCAssert([_performer isCurrentPerformer], @"");
    // Setup the audio session token correctly
    SCAudioConfigurationToken *audioConfiguration = _audioConfiguration;
    [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                     uniqueId:_captureSessionID
                                     stepName:@"audio_queue_stop_begin"];
    NSString *captureSessionID = _captureSessionID;
    [_audioCaptureSession disposeAudioRecordingSynchronouslyWithCompletionHandler:^{
        [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                         uniqueId:captureSessionID
                                         stepName:@"audio_queue_stop_end"];
        SCLogVideoCapturerInfo(@"Did dispose audio recording");
        if (audioConfiguration) {
            [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                             uniqueId:captureSessionID
                                             stepName:@"audio_session_stop_begin"];
            [SCAudioSessionExperimentAdapter
                relinquishConfiguration:audioConfiguration
                              performer:_performer
                             completion:^(NSError *_Nullable error) {
                                 [[SCLogger sharedInstance] logStepToEvent:kSCCameraMetricsAudioDelay
                                                                  uniqueId:captureSessionID
                                                                  stepName:@"audio_session_stop_end"];
                                 [[SCLogger sharedInstance] logTimedEventEnd:kSCCameraMetricsAudioDelay
                                                                    uniqueId:captureSessionID
                                                                  parameters:nil];
                             }];
        }
    }];
    _audioConfiguration = nil;
}

- (CIContext *)ciContext
{
    if (!_ciContext) {
        _ciContext = [CIContext contextWithOptions:nil];
    }
    return _ciContext;
}

#pragma mark - SCAudioCaptureSessionDelegate

- (void)audioCaptureSession:(SCAudioCaptureSession *)audioCaptureSession
      didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    SCTraceStart();
    if (self.status != SCManagedVideoCapturerStatusRecording) {
        return;
    }
    CFRetain(sampleBuffer);
    [_performer performImmediatelyIfCurrentPerformer:^{
        if (self.status == SCManagedVideoCapturerStatusRecording) {
            // Audio always follows video, there is no other way around this :)
            if (_hasWritten && CACurrentMediaTime() - _recordStartTime <= _maxDuration) {
                [self _processAudioSampleBuffer:sampleBuffer];
                [_videoWriter appendAudioSampleBuffer:sampleBuffer];
            }
        }
        CFRelease(sampleBuffer);
    }];
}

#pragma mark - SCManagedVideoDataSourceListener

- (void)managedVideoDataSource:(id<SCManagedVideoDataSource>)managedVideoDataSource
         didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
                devicePosition:(SCManagedCaptureDevicePosition)devicePosition
{
    SCTraceStart();
    if (self.status != SCManagedVideoCapturerStatusRecording) {
        return;
    }
    CFRetain(sampleBuffer);
    [_performer performImmediatelyIfCurrentPerformer:^{
        // the following check will allow the capture pipeline to drain
        if (CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) > _stopTime) {
            [self _stopRecording];
        } else {
            if (self.status == SCManagedVideoCapturerStatusRecording) {
                _isFrontFacingCamera = (devicePosition == SCManagedCaptureDevicePositionFront);
                CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
                if (CMTIME_IS_VALID(presentationTime)) {
                    SCLogVideoCapturerInfo(@"Obtained video data source at time %lld", presentationTime.value);
                } else {
                    SCLogVideoCapturerInfo(@"Obtained video data source with an invalid time");
                }
                if (!_hasWritten) {
                    // Start writing!
                    [_videoWriter startWritingAtSourceTime:presentationTime];
                    [_capturerLogger endLoggingForStarting];
                    _startSessionTime = presentationTime;
                    _startSessionRealTime = CACurrentMediaTime();
                    SCLogVideoCapturerInfo(@"First frame processed %f seconds after presentation Time",
                                           _startSessionRealTime - CMTimeGetSeconds(presentationTime));
                    _hasWritten = YES;
                    [[SCLogger sharedInstance] logPreCaptureOperationFinishedAt:CMTimeGetSeconds(presentationTime)];
                    [[SCCoreCameraLogger sharedInstance]
                        logCameraCreationDelaySplitPointPreCaptureOperationFinishedAt:CMTimeGetSeconds(
                                                                                          presentationTime)];
                    SCLogVideoCapturerInfo(@"SCVideoCaptureSessionInfo after first frame: %@",
                                           SCVideoCaptureSessionInfoGetDebugDescription(self.activeSession));
                }
                // Only respect video end session time, audio can be cut off, not video,
                // not video
                if (CMTIME_IS_INVALID(_endSessionTime)) {
                    _endSessionTime = presentationTime;
                } else {
                    _endSessionTime = CMTimeMaximum(_endSessionTime, presentationTime);
                }
                if (CACurrentMediaTime() - _recordStartTime <= _maxDuration) {
                    [_videoWriter appendVideoSampleBuffer:sampleBuffer];
                    [self _processVideoSampleBuffer:sampleBuffer];
                }
                if (_timeObserver) {
                    [_timeObserver processTime:CMTimeSubtract(presentationTime, _startSessionTime)
                        sessionStartTimeDelayInSecond:_startSessionRealTime - CMTimeGetSeconds(_startSessionTime)];
                }
            }
        }
        CFRelease(sampleBuffer);
    }];
}

- (void)_generatePlaceholderImageWithPixelBuffer:(CVImageBufferRef)pixelBuffer metaData:(NSDictionary *)metadata
{
    SCTraceStart();
    CVImageBufferRef imageBuffer = CVPixelBufferRetain(pixelBuffer);
    if (imageBuffer) {
        dispatch_async(SCPlaceholderImageGenerationQueue(), ^{
            UIImage *placeholderImage = [UIImage imageWithPixelBufferRef:imageBuffer
                                                             backingType:UIImageBackingTypeCGImage
                                                             orientation:UIImageOrientationRight
                                                                 context:[self ciContext]];
            placeholderImage =
                SCCropImageToTargetAspectRatio(placeholderImage, SCManagedCapturedImageAndVideoAspectRatio());
            [_performer performImmediatelyIfCurrentPerformer:^{
                // After processing, assign it back.
                if (self.status == SCManagedVideoCapturerStatusRecording) {
                    _placeholderImage = placeholderImage;
                    // Check video frame health by placeholder image
                    [[SCManagedFrameHealthChecker sharedInstance]
                        checkVideoHealthForCaptureFrameImage:placeholderImage
                                                    metedata:metadata
                                            captureSessionID:_captureSessionID];
                }
                CVPixelBufferRelease(imageBuffer);
            }];
        });
    }
}

#pragma mark - Pixel Buffer methods

- (void)_processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    SC_GUARD_ELSE_RETURN(sampleBuffer);
    CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
    BOOL shouldGeneratePlaceholderImage = CMTimeCompare(presentationTime, _startSessionTime) == 0;

    CVImageBufferRef outputPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    if (outputPixelBuffer) {
        [self _addVideoRawDataWithPixelBuffer:outputPixelBuffer];
        if (shouldGeneratePlaceholderImage) {
            NSDictionary *extraInfo = [_delegate managedVideoCapturerGetExtraFrameHealthInfo:self];
            NSDictionary *metadata =
                [[[SCManagedFrameHealthChecker sharedInstance] metadataForSampleBuffer:sampleBuffer extraInfo:extraInfo]
                    copy];
            [self _generatePlaceholderImageWithPixelBuffer:outputPixelBuffer metaData:metadata];
        }
    }

    [_delegate managedVideoCapturer:self
         didAppendVideoSampleBuffer:sampleBuffer
              presentationTimestamp:CMTimeSubtract(presentationTime, _startSessionTime)];
}

- (void)_processAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [_announcer managedAudioDataSource:self didOutputSampleBuffer:sampleBuffer];
    if (!CMTIME_IS_VALID(self.firstWrittenAudioBufferDelay)) {
        self.firstWrittenAudioBufferDelay =
            CMTimeSubtract(CMSampleBufferGetPresentationTimeStamp(sampleBuffer), _startSessionTime);
    }
}

- (void)_addVideoRawDataWithPixelBuffer:(CVImageBufferRef)pixelBuffer
{
    if (_videoFrameRawDataCollector && [SnapVideoMetadata deviceMeetsRequirementsForContentAdaptiveVideoEncoding] &&
        ((_rawDataFrameNum % kSCVideoContentComplexitySamplingRate) == 0) && (_rawDataFrameNum > 0)) {
        if (_videoFrameRawDataCollector) {
            CVImageBufferRef imageBuffer = CVPixelBufferRetain(pixelBuffer);
            [_videoFrameRawDataCollector collectVideoFrameRawDataWithImageBuffer:imageBuffer
                                                                        frameNum:_rawDataFrameNum
                                                                      completion:^{
                                                                          CVPixelBufferRelease(imageBuffer);
                                                                      }];
        }
    }
    _rawDataFrameNum++;
}

#pragma mark - SCManagedAudioDataSource

- (void)addListener:(id<SCManagedAudioDataSourceListener>)listener
{
    [_announcer addListener:listener];
}

- (void)removeListener:(id<SCManagedAudioDataSourceListener>)listener
{
    [_announcer removeListener:listener];
}

- (void)startStreamingWithAudioConfiguration:(SCAudioConfiguration *)configuration
{
    SCAssertFail(@"Controlled by recorder");
}

- (void)stopStreaming
{
    SCAssertFail(@"Controlled by recorder");
}

- (BOOL)isStreaming
{
    return self.status == SCManagedVideoCapturerStatusRecording;
}

@end