Vuo  2.3.2
VuoAvWriterObject.m
Go to the documentation of this file.
1 
10 #include "module.h"
11 #include "VuoAvWriterObject.h"
12 #include "VuoOsStatus.h"
13 #include <CoreVideo/CoreVideo.h>
14 #import <AVFoundation/AVFoundation.h>
15 #include <OpenGL/CGLMacro.h>
16 #include <sys/stat.h>
17 
18 #ifdef VUO_COMPILER
20  "title" : "VuoAvWriterObject",
21  "dependencies" : [
22  "VuoFont",
23  "VuoImage",
24  "VuoImageText",
25  "VuoAudioSamples",
26  "VuoImageRenderer",
27  "VuoOsStatus",
28  "AVFoundation.framework",
29  "CoreMedia.framework",
30  "CoreVideo.framework"
31  ]
32  });
33 #endif
34 
35 const double TIMEBASE = 1000.;
36 
37 const long MIN_AUDIO_BITRATE = 64000;
38 const long MAX_AUDIO_BITRATE = 320000;
39 
43 @interface VuoAvWriterObject()
44 @property (retain) AVAssetWriter* assetWriter;
45 @property (retain) AVAssetWriterInput* videoInput;
46 @property (retain) AVAssetWriterInput* audioInput;
47 @property (retain) AVAssetWriterInputPixelBufferAdaptor* avAdaptor;
48 
49 @property CMFormatDescriptionRef audio_fmt_desc;
50 @property long audio_sample_position;
51 
52 @property (retain) NSDate* startDate;
53 @property CMTime lastImageTimestamp;
54 @property CMTime lastAudioTimestamp;
55 
56 @property int originalChannelCount;
57 @property int originalWidth;
58 @property int originalHeight;
59 @property bool firstFrame;
60 @end
61 
62 @implementation VuoAvWriterObject
63 
64 - (BOOL) isRecording
65 {
66  return self.assetWriter != nil;
67 }
68 
69 - (BOOL) setupAssetWriterWithUrl:(NSURL*) fileUrl imageWidth:(int)width imageHeight:(int)height channelCount:(int)channels movieFormat:(VuoMovieFormat)format
70 {
71  NSError *error = nil;
72 
73  self.lastImageTimestamp = kCMTimeNegativeInfinity;
74  self.lastAudioTimestamp = kCMTimeNegativeInfinity;
75 
76  self.originalWidth = width;
77  self.originalHeight = height;
78 
79 #pragma clang diagnostic push
80 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
81  // The replacements, AVVideoCodecType*, aren't available until macOS 10.13.
82  NSString* videoEncoding = AVVideoCodecJPEG;
83 
84  if (format.imageEncoding == VuoMovieImageEncoding_H264)
85  videoEncoding = AVVideoCodecH264;
86  else if(format.imageEncoding == VuoMovieImageEncoding_ProRes4444)
87  videoEncoding = AVVideoCodecAppleProRes4444;
88  else if(format.imageEncoding == VuoMovieImageEncoding_ProRes422)
89  videoEncoding = AVVideoCodecAppleProRes422;
90 #pragma clang diagnostic pop
91  else if (format.imageEncoding == VuoMovieImageEncoding_ProRes422HQ)
92  {
93  if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10,15,0}])
94  videoEncoding = @"apch"; // AVVideoCodecTypeAppleProRes422HQ
95  else
96  {
97  VUserLog("Error: macOS %d.%d doesn't support ProRes 422 HQ.", (int)NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, (int)NSProcessInfo.processInfo.operatingSystemVersion.minorVersion);
98  return NO;
99  }
100  }
101  else if (format.imageEncoding == VuoMovieImageEncoding_ProRes422LT)
102  {
103  if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10,15,0}])
104  videoEncoding = @"apcs"; // AVVideoCodecTypeAppleProRes422LT
105  else
106  {
107  VUserLog("Error: macOS %d.%d doesn't support ProRes 422 LT.", (int)NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, (int)NSProcessInfo.processInfo.operatingSystemVersion.minorVersion);
108  return NO;
109  }
110  }
111  else if (format.imageEncoding == VuoMovieImageEncoding_ProRes422Proxy)
112  {
113  if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10,15,0}])
114  videoEncoding = @"apco"; // AVVideoCodecTypeAppleProRes422Proxy
115  else
116  {
117  VUserLog("Error: macOS %d.%d doesn't support ProRes 422 Proxy.", (int)NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, (int)NSProcessInfo.processInfo.operatingSystemVersion.minorVersion);
118  return NO;
119  }
120  }
121  else if (format.imageEncoding == VuoMovieImageEncoding_HEVC)
122  {
123  if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10,13,0}])
124  videoEncoding = @"hvc1"; // AVVideoCodecTypeHEVC
125  else
126  {
127  VUserLog("Error: macOS %d.%d doesn't support HEVC/h.265.", (int)NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, (int)NSProcessInfo.processInfo.operatingSystemVersion.minorVersion);
128  return NO;
129  }
130  }
131  else if (format.imageEncoding == VuoMovieImageEncoding_HEVCAlpha)
132  {
133  if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10,15,0}])
134  videoEncoding = @"muxa"; // AVVideoCodecTypeHEVCWithAlpha
135  else
136  {
137  VUserLog("Error: macOS %d.%d doesn't support HEVC/h.265 with alpha channel.", (int)NSProcessInfo.processInfo.operatingSystemVersion.majorVersion, (int)NSProcessInfo.processInfo.operatingSystemVersion.minorVersion);
138  return NO;
139  }
140  }
141 
142  // allocate the writer object with our output file URL
143  self.assetWriter = [[[AVAssetWriter alloc] initWithURL:fileUrl fileType:AVFileTypeQuickTimeMovie error:&error] autorelease];
144  _assetWriter.movieFragmentInterval = CMTimeMake(TIMEBASE*10, TIMEBASE);
145 
146  if (error) {
147  VUserLog("AVAssetWriter initWithURL failed with error %s", [[error localizedDescription] UTF8String]);
148  return NO;
149  }
150 
151  // https://developer.apple.com/library/mac/documentation/AVFoundation/Reference/AVFoundation_Constants/#//apple_ref/doc/constant_group/Video_Settings
152  NSMutableDictionary *videoOutputSettings = [@{
153  AVVideoCodecKey: videoEncoding,
154  AVVideoWidthKey: [NSNumber numberWithInt:self.originalWidth],
155  AVVideoHeightKey: [NSNumber numberWithInt:self.originalHeight]
156  } mutableCopy];
157 
158  float fudge = 0.75;
159  float clampedQuality = MAX(format.imageQuality, 0.01);
160  float bitrate = clampedQuality * width * height * 60. * fudge;
161 
162 #pragma clang diagnostic push
163 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
164  // The replacements, AVVideoCodecType*, aren't available until macOS 10.13.
165  if( [videoEncoding isEqualToString:AVVideoCodecJPEG] )
166  videoOutputSettings[AVVideoCompressionPropertiesKey] = @{
167  AVVideoQualityKey: @(clampedQuality),
168  };
169  else if ([videoEncoding isEqualToString:AVVideoCodecH264]
170  || [videoEncoding isEqualToString:@"hvc1" /*AVVideoCodecTypeHEVC*/])
171  videoOutputSettings[AVVideoCompressionPropertiesKey] = @{
172  AVVideoAverageBitRateKey: @(bitrate),
173  };
174 #pragma clang diagnostic pop
175  else if ([videoEncoding isEqualToString:@"muxa" /*AVVideoCodecTypeHEVCWithAlpha*/])
176  {
177  videoOutputSettings[AVVideoCompressionPropertiesKey] = @{
178  AVVideoAverageBitRateKey: @(bitrate),
179  @"TargetQualityForAlpha" /*kVTCompressionPropertyKey_TargetQualityForAlpha*/: @(clampedQuality),
180  @"AlphaChannelMode" /*kVTCompressionPropertyKey_AlphaChannelMode*/: @"PremultipliedAlpha", // kVTAlphaChannelMode_PremultipliedAlpha
181  };
182  }
183 
184  if (videoOutputSettings[AVVideoCompressionPropertiesKey][AVVideoAverageBitRateKey])
185  VUserLog("Asking AV Foundation to encode with average bitrate %0.2g Mbit/sec (%g * %d * %d * 60 * %g).", bitrate / 1024.f / 1024.f, clampedQuality, width, height, fudge);
186 
187  self.videoInput = [[[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoOutputSettings] autorelease];
188  [videoOutputSettings release];
189  [self.videoInput setExpectsMediaDataInRealTime:YES];
190 
191  NSDictionary *pa = @{
192  (NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_32BGRA],
193  (NSString *)kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.originalWidth],
194  (NSString *)kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.originalHeight],
195  };
196 
197  self.avAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:pa];
198 
199  if ([self.assetWriter canAddInput:self.videoInput])
200  {
201  [self.assetWriter addInput:self.videoInput];
202  }
203  else
204  {
205  VUserLog("Failed adding a video input to the AVWriter.");
206  self.assetWriter = nil;
207  self.videoInput = nil;
208  self.avAdaptor = nil;
209 
210  return NO;
211  }
212 
213  self.originalChannelCount = channels;
214 
216  if(channels > 0)
217  {
218  AudioStreamBasicDescription audioFormat;
219  bzero(&audioFormat, sizeof(audioFormat));
220 
221  audioFormat.mSampleRate = VuoAudioSamples_sampleRate;
222  audioFormat.mFormatID = kAudioFormatLinearPCM;
223  audioFormat.mFramesPerPacket = 1;
224  audioFormat.mChannelsPerFrame = channels;
225  int bytes_per_sample = sizeof(float);
226  audioFormat.mFormatFlags = kAudioFormatFlagIsFloat;
227  audioFormat.mBitsPerChannel = bytes_per_sample * 8;
228  audioFormat.mBytesPerPacket = bytes_per_sample * channels;
229  audioFormat.mBytesPerFrame = bytes_per_sample * channels;
230 
231  CMFormatDescriptionRef fmt;
232  CMAudioFormatDescriptionCreate(kCFAllocatorDefault,
233  &audioFormat,
234  0,
235  NULL,
236  0,
237  NULL,
238  NULL,
239  &fmt
240  );
241  self.audio_fmt_desc = fmt;
242 
243  AudioChannelLayout acl;
244  bzero( &acl, sizeof(acl));
245  acl.mChannelLayoutTag = channels > 1 ? kAudioChannelLayoutTag_Stereo : kAudioChannelLayoutTag_Mono;
246 
247  int audioEncoding = kAudioFormatLinearPCM;
248  NSDictionary* audioOutputSettings;
249 
250  if(format.audioEncoding == VuoAudioEncoding_LinearPCM)
251  {
252  audioEncoding = kAudioFormatLinearPCM;
253  audioOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
254  [NSNumber numberWithInt: audioEncoding ], AVFormatIDKey,
255  [NSNumber numberWithInt: channels ], AVNumberOfChannelsKey,
256  [NSNumber numberWithFloat: VuoAudioSamples_sampleRate], AVSampleRateKey,
257  [NSNumber numberWithInt: channels],AVNumberOfChannelsKey,
258  [NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
259  [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
260  [NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
261  [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
262  [ NSData dataWithBytes: &acl length: sizeof( acl ) ], AVChannelLayoutKey,
263  nil];
264  }
265  else
266  {
267  audioEncoding = kAudioFormatMPEG4AAC;
268  long audioBitrate = (long)(MIN_AUDIO_BITRATE + ((float)(MAX_AUDIO_BITRATE-MIN_AUDIO_BITRATE) * format.audioQuality));
269 
270  audioOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
271  [ NSNumber numberWithInt: audioEncoding ], AVFormatIDKey,
272  [ NSNumber numberWithInt: channels ], AVNumberOfChannelsKey,
273  [ NSNumber numberWithFloat: VuoAudioSamples_sampleRate], AVSampleRateKey,
274  [ NSNumber numberWithInt:audioBitrate], AVEncoderBitRateKey,
275  [ NSData dataWithBytes: &acl length: sizeof( acl ) ], AVChannelLayoutKey,
276  nil];
277  }
278 
279  self.audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioOutputSettings];
280  self.audioInput.expectsMediaDataInRealTime=YES;
281  self.audio_sample_position = 0;
282 
283  if([self.assetWriter canAddInput:self.audioInput])
284  {
285  [self.assetWriter addInput:self.audioInput];
286  }
287  else
288  {
289  VUserLog("Could not add audio input.");
290  self.audioInput = nil;
291  }
292  }
293 
294  // initiates a sample-writing at time 0
295  [self.assetWriter startWriting];
296  [self.assetWriter startSessionAtSourceTime:kCMTimeZero];
297 
298  self.firstFrame = true;
299 
300  return YES;
301 }
302 
303 - (void) appendImage:(VuoImage)image presentationTime:(double)timestamp blockIfNotReady:(BOOL)blockIfNotReady
304 {
305  if(image->pixelsWide != self.originalWidth || image->pixelsHigh != self.originalHeight)
306  {
307  VUserLog("Error: Can't append image because it is not the same dimensions as the the current movie.");
308  return;
309  }
310 
311  if(!self.videoInput.readyForMoreMediaData)
312  {
313  if (blockIfNotReady)
314  {
315  VUserLog("Warning: The AVFoundation asset writer isn't keeping up; waiting for it to catch up before writing this video frame.");
316  while (!self.videoInput.readyForMoreMediaData)
317  usleep(USEC_PER_SEC/10);
318  }
319  else
320  {
321  VUserLog("Error: The AVFoundation asset writer isn't keeping up; dropping this video frame.");
322  return;
323  }
324  }
325 
326  CVPixelBufferRef pb = nil;
327 
328  const unsigned char *buf = VuoImage_getBuffer(image, GL_BGRA);
329 
330  CVReturn ret = CVPixelBufferPoolCreatePixelBuffer(nil, [self.avAdaptor pixelBufferPool], &pb);
331 
332  if(ret != kCVReturnSuccess)
333  {
334  VUserLog("Error: Couldn't get PixelBuffer from pool. %d", ret);
335  return;
336  }
337 
338  ret = CVPixelBufferLockBaseAddress(pb, 0);
339 
340  if(ret != kCVReturnSuccess)
341  {
342  VUserLog("Error: Couldn't lock PixelBuffer base address");
343  return;
344  }
345 
346  unsigned char *bytes = (unsigned char*)CVPixelBufferGetBaseAddress(pb);
347 
348  if(!bytes)
349  {
350  VUserLog("Error: Couldn't get the pixel buffer base address");
351  return;
352  }
353 
354  unsigned int bytesPerRow = CVPixelBufferGetBytesPerRow(pb);
355 
356  for(unsigned long y = 0; y < self.originalHeight; y++)
357  memcpy(bytes + bytesPerRow * (self.originalHeight - y - 1), buf + self.originalWidth * y * 4, self.originalWidth * 4);
358 
359  ret = CVPixelBufferUnlockBaseAddress(pb, 0);
360 
361  if(ret != kCVReturnSuccess)
362  {
363  VUserLog("Error: Couldn't unlock pixelbuffer base address. %d", ret);
364  return;
365  }
366 
367  CMTime presentationTime = CMTimeMakeWithSeconds(timestamp, TIMEBASE);
368 
369  while (CMTimeCompare(presentationTime, self.lastImageTimestamp) <= 0)
370  presentationTime.value++;
371 
372  self.lastImageTimestamp = presentationTime;
373 
374 // VLog("video time: %lld %f pts: %f ts=%g", presentationTime.value, [[NSDate date] timeIntervalSinceDate:self.startDate], CMTimeGetSeconds(presentationTime),timestamp);
375 
376  if (![self.avAdaptor appendPixelBuffer:pb withPresentationTime:presentationTime])
377  {
378  VUserLog("Couldn't write frame %lld (%fs): %s", presentationTime.value, CMTimeGetSeconds(presentationTime), [[_assetWriter.error description] UTF8String]);
379  }
380 
381  if(pb)
382  CVPixelBufferRelease(pb);
383 }
384 
385 - (void) appendAudio:(VuoList_VuoAudioSamples) samples presentationTime:(VuoReal)timestamp blockIfNotReady:(BOOL)blockIfNotReady
386 {
387  long sampleCount = VuoAudioSamples_bufferSize;
388  long channelCount = VuoListGetCount_VuoAudioSamples(samples);
389 
390  if(channelCount != self.originalChannelCount)
391  {
392  if (self.originalChannelCount == -1)
393  VUserLog("Error: Attempting to write %lu audio channels to a silent movie.", channelCount);
394  else
395  VUserLog("Error: Attempting to write %lu audio channels to a movie with %d channels.", channelCount, self.originalChannelCount);
396  return;
397  }
398 
399  if ( !self.audioInput.readyForMoreMediaData) {
400  if (blockIfNotReady)
401  {
402  VUserLog("Warning: The AVFoundation asset writer isn't keeping up; waiting for it to catch up before writing this audio frame.");
403  while (!self.audioInput.readyForMoreMediaData)
404  usleep(USEC_PER_SEC/10);
405  }
406  else
407  {
408  VUserLog("Error: The AVFoundation asset writer isn't keeping up; dropping this audio frame.");
409  return;
410  }
411  }
412 
413  OSStatus status;
414  CMBlockBufferRef bbuf = NULL;
415  CMSampleBufferRef sbuf = NULL;
416 
417  size_t buflen = sampleCount * channelCount * sizeof(float);
418 
419  float* interleaved = (float*)malloc(sizeof(float) * sampleCount * channelCount);
420  VuoDefer(^{ free(interleaved); });
421 
422  for(int n = 0; n < channelCount; n++)
423  {
425  double *channel = s.samples;
426  if (!channel)
427  {
428  VUserLog("Error: Attempting to write a NULL audio sample buffer. Skipping this audio frame.");
429  return;
430  }
432  {
433  VUserLog("Error: Attempting to write an audio sample buffer that has %lld samples, but expected %lld. Skipping this audio frame.", s.sampleCount, VuoAudioSamples_bufferSize);
434  return;
435  }
436 
437  for(int i = 0; i < sampleCount; i++)
438  {
439  interleaved[i*channelCount + n] = channel[i];
440  }
441  }
442 
443  // Create sample buffer for adding to the audio input.
444  CMBlockBufferRef tmp;
445  status = CMBlockBufferCreateWithMemoryBlock( kCFAllocatorDefault,
446  interleaved,
447  buflen,
448  kCFAllocatorNull,
449  NULL,
450  0,
451  buflen,
452  0,
453  &tmp);
454  if (status != noErr) {
455  char *s = VuoOsStatus_getText(status);
456  VUserLog("CMBlockBufferCreateWithMemoryBlock error: %s", s);
457  free(s);
458  return;
459  }
460 
461  status = CMBlockBufferCreateContiguous(kCFAllocatorDefault, tmp, kCFAllocatorDefault, NULL, 0,
462  buflen, kCMBlockBufferAlwaysCopyDataFlag, &bbuf);
463 
464  CFRelease(tmp);
465 
466  if (status != noErr) {
467  char *s = VuoOsStatus_getText(status);
468  VUserLog("CMBlockBufferCreateContiguous error: %s", s);
469  free(s);
470  return;
471  }
472 
473  CMTime presentationTime = CMTimeMakeWithSeconds(timestamp, TIMEBASE);
474 
475  // if(self.firstFrame)
476  // {
477  // self.firstFrame = false;
478  // self.startDate = [NSDate date];
479  // timestamp = CMTimeMake(0, AUDIO_TIMEBASE);
480  // }
481  // else
482  // {
483  // timestamp = CMTimeMake([[NSDate date] timeIntervalSinceDate:self.startDate] * AUDIO_TIMEBASE, AUDIO_TIMEBASE);
484  // }
485 
486  // CMTime timestamp = CMTimeMake(self.audio_sample_position, VuoAudioSamples_sampleRate);
487  // self.audio_sample_position += sampleCount;
488 
489  while (CMTimeCompare(presentationTime, self.lastAudioTimestamp) <= 0)
490  presentationTime.value++;
491 
492  self.lastAudioTimestamp = presentationTime;
493 
494 // VLog("audio time: %lld %f pts: %f ts=%g", presentationTime.value, [[NSDate date] timeIntervalSinceDate:self.startDate], CMTimeGetSeconds(presentationTime),timestamp);
495 
496  status = CMAudioSampleBufferCreateWithPacketDescriptions( kCFAllocatorDefault,
497  bbuf,
498  TRUE,
499  0,
500  NULL,
501  self.audio_fmt_desc,
503  presentationTime,
504  NULL, &sbuf);
505 
506  if (status != noErr) {
507  VUserLog("CMSampleBufferCreate error");
508  return;
509  }
510 
511  if ( ![self.audioInput appendSampleBuffer:sbuf] )
512  {
513  VUserLog("AppendSampleBuffer error");
514  }
515 
516  CFRelease(bbuf);
517  CFRelease(sbuf);
518  bbuf = nil;
519  sbuf = nil;
520 }
521 
522 - (void) finalizeRecording
523 {
524  if( self.assetWriter )
525  {
526  [self.videoInput markAsFinished];
527 
528  if(self.audioInput != nil)
529  [self.audioInput markAsFinished];
530 
531  dispatch_semaphore_t finishedWriting = dispatch_semaphore_create(0);
532  [_assetWriter finishWritingWithCompletionHandler:^{
533  dispatch_semaphore_signal(finishedWriting);
534  }];
535  dispatch_semaphore_wait(finishedWriting, DISPATCH_TIME_FOREVER);
536  dispatch_release(finishedWriting);
537 
538  if (_assetWriter.status != AVAssetWriterStatusCompleted)
539  VUserLog("Error: %s", [[_assetWriter.error localizedDescription] UTF8String]);
540 
541  self.assetWriter = nil;
542  self.videoInput = nil;
543  self.audioInput = nil;
544  }
545 }
546 
547 - (void) dealloc
548 {
549  if(self.assetWriter)
550  [self finalizeRecording];
551 
552  [super dealloc];
553 }
554 
555 @end