You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1107 lines
53 KiB
1107 lines
53 KiB
//
|
|
// 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
|