Vuo  2.3.2
VuoWindowRecorder.m
Go to the documentation of this file.
1 
10 #include "VuoWindowRecorder.h"
11 
12 #include "VuoGraphicsWindow.h"
13 #include "VuoGraphicsLayer.h"
14 
15 #include <OpenGL/OpenGL.h>
16 #include <OpenGL/CGLMacro.h>
17 #include <AVFoundation/AVFoundation.h>
18 
19 #include "VuoGlPool.h"
20 #include "VuoImageResize.h"
21 
22 #include "module.h"
23 
24 #ifdef VUO_COMPILER
26  "title" : "VuoWindowRecorder",
27  "dependencies" : [
28  "AVFoundation.framework",
29  "CoreMedia.framework",
30  "OpenGL.framework",
31  "VuoFont",
32  "VuoGlPool",
33  "VuoGraphicsWindow",
34  "VuoGraphicsLayer",
35  "VuoImage",
36  "VuoImageRenderer",
37  "VuoImageResize",
38  "VuoImageText",
39  "VuoShader"
40  ]
41  });
42 #endif
43 
44 
45 #define TIMEBASE 60
46 
47 
51 @interface VuoWindowRecorder ()
52 @property dispatch_queue_t queue;
53 @property bool first;
54 @property bool stopping;
55 @property double startTime;
56 @property CMTime priorFrameTime;
57 @property int originalWidth;
58 @property int originalHeight;
59 @property int priorWidth;
60 @property int priorHeight;
61 @property int frameCount;
62 @property double totalSyncTime;
63 @property double totalAsyncTime;
64 @property (retain) AVAssetWriter *assetWriter;
65 @property (retain) AVAssetWriterInput *assetWriterInput;
66 @property (retain) AVAssetWriterInputPixelBufferAdaptor *assetWriterInputPixelBufferAdaptor;
67 @property (assign) VuoImageResize resize;
68 @end
69 
70 
71 @implementation VuoWindowRecorder
72 
78 - (instancetype)initWithWindow:(VuoGraphicsWindow *)window url:(NSURL *)url
79 {
80  if (self = [super init])
81  {
82  _queue = dispatch_queue_create("org.vuo.VuoWindowRecorder", NULL);
83 
84  _first = true;
85  _priorFrameTime = CMTimeMake(-1, TIMEBASE);
86 
87  NSView *v = window.contentView;
88  VuoGraphicsLayer *l = (VuoGraphicsLayer *)v.layer;
89  NSRect frameInPoints = v.frame;
90  NSRect frameInPixels = [window convertRectToBacking:frameInPoints];
91  _originalWidth = _priorWidth = frameInPixels.size.width;
92  _originalHeight = _priorHeight = frameInPixels.size.height;
93 
94 
95  _resize = VuoImageResize_make();
96  VuoRetain(_resize);
97 
98 
99  NSError *e = nil;
100  self.assetWriter = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeQuickTimeMovie error:&e];
101  _assetWriter.movieFragmentInterval = CMTimeMake(TIMEBASE*10, TIMEBASE);
102 
103  NSDictionary *videoSettings = @{
104 #pragma clang diagnostic push
105 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
106  // The replacement, AVVideoCodecTypeH264, isn't available until macOS 10.13.
107  AVVideoCodecKey: AVVideoCodecH264,
108 #pragma clang diagnostic pop
109  AVVideoWidthKey: [NSNumber numberWithInt:_originalWidth],
110  AVVideoHeightKey: [NSNumber numberWithInt:_originalHeight]
111  };
112 
113 
114  self.assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
115  _assetWriterInput.expectsMediaDataInRealTime = YES;
116 
117 
118 
119  NSDictionary *pa = @{
120  (NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_32BGRA],
121  (NSString *)kCVPixelBufferWidthKey: [NSNumber numberWithInt:_originalWidth],
122  (NSString *)kCVPixelBufferHeightKey: [NSNumber numberWithInt:_originalHeight],
123  };
124 
125  self.assetWriterInputPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor
126  assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetWriterInput
127  sourcePixelBufferAttributes:pa];
128 
129  if (![_assetWriter canAddInput:_assetWriterInput])
130  {
131  VUserLog("Error adding AVAssetWriterInput: %s", [[_assetWriter.error description] UTF8String]);
132  return nil;
133  }
134  [_assetWriter addInput:_assetWriterInput];
135 
136  if (![_assetWriter startWriting])
137  {
138  VUserLog("Error starting writing: %s", [[_assetWriter.error description] UTF8String]);
139  return nil;
140  }
141  [_assetWriter startSessionAtSourceTime:CMTimeMake(0, TIMEBASE)];
142 
143 
144  // Save the current image (to ensure the movie has a frame, even if the composition is stationary).
145  [self saveImage:l.ioSurface];
146  }
147 
148  return self;
149 }
150 
151 
155 - (void)appendBuffer:(const unsigned char *)sourceBytes width:(unsigned long)width height:(unsigned long)height
156 {
157  double captureTime = VuoLogGetTime() - _startTime;
158 
159  CMTime time;
160  if (_first)
161  {
162  time = CMTimeMake(0, TIMEBASE);
163  _first = false;
164  _startTime = VuoLogGetTime();
165  }
166  else
167  time = CMTimeMake(captureTime * TIMEBASE, TIMEBASE);
168 
169  if (_stopping)
170  return;
171 
172  if (!_assetWriterInput.readyForMoreMediaData)
173  {
174  VUserLog("Warning: AVFoundation video encoder isn't keeping up. Dropping this frame.");
175  return;
176  }
177 
178  CVPixelBufferRef pb = NULL;
179  CVReturn ret = CVPixelBufferPoolCreatePixelBuffer(NULL, [_assetWriterInputPixelBufferAdaptor pixelBufferPool], &pb);
180  if (ret != kCVReturnSuccess)
181  {
182  VUserLog("Error: Couldn't get buffer from pool: %d", ret);
183  return;
184  }
185  VuoDefer(^{ CVPixelBufferRelease(pb); });
186 
187  ret = CVPixelBufferLockBaseAddress(pb, 0);
188  if (ret != kCVReturnSuccess)
189  {
190  VUserLog("Error locking buffer: %d", ret);
191  return;
192  }
193 
194  unsigned char *bytes = (unsigned char *)CVPixelBufferGetBaseAddress(pb);
195  if (!bytes)
196  {
197  VUserLog("Error getting buffer base address.");
198  ret = CVPixelBufferUnlockBaseAddress(pb, 0);
199  if (ret != kCVReturnSuccess)
200  VUserLog("Error unlocking buffer: %d", ret);
201  return;
202  }
203 
204  unsigned int bytesPerRow = CVPixelBufferGetBytesPerRow(pb);
205 
206  // Flip the image data (OpenGL returns flipped data, but CVPixelBufferRef assumes it is not flipped),
207  // while copying it into `bytes`.
208  for (unsigned long y = 0; y < height; ++y)
209  memcpy(bytes + bytesPerRow * (height - y - 1), sourceBytes + width * y * 4, width * 4);
210 
211  ret = CVPixelBufferUnlockBaseAddress(pb, 0);
212  if (ret != kCVReturnSuccess)
213  VUserLog("Error unlocking buffer: %d", ret);
214 
215 
216  if (CMTimeCompare(time, _priorFrameTime) <= 0)
217  {
218 // VLog("Warning: Same or earlier time than prior frame; sliding this frame back.");
219  time = _priorFrameTime;
220  ++time.value;
221  }
222 
223 
224  if (![_assetWriterInputPixelBufferAdaptor appendPixelBuffer:pb withPresentationTime:time])
225  VUserLog("Error appending buffer: %s", [[_assetWriter.error description] UTF8String]);
226 
227  _priorFrameTime = time;
228 }
229 
230 static void VuoWindowRecorder_doNothingCallback(VuoImage imageToFree)
231 {
232 }
233 
239 - (void)saveImage:(VuoIoSurface)vis
240 {
241  if (!vis)
242  {
243  VUserLog("Error: NULL IOSurface. Skipping frame.");
244  return;
245  }
246 
247  unsigned short width = VuoIoSurfacePool_getWidth(vis);
248  unsigned short height = VuoIoSurfacePool_getHeight(vis);
249 
250  if (width == 0 || height == 0)
251  {
252  VUserLog("Error: Invalid viewport size %dx%d. Skipping frame.", width, height);
253  return;
254  }
255 
256  double t0 = VuoLogGetElapsedTime();
257  if (!_stopping)
258  dispatch_sync(_queue, ^{
259  if (!_stopping)
260  {
261  VuoImage image = VuoImage_makeClientOwnedGlTextureRectangle(VuoIoSurfacePool_getTexture(vis), GL_RGBA8, width, height, VuoWindowRecorder_doNothingCallback, NULL);
262  VuoRetain(image);
263 
264  if (width == _originalWidth && height == _originalHeight)
265  {
266  // rdar://23547737 — glGetTexImage() returns garbage for OpenGL textures backed by IOSurfaces
267  VuoImage copiedImage = VuoImage_makeCopy(image, false, 0, 0, false);
268  VuoRetain(copiedImage);
269  VuoRelease(image);
270  image = copiedImage;
271  }
272  else
273  {
274  // Resize.
275  if (width != _originalWidth || height != _originalHeight)
276  {
277  VuoImage resizedImage = VuoImageResize_resize(image, _resize, VuoSizingMode_Fit, _originalWidth, _originalHeight);
278  if (!resizedImage)
279  {
280  VUserLog("Error: Failed to resize image.");
281  VuoRelease(image);
282  return;
283  }
284 
285  VuoRetain(resizedImage);
286  VuoRelease(image);
287  image = resizedImage;
288  }
289  }
290 
291  // Download from GPU to CPU, and append to movie.
292  dispatch_async(_queue, ^{
293  double t0 = VuoLogGetElapsedTime();
294  const unsigned char *sourceBytes = VuoImage_getBuffer(image, GL_BGRA);
295  if (sourceBytes)
296  {
297  [self appendBuffer:sourceBytes width:_originalWidth height:_originalHeight];
298  VuoRelease(image);
299  double t1 = VuoLogGetElapsedTime();
300  _totalAsyncTime += t1 - t0;
301  ++_frameCount;
302  }
303  else
304  VUserLog("Error: Couldn't download image.");
305  });
306 
307  _priorWidth = width;
308  _priorHeight = height;
309 
310  double t1 = VuoLogGetElapsedTime();
311  _totalSyncTime += t1 - t0;
312  }
313  });
314 }
315 
321 - (void)finish
322 {
323  _stopping = true;
324 
325  dispatch_sync(_queue, ^{
326  dispatch_semaphore_t finishedWriting = dispatch_semaphore_create(0);
327  [_assetWriter finishWritingWithCompletionHandler:^{
328  dispatch_semaphore_signal(finishedWriting);
329  }];
330  dispatch_semaphore_wait(finishedWriting, DISPATCH_TIME_FOREVER);
331  dispatch_release(finishedWriting);
332 
333  if (_assetWriter.status != AVAssetWriterStatusCompleted)
334  VUserLog("Error: %s", [[_assetWriter.error localizedDescription] UTF8String]);
335  });
336  dispatch_release(_queue);
337 
338  VuoRelease(_resize);
339 
340  if (VuoIsDebugEnabled())
341  {
342  VUserLog("Average render-blocking record time per frame: %g", _totalSyncTime / _frameCount);
343  VUserLog("Average background record time per frame: %g", _totalAsyncTime / _frameCount);
344  }
345 }
346 
347 @end