Vuo  2.0.0
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  AVVideoCodecKey: AVVideoCodecH264,
105  AVVideoWidthKey: [NSNumber numberWithInt:_originalWidth],
106  AVVideoHeightKey: [NSNumber numberWithInt:_originalHeight]
107  };
108 
109 
110  self.assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
111  _assetWriterInput.expectsMediaDataInRealTime = YES;
112 
113 
114 
115  NSDictionary *pa = @{
116  (NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_32BGRA],
117  (NSString *)kCVPixelBufferWidthKey: [NSNumber numberWithInt:_originalWidth],
118  (NSString *)kCVPixelBufferHeightKey: [NSNumber numberWithInt:_originalHeight],
119  };
120 
121  self.assetWriterInputPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor
122  assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetWriterInput
123  sourcePixelBufferAttributes:pa];
124 
125  if (![_assetWriter canAddInput:_assetWriterInput])
126  {
127  VUserLog("Error adding AVAssetWriterInput: %s", [[_assetWriter.error description] UTF8String]);
128  return nil;
129  }
130  [_assetWriter addInput:_assetWriterInput];
131 
132  if (![_assetWriter startWriting])
133  {
134  VUserLog("Error starting writing: %s", [[_assetWriter.error description] UTF8String]);
135  return nil;
136  }
137  [_assetWriter startSessionAtSourceTime:CMTimeMake(0, TIMEBASE)];
138 
139 
140  // Save the current image (to ensure the movie has a frame, even if the composition is stationary).
141  [self saveImage:l.ioSurface];
142  }
143 
144  return self;
145 }
146 
147 
151 - (void)appendBuffer:(const unsigned char *)sourceBytes width:(unsigned long)width height:(unsigned long)height
152 {
153  double captureTime = VuoLogGetTime() - _startTime;
154 
155  CMTime time;
156  if (_first)
157  {
158  time = CMTimeMake(0, TIMEBASE);
159  _first = false;
160  _startTime = VuoLogGetTime();
161  }
162  else
163  time = CMTimeMake(captureTime * TIMEBASE, TIMEBASE);
164 
165  if (_stopping)
166  return;
167 
168  if (!_assetWriterInput.readyForMoreMediaData)
169  {
170  VUserLog("Warning: AVFoundation video encoder isn't keeping up. Dropping this frame.");
171  return;
172  }
173 
174  CVPixelBufferRef pb = NULL;
175  CVReturn ret = CVPixelBufferPoolCreatePixelBuffer(NULL, [_assetWriterInputPixelBufferAdaptor pixelBufferPool], &pb);
176  if (ret != kCVReturnSuccess)
177  {
178  VUserLog("Error: Couldn't get buffer from pool: %d", ret);
179  return;
180  }
181  VuoDefer(^{ CVPixelBufferRelease(pb); });
182 
183  ret = CVPixelBufferLockBaseAddress(pb, 0);
184  if (ret != kCVReturnSuccess)
185  {
186  VUserLog("Error locking buffer: %d", ret);
187  return;
188  }
189 
190  unsigned char *bytes = (unsigned char *)CVPixelBufferGetBaseAddress(pb);
191  if (!bytes)
192  {
193  VUserLog("Error getting buffer base address.");
194  ret = CVPixelBufferUnlockBaseAddress(pb, 0);
195  if (ret != kCVReturnSuccess)
196  VUserLog("Error unlocking buffer: %d", ret);
197  return;
198  }
199 
200  unsigned int bytesPerRow = CVPixelBufferGetBytesPerRow(pb);
201 
202  // Flip the image data (OpenGL returns flipped data, but CVPixelBufferRef assumes it is not flipped),
203  // while copying it into `bytes`.
204  for (unsigned long y = 0; y < height; ++y)
205  memcpy(bytes + bytesPerRow * (height - y - 1), sourceBytes + width * y * 4, width * 4);
206 
207  ret = CVPixelBufferUnlockBaseAddress(pb, 0);
208  if (ret != kCVReturnSuccess)
209  VUserLog("Error unlocking buffer: %d", ret);
210 
211 
212  if (CMTimeCompare(time, _priorFrameTime) <= 0)
213  {
214 // VLog("Warning: Same or earlier time than prior frame; sliding this frame back.");
215  time = _priorFrameTime;
216  ++time.value;
217  }
218 
219 
220  if (![_assetWriterInputPixelBufferAdaptor appendPixelBuffer:pb withPresentationTime:time])
221  VUserLog("Error appending buffer: %s", [[_assetWriter.error description] UTF8String]);
222 
223  _priorFrameTime = time;
224 }
225 
226 static void VuoWindowRecorder_doNothingCallback(VuoImage imageToFree)
227 {
228 }
229 
235 - (void)saveImage:(VuoIoSurface)vis
236 {
237  if (!vis)
238  {
239  VUserLog("Error: NULL IOSurface. Skipping frame.");
240  return;
241  }
242 
243  unsigned short width = VuoIoSurfacePool_getWidth(vis);
244  unsigned short height = VuoIoSurfacePool_getHeight(vis);
245 
246  if (width == 0 || height == 0)
247  {
248  VUserLog("Error: Invalid viewport size %dx%d. Skipping frame.", width, height);
249  return;
250  }
251 
252  double t0 = VuoLogGetElapsedTime();
253  if (!_stopping)
254  dispatch_sync(_queue, ^{
255  if (!_stopping)
256  {
257  VuoImage image = VuoImage_makeClientOwnedGlTextureRectangle(VuoIoSurfacePool_getTexture(vis), GL_RGBA8, width, height, VuoWindowRecorder_doNothingCallback, NULL);
258  VuoRetain(image);
259 
260  if (width == _originalWidth && height == _originalHeight)
261  {
262  // rdar://23547737 — glGetTexImage() returns garbage for OpenGL textures backed by IOSurfaces
263  VuoImage copiedImage = VuoImage_makeCopy(image, false, 0, 0, false);
264  VuoRetain(copiedImage);
265  VuoRelease(image);
266  image = copiedImage;
267  }
268  else
269  {
270  // Resize.
271  if (width != _originalWidth || height != _originalHeight)
272  {
273  VuoImage resizedImage = VuoImageResize_resize(image, _resize, VuoSizingMode_Fit, _originalWidth, _originalHeight);
274  if (!resizedImage)
275  {
276  VUserLog("Error: Failed to resize image.");
277  VuoRelease(image);
278  return;
279  }
280 
281  VuoRetain(resizedImage);
282  VuoRelease(image);
283  image = resizedImage;
284  }
285  }
286 
287  // Download from GPU to CPU, and append to movie.
288  dispatch_async(_queue, ^{
289  double t0 = VuoLogGetElapsedTime();
290  const unsigned char *sourceBytes = VuoImage_getBuffer(image, GL_BGRA);
291  if (sourceBytes)
292  {
293  [self appendBuffer:sourceBytes width:_originalWidth height:_originalHeight];
294  VuoRelease(image);
295  double t1 = VuoLogGetElapsedTime();
296  _totalAsyncTime += t1 - t0;
297  ++_frameCount;
298  }
299  else
300  VUserLog("Error: Couldn't download image.");
301  });
302 
303  _priorWidth = width;
304  _priorHeight = height;
305 
306  double t1 = VuoLogGetElapsedTime();
307  _totalSyncTime += t1 - t0;
308  }
309  });
310 }
311 
317 - (void)finish
318 {
319  _stopping = true;
320 
321  dispatch_sync(_queue, ^{
322  dispatch_semaphore_t finishedWriting = dispatch_semaphore_create(0);
323  [_assetWriter finishWritingWithCompletionHandler:^{
324  dispatch_semaphore_signal(finishedWriting);
325  }];
326  dispatch_semaphore_wait(finishedWriting, DISPATCH_TIME_FOREVER);
327  dispatch_release(finishedWriting);
328 
329  if (_assetWriter.status != AVAssetWriterStatusCompleted)
330  VUserLog("Error: %s", [[_assetWriter.error localizedDescription] UTF8String]);
331  });
332  dispatch_release(_queue);
333 
334  VuoRelease(_resize);
335 
336  if (VuoIsDebugEnabled())
337  {
338  VUserLog("Average render-blocking record time per frame: %g", _totalSyncTime / _frameCount);
339  VUserLog("Average background record time per frame: %g", _totalAsyncTime / _frameCount);
340  }
341 }
342 
343 @end