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