1*d9f75844SAndroid Build Coastguard Worker/** 2*d9f75844SAndroid Build Coastguard Worker * Copyright 2017 The WebRTC Project Authors. All rights reserved. 3*d9f75844SAndroid Build Coastguard Worker * 4*d9f75844SAndroid Build Coastguard Worker * Use of this source code is governed by a BSD-style license 5*d9f75844SAndroid Build Coastguard Worker * that can be found in the LICENSE file in the root of the source 6*d9f75844SAndroid Build Coastguard Worker * tree. An additional intellectual property rights grant can be found 7*d9f75844SAndroid Build Coastguard Worker * in the file PATENTS. All contributing project authors may 8*d9f75844SAndroid Build Coastguard Worker * be found in the AUTHORS file in the root of the source tree. 9*d9f75844SAndroid Build Coastguard Worker */ 10*d9f75844SAndroid Build Coastguard Worker 11*d9f75844SAndroid Build Coastguard Worker#import "RTCFileVideoCapturer.h" 12*d9f75844SAndroid Build Coastguard Worker 13*d9f75844SAndroid Build Coastguard Worker#import "base/RTCLogging.h" 14*d9f75844SAndroid Build Coastguard Worker#import "base/RTCVideoFrameBuffer.h" 15*d9f75844SAndroid Build Coastguard Worker#import "components/video_frame_buffer/RTCCVPixelBuffer.h" 16*d9f75844SAndroid Build Coastguard Worker#include "rtc_base/system/gcd_helpers.h" 17*d9f75844SAndroid Build Coastguard Worker 18*d9f75844SAndroid Build Coastguard WorkerNSString *const kRTCFileVideoCapturerErrorDomain = 19*d9f75844SAndroid Build Coastguard Worker @"org.webrtc.RTC_OBJC_TYPE(RTCFileVideoCapturer)"; 20*d9f75844SAndroid Build Coastguard Worker 21*d9f75844SAndroid Build Coastguard Workertypedef NS_ENUM(NSInteger, RTCFileVideoCapturerErrorCode) { 22*d9f75844SAndroid Build Coastguard Worker RTCFileVideoCapturerErrorCode_CapturerRunning = 2000, 23*d9f75844SAndroid Build Coastguard Worker RTCFileVideoCapturerErrorCode_FileNotFound 24*d9f75844SAndroid Build Coastguard Worker}; 25*d9f75844SAndroid Build Coastguard Worker 26*d9f75844SAndroid Build Coastguard Workertypedef NS_ENUM(NSInteger, RTCFileVideoCapturerStatus) { 27*d9f75844SAndroid Build Coastguard Worker RTCFileVideoCapturerStatusNotInitialized, 28*d9f75844SAndroid Build Coastguard Worker RTCFileVideoCapturerStatusStarted, 29*d9f75844SAndroid Build Coastguard Worker RTCFileVideoCapturerStatusStopped 30*d9f75844SAndroid Build Coastguard Worker}; 31*d9f75844SAndroid Build Coastguard Worker 32*d9f75844SAndroid Build Coastguard Worker@interface RTC_OBJC_TYPE (RTCFileVideoCapturer) 33*d9f75844SAndroid Build Coastguard Worker() @property(nonatomic, assign) CMTime lastPresentationTime; 34*d9f75844SAndroid Build Coastguard Worker@property(nonatomic, strong) NSURL *fileURL; 35*d9f75844SAndroid Build Coastguard Worker@end 36*d9f75844SAndroid Build Coastguard Worker 37*d9f75844SAndroid Build Coastguard Worker@implementation RTC_OBJC_TYPE (RTCFileVideoCapturer) { 38*d9f75844SAndroid Build Coastguard Worker AVAssetReader *_reader; 39*d9f75844SAndroid Build Coastguard Worker AVAssetReaderTrackOutput *_outTrack; 40*d9f75844SAndroid Build Coastguard Worker RTCFileVideoCapturerStatus _status; 41*d9f75844SAndroid Build Coastguard Worker dispatch_queue_t _frameQueue; 42*d9f75844SAndroid Build Coastguard Worker} 43*d9f75844SAndroid Build Coastguard Worker 44*d9f75844SAndroid Build Coastguard Worker@synthesize lastPresentationTime = _lastPresentationTime; 45*d9f75844SAndroid Build Coastguard Worker@synthesize fileURL = _fileURL; 46*d9f75844SAndroid Build Coastguard Worker 47*d9f75844SAndroid Build Coastguard Worker- (void)startCapturingFromFileNamed:(NSString *)nameOfFile 48*d9f75844SAndroid Build Coastguard Worker onError:(RTCFileVideoCapturerErrorBlock)errorBlock { 49*d9f75844SAndroid Build Coastguard Worker if (_status == RTCFileVideoCapturerStatusStarted) { 50*d9f75844SAndroid Build Coastguard Worker NSError *error = 51*d9f75844SAndroid Build Coastguard Worker [NSError errorWithDomain:kRTCFileVideoCapturerErrorDomain 52*d9f75844SAndroid Build Coastguard Worker code:RTCFileVideoCapturerErrorCode_CapturerRunning 53*d9f75844SAndroid Build Coastguard Worker userInfo:@{NSUnderlyingErrorKey : @"Capturer has been started."}]; 54*d9f75844SAndroid Build Coastguard Worker 55*d9f75844SAndroid Build Coastguard Worker errorBlock(error); 56*d9f75844SAndroid Build Coastguard Worker return; 57*d9f75844SAndroid Build Coastguard Worker } else { 58*d9f75844SAndroid Build Coastguard Worker _status = RTCFileVideoCapturerStatusStarted; 59*d9f75844SAndroid Build Coastguard Worker } 60*d9f75844SAndroid Build Coastguard Worker 61*d9f75844SAndroid Build Coastguard Worker dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 62*d9f75844SAndroid Build Coastguard Worker NSString *pathForFile = [self pathForFileName:nameOfFile]; 63*d9f75844SAndroid Build Coastguard Worker if (!pathForFile) { 64*d9f75844SAndroid Build Coastguard Worker NSString *errorString = 65*d9f75844SAndroid Build Coastguard Worker [NSString stringWithFormat:@"File %@ not found in bundle", nameOfFile]; 66*d9f75844SAndroid Build Coastguard Worker NSError *error = [NSError errorWithDomain:kRTCFileVideoCapturerErrorDomain 67*d9f75844SAndroid Build Coastguard Worker code:RTCFileVideoCapturerErrorCode_FileNotFound 68*d9f75844SAndroid Build Coastguard Worker userInfo:@{NSUnderlyingErrorKey : errorString}]; 69*d9f75844SAndroid Build Coastguard Worker errorBlock(error); 70*d9f75844SAndroid Build Coastguard Worker return; 71*d9f75844SAndroid Build Coastguard Worker } 72*d9f75844SAndroid Build Coastguard Worker 73*d9f75844SAndroid Build Coastguard Worker self.lastPresentationTime = CMTimeMake(0, 0); 74*d9f75844SAndroid Build Coastguard Worker 75*d9f75844SAndroid Build Coastguard Worker self.fileURL = [NSURL fileURLWithPath:pathForFile]; 76*d9f75844SAndroid Build Coastguard Worker [self setupReaderOnError:errorBlock]; 77*d9f75844SAndroid Build Coastguard Worker }); 78*d9f75844SAndroid Build Coastguard Worker} 79*d9f75844SAndroid Build Coastguard Worker 80*d9f75844SAndroid Build Coastguard Worker- (void)setupReaderOnError:(RTCFileVideoCapturerErrorBlock)errorBlock { 81*d9f75844SAndroid Build Coastguard Worker AVURLAsset *asset = [AVURLAsset URLAssetWithURL:_fileURL options:nil]; 82*d9f75844SAndroid Build Coastguard Worker 83*d9f75844SAndroid Build Coastguard Worker NSArray *allTracks = [asset tracksWithMediaType:AVMediaTypeVideo]; 84*d9f75844SAndroid Build Coastguard Worker NSError *error = nil; 85*d9f75844SAndroid Build Coastguard Worker 86*d9f75844SAndroid Build Coastguard Worker _reader = [[AVAssetReader alloc] initWithAsset:asset error:&error]; 87*d9f75844SAndroid Build Coastguard Worker if (error) { 88*d9f75844SAndroid Build Coastguard Worker errorBlock(error); 89*d9f75844SAndroid Build Coastguard Worker return; 90*d9f75844SAndroid Build Coastguard Worker } 91*d9f75844SAndroid Build Coastguard Worker 92*d9f75844SAndroid Build Coastguard Worker NSDictionary *options = @{ 93*d9f75844SAndroid Build Coastguard Worker (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) 94*d9f75844SAndroid Build Coastguard Worker }; 95*d9f75844SAndroid Build Coastguard Worker _outTrack = 96*d9f75844SAndroid Build Coastguard Worker [[AVAssetReaderTrackOutput alloc] initWithTrack:allTracks.firstObject outputSettings:options]; 97*d9f75844SAndroid Build Coastguard Worker [_reader addOutput:_outTrack]; 98*d9f75844SAndroid Build Coastguard Worker 99*d9f75844SAndroid Build Coastguard Worker [_reader startReading]; 100*d9f75844SAndroid Build Coastguard Worker RTCLog(@"File capturer started reading"); 101*d9f75844SAndroid Build Coastguard Worker [self readNextBuffer]; 102*d9f75844SAndroid Build Coastguard Worker} 103*d9f75844SAndroid Build Coastguard Worker- (void)stopCapture { 104*d9f75844SAndroid Build Coastguard Worker _status = RTCFileVideoCapturerStatusStopped; 105*d9f75844SAndroid Build Coastguard Worker RTCLog(@"File capturer stopped."); 106*d9f75844SAndroid Build Coastguard Worker} 107*d9f75844SAndroid Build Coastguard Worker 108*d9f75844SAndroid Build Coastguard Worker#pragma mark - Private 109*d9f75844SAndroid Build Coastguard Worker 110*d9f75844SAndroid Build Coastguard Worker- (nullable NSString *)pathForFileName:(NSString *)fileName { 111*d9f75844SAndroid Build Coastguard Worker NSArray *nameComponents = [fileName componentsSeparatedByString:@"."]; 112*d9f75844SAndroid Build Coastguard Worker if (nameComponents.count != 2) { 113*d9f75844SAndroid Build Coastguard Worker return nil; 114*d9f75844SAndroid Build Coastguard Worker } 115*d9f75844SAndroid Build Coastguard Worker 116*d9f75844SAndroid Build Coastguard Worker NSString *path = 117*d9f75844SAndroid Build Coastguard Worker [[NSBundle mainBundle] pathForResource:nameComponents[0] ofType:nameComponents[1]]; 118*d9f75844SAndroid Build Coastguard Worker return path; 119*d9f75844SAndroid Build Coastguard Worker} 120*d9f75844SAndroid Build Coastguard Worker 121*d9f75844SAndroid Build Coastguard Worker- (dispatch_queue_t)frameQueue { 122*d9f75844SAndroid Build Coastguard Worker if (!_frameQueue) { 123*d9f75844SAndroid Build Coastguard Worker _frameQueue = RTCDispatchQueueCreateWithTarget( 124*d9f75844SAndroid Build Coastguard Worker "org.webrtc.filecapturer.video", 125*d9f75844SAndroid Build Coastguard Worker DISPATCH_QUEUE_SERIAL, 126*d9f75844SAndroid Build Coastguard Worker dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); 127*d9f75844SAndroid Build Coastguard Worker } 128*d9f75844SAndroid Build Coastguard Worker return _frameQueue; 129*d9f75844SAndroid Build Coastguard Worker} 130*d9f75844SAndroid Build Coastguard Worker 131*d9f75844SAndroid Build Coastguard Worker- (void)readNextBuffer { 132*d9f75844SAndroid Build Coastguard Worker if (_status == RTCFileVideoCapturerStatusStopped) { 133*d9f75844SAndroid Build Coastguard Worker [_reader cancelReading]; 134*d9f75844SAndroid Build Coastguard Worker _reader = nil; 135*d9f75844SAndroid Build Coastguard Worker return; 136*d9f75844SAndroid Build Coastguard Worker } 137*d9f75844SAndroid Build Coastguard Worker 138*d9f75844SAndroid Build Coastguard Worker if (_reader.status == AVAssetReaderStatusCompleted) { 139*d9f75844SAndroid Build Coastguard Worker [_reader cancelReading]; 140*d9f75844SAndroid Build Coastguard Worker _reader = nil; 141*d9f75844SAndroid Build Coastguard Worker [self setupReaderOnError:nil]; 142*d9f75844SAndroid Build Coastguard Worker return; 143*d9f75844SAndroid Build Coastguard Worker } 144*d9f75844SAndroid Build Coastguard Worker 145*d9f75844SAndroid Build Coastguard Worker CMSampleBufferRef sampleBuffer = [_outTrack copyNextSampleBuffer]; 146*d9f75844SAndroid Build Coastguard Worker if (!sampleBuffer) { 147*d9f75844SAndroid Build Coastguard Worker [self readNextBuffer]; 148*d9f75844SAndroid Build Coastguard Worker return; 149*d9f75844SAndroid Build Coastguard Worker } 150*d9f75844SAndroid Build Coastguard Worker if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 || !CMSampleBufferIsValid(sampleBuffer) || 151*d9f75844SAndroid Build Coastguard Worker !CMSampleBufferDataIsReady(sampleBuffer)) { 152*d9f75844SAndroid Build Coastguard Worker CFRelease(sampleBuffer); 153*d9f75844SAndroid Build Coastguard Worker [self readNextBuffer]; 154*d9f75844SAndroid Build Coastguard Worker return; 155*d9f75844SAndroid Build Coastguard Worker } 156*d9f75844SAndroid Build Coastguard Worker 157*d9f75844SAndroid Build Coastguard Worker [self publishSampleBuffer:sampleBuffer]; 158*d9f75844SAndroid Build Coastguard Worker} 159*d9f75844SAndroid Build Coastguard Worker 160*d9f75844SAndroid Build Coastguard Worker- (void)publishSampleBuffer:(CMSampleBufferRef)sampleBuffer { 161*d9f75844SAndroid Build Coastguard Worker CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); 162*d9f75844SAndroid Build Coastguard Worker Float64 presentationDifference = 163*d9f75844SAndroid Build Coastguard Worker CMTimeGetSeconds(CMTimeSubtract(presentationTime, _lastPresentationTime)); 164*d9f75844SAndroid Build Coastguard Worker _lastPresentationTime = presentationTime; 165*d9f75844SAndroid Build Coastguard Worker int64_t presentationDifferenceRound = lroundf(presentationDifference * NSEC_PER_SEC); 166*d9f75844SAndroid Build Coastguard Worker 167*d9f75844SAndroid Build Coastguard Worker __block dispatch_source_t timer = [self createStrictTimer]; 168*d9f75844SAndroid Build Coastguard Worker // Strict timer that will fire `presentationDifferenceRound` ns from now and never again. 169*d9f75844SAndroid Build Coastguard Worker dispatch_source_set_timer(timer, 170*d9f75844SAndroid Build Coastguard Worker dispatch_time(DISPATCH_TIME_NOW, presentationDifferenceRound), 171*d9f75844SAndroid Build Coastguard Worker DISPATCH_TIME_FOREVER, 172*d9f75844SAndroid Build Coastguard Worker 0); 173*d9f75844SAndroid Build Coastguard Worker dispatch_source_set_event_handler(timer, ^{ 174*d9f75844SAndroid Build Coastguard Worker dispatch_source_cancel(timer); 175*d9f75844SAndroid Build Coastguard Worker timer = nil; 176*d9f75844SAndroid Build Coastguard Worker 177*d9f75844SAndroid Build Coastguard Worker CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 178*d9f75844SAndroid Build Coastguard Worker if (!pixelBuffer) { 179*d9f75844SAndroid Build Coastguard Worker CFRelease(sampleBuffer); 180*d9f75844SAndroid Build Coastguard Worker dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 181*d9f75844SAndroid Build Coastguard Worker [self readNextBuffer]; 182*d9f75844SAndroid Build Coastguard Worker }); 183*d9f75844SAndroid Build Coastguard Worker return; 184*d9f75844SAndroid Build Coastguard Worker } 185*d9f75844SAndroid Build Coastguard Worker 186*d9f75844SAndroid Build Coastguard Worker RTC_OBJC_TYPE(RTCCVPixelBuffer) *rtcPixelBuffer = 187*d9f75844SAndroid Build Coastguard Worker [[RTC_OBJC_TYPE(RTCCVPixelBuffer) alloc] initWithPixelBuffer:pixelBuffer]; 188*d9f75844SAndroid Build Coastguard Worker NSTimeInterval timeStampSeconds = CACurrentMediaTime(); 189*d9f75844SAndroid Build Coastguard Worker int64_t timeStampNs = lroundf(timeStampSeconds * NSEC_PER_SEC); 190*d9f75844SAndroid Build Coastguard Worker RTC_OBJC_TYPE(RTCVideoFrame) *videoFrame = 191*d9f75844SAndroid Build Coastguard Worker [[RTC_OBJC_TYPE(RTCVideoFrame) alloc] initWithBuffer:rtcPixelBuffer 192*d9f75844SAndroid Build Coastguard Worker rotation:0 193*d9f75844SAndroid Build Coastguard Worker timeStampNs:timeStampNs]; 194*d9f75844SAndroid Build Coastguard Worker CFRelease(sampleBuffer); 195*d9f75844SAndroid Build Coastguard Worker 196*d9f75844SAndroid Build Coastguard Worker dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 197*d9f75844SAndroid Build Coastguard Worker [self readNextBuffer]; 198*d9f75844SAndroid Build Coastguard Worker }); 199*d9f75844SAndroid Build Coastguard Worker 200*d9f75844SAndroid Build Coastguard Worker [self.delegate capturer:self didCaptureVideoFrame:videoFrame]; 201*d9f75844SAndroid Build Coastguard Worker }); 202*d9f75844SAndroid Build Coastguard Worker dispatch_activate(timer); 203*d9f75844SAndroid Build Coastguard Worker} 204*d9f75844SAndroid Build Coastguard Worker 205*d9f75844SAndroid Build Coastguard Worker- (dispatch_source_t)createStrictTimer { 206*d9f75844SAndroid Build Coastguard Worker dispatch_source_t timer = dispatch_source_create( 207*d9f75844SAndroid Build Coastguard Worker DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, [self frameQueue]); 208*d9f75844SAndroid Build Coastguard Worker return timer; 209*d9f75844SAndroid Build Coastguard Worker} 210*d9f75844SAndroid Build Coastguard Worker 211*d9f75844SAndroid Build Coastguard Worker- (void)dealloc { 212*d9f75844SAndroid Build Coastguard Worker [self stopCapture]; 213*d9f75844SAndroid Build Coastguard Worker} 214*d9f75844SAndroid Build Coastguard Worker 215*d9f75844SAndroid Build Coastguard Worker@end 216