To record screencast, AVAssetWriter APIs were called for each
cocoa_update call.

Commands for start/stop recording were added to View menu.

AVFoundation, CoreMedia and CoreVideo were added as linking
dependencies.

Signed-off-by: Zhang Chen <tgfb...@me.com>
---
 meson.build    |   6 +++
 ui/cocoa.m     | 132 +++++++++++++++++++++++++++++++++++++++++++++++--
 ui/meson.build |   1 +
 3 files changed, 136 insertions(+), 3 deletions(-)

diff --git a/meson.build b/meson.build
index 886f0a9343..70c38c4135 100644
--- a/meson.build
+++ b/meson.build
@@ -281,6 +281,9 @@ emulator_link_args = []
 nvmm =not_found
 hvf = not_found
 host_dsosuf = '.so'
+avfoundation = []
+coremedia = []
+corevideo = []
 if targetos == 'windows'
   socket = cc.find_library('ws2_32')
   winmm = cc.find_library('winmm')
@@ -292,6 +295,9 @@ if targetos == 'windows'
   host_dsosuf = '.dll'
 elif targetos == 'darwin'
   coref = dependency('appleframeworks', modules: 'CoreFoundation')
+  avfoundation = dependency('appleframeworks', modules: 'AVFoundation')
+  coremedia = dependency('appleframeworks', modules: 'CoreMedia')
+  corevideo = dependency('appleframeworks', modules: 'CoreVideo')
   iokit = dependency('appleframeworks', modules: 'IOKit', required: false)
   host_dsosuf = '.dylib'
 elif targetos == 'sunos'
diff --git a/ui/cocoa.m b/ui/cocoa.m
index 69745c483b..6a0fc24414 100644
--- a/ui/cocoa.m
+++ b/ui/cocoa.m
@@ -25,6 +25,7 @@
 #include "qemu/osdep.h"
 
 #import <Cocoa/Cocoa.h>
+#import <AVFoundation/AVFoundation.h>
 #include <crt_externs.h>
 
 #include "qemu-common.h"
@@ -309,6 +310,12 @@ static void handleAnyDeviceErrors(Error * err)
     BOOL isMouseGrabbed;
     BOOL isFullscreen;
     BOOL isAbsoluteEnabled;
+    AVAssetWriter *capture;
+    AVAssetWriterInput *captureInput;
+    AVAssetWriterInputPixelBufferAdaptor *captureInputAdaptor;
+    BOOL isCapturing;
+    NSDate *captureStart;
+    CVPixelBufferRef captureBuffer;
 }
 - (void) switchSurface:(pixman_image_t *)image;
 - (void) grabMouse;
@@ -332,6 +339,9 @@ static void handleAnyDeviceErrors(Error * err)
 - (float) cdy;
 - (QEMUScreen) gscreen;
 - (void) raiseAllKeys;
+- (void) startCapture;
+- (void) stopCapture;
+- (BOOL) isCapturing;
 @end
 
 QemuCocoaView *cocoaView;
@@ -364,6 +374,10 @@ QemuCocoaView *cocoaView;
     [super dealloc];
 }
 
+- (BOOL) isCapturing {
+    return isCapturing;
+}
+
 - (BOOL) isOpaque
 {
     return YES;
@@ -425,6 +439,81 @@ QemuCocoaView *cocoaView;
     [NSCursor unhide];
 }
 
+- (IBAction) startCapture
+{
+    NSError *err;
+    
+    NSString *outputPath = [NSString 
stringWithFormat:@"/tmp/capture_%.1f.mp4", [[NSDate now] 
timeIntervalSinceReferenceDate]];
+    NSURL *fileURL = [NSURL fileURLWithPath:outputPath];
+    capture = [[AVAssetWriter alloc] initWithURL:fileURL 
fileType:AVFileTypeMPEG4 error:&err];
+    
+    captureInput = [[AVAssetWriterInput alloc] 
initWithMediaType:AVMediaTypeVideo
+                                                   outputSettings:@{
+        AVVideoCodecKey: AVVideoCodecTypeH264,
+        AVVideoWidthKey: [NSNumber numberWithInt:screen.width],
+        AVVideoHeightKey: [NSNumber numberWithInt:screen.height],
+    }];
+    NSParameterAssert([capture canAddInput:captureInput]);
+    [capture addInput:captureInput];
+    captureInputAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] 
initWithAssetWriterInput:captureInput sourcePixelBufferAttributes:nil];
+    
+    OSStatus bufferStatus = CVPixelBufferCreateWithBytes(NULL,
+                                                         screen.width,
+                                                         screen.height,
+                                                         
kCVPixelFormatType_32BGRA,
+                                                         
pixman_image_get_data(pixman_image),
+                                                         
pixman_image_get_stride(pixman_image),
+                                                         NULL,
+                                                         NULL,
+                                                         NULL,
+                                                         &captureBuffer);
+    if (bufferStatus != kCVReturnSuccess) {
+        NSLog(@"err creating pixel buf: %d", bufferStatus);
+        return;
+    }
+    captureStart = [NSDate new];
+    [capture startWriting];
+    [capture startSessionAtSourceTime:kCMTimeZero];
+    isCapturing = TRUE;
+    [self captureFrame];
+}
+
+- (void) stopCapture
+{
+    if (isCapturing) {
+        isCapturing = FALSE;
+        NSDate *now = [NSDate now];
+        NSTimeInterval interval = [now timeIntervalSinceDate:captureStart];
+        CMTime ts = CMTimeMakeWithSeconds(interval, 1000000);
+        [captureInput markAsFinished];
+        [capture endSessionAtSourceTime:ts];
+        pixman_image_ref(pixman_image);
+        [capture finishWritingWithCompletionHandler:^() {
+            NSLog(@"finishWriting");
+            [captureInputAdaptor release];
+            [captureInput release];
+            CFRelease(captureBuffer);
+            pixman_image_unref(pixman_image);
+        }];
+        [captureStart release];
+    }
+}
+
+- (void) captureFrame
+{
+    if (isCapturing && captureBuffer && [captureInput 
isReadyForMoreMediaData]) {
+        NSDate *now = [NSDate now];
+        NSTimeInterval interval = [now timeIntervalSinceDate:captureStart];
+        CMTime ts = CMTimeMakeWithSeconds(interval, 1000000);
+        CFRetain(captureBuffer);
+        pixman_image_ref(pixman_image);
+        [captureInputAdaptor appendPixelBuffer:captureBuffer
+                           withPresentationTime:ts];
+        CFRelease(captureBuffer);
+        pixman_image_unref(pixman_image);
+    }
+}
+
 - (void) drawRect:(NSRect) rect
 {
     COCOA_DEBUG("QemuCocoaView: drawRect\n");
@@ -573,6 +662,7 @@ QemuCocoaView *cocoaView;
     bool isResize = (w != screen.width || h != screen.height || cdx == 0.0);
 
     int oldh = screen.height;
+    BOOL needsRestartCapture = isResize && isCapturing;
     if (isResize) {
         // Resize before we trigger the redraw, or we'll redraw at the wrong 
size
         COCOA_DEBUG("switchSurface: new size %d x %d\n", w, h);
@@ -580,6 +670,7 @@ QemuCocoaView *cocoaView;
         screen.height = h;
         [self setContentDimensions];
         [self setFrame:NSMakeRect(cx, cy, cw, ch)];
+        [self stopCapture];
     }
 
     // update screenBuffer
@@ -588,7 +679,9 @@ QemuCocoaView *cocoaView;
     }
 
     pixman_image = image;
-
+    if (needsRestartCapture) {
+        [self startCapture];
+    }
     // update windows
     if (isFullscreen) {
         [[fullScreenWindow contentView] setFrame:[[NSScreen mainScreen] 
frame]];
@@ -1117,6 +1210,8 @@ QemuCocoaView *cocoaView;
 - (IBAction) do_about_menu_item: (id) sender;
 - (void)make_about_window;
 - (void)adjustSpeed:(id)sender;
+- (IBAction) startCapture:(id)sender;
+- (IBAction) stopCapture:(id)sender;
 @end
 
 @implementation QemuCocoaAppController
@@ -1175,8 +1270,10 @@ QemuCocoaView *cocoaView;
 {
     COCOA_DEBUG("QemuCocoaAppController: dealloc\n");
 
-    if (cocoaView)
+    if (cocoaView) {
+        [cocoaView stopCapture];
         [cocoaView release];
+    }
     [super dealloc];
 }
 
@@ -1569,6 +1666,23 @@ QemuCocoaView *cocoaView;
     COCOA_DEBUG("cpu throttling at %d%c\n", cpu_throttle_get_percentage(), 
'%');
 }
 
+- (IBAction) startCapture:(id)sender
+{
+    [sender setEnabled:NO];
+    [cocoaView startCapture];
+    if ([cocoaView isCapturing]) {
+        [[[sender menu] itemWithTitle:@"Stop Capture"] setEnabled: YES];
+    }
+}
+
+- (IBAction) stopCapture:(id)sender
+{
+    [sender setEnabled: NO];
+    [cocoaView stopCapture];
+    if (![cocoaView isCapturing]) {
+        [[[sender menu] itemWithTitle:@"Capture"] setEnabled: YES];
+    }
+}
 @end
 
 @interface QemuApplication : NSApplication
@@ -1623,8 +1737,18 @@ static void create_initial_menus(void)
 
     // View menu
     menu = [[NSMenu alloc] initWithTitle:@"View"];
+    [menu setAutoenablesItems: NO];
     [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Enter Fullscreen" 
action:@selector(doToggleFullScreen:) keyEquivalent:@"f"] autorelease]]; // 
Fullscreen
     [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Zoom To Fit" 
action:@selector(zoomToFit:) keyEquivalent:@""] autorelease]];
+    [menu addItem:[NSMenuItem separatorItem]];
+    menuItem = [[[NSMenuItem alloc] initWithTitle:@"Capture" 
action:@selector(startCapture:) keyEquivalent:@""] autorelease];
+    [menu addItem: menuItem];
+    [menuItem setTag:1200];
+    [menuItem setEnabled: YES];
+    menuItem = [[[NSMenuItem alloc] initWithTitle:@"Stop Capture" 
action:@selector(stopCapture:) keyEquivalent:@""] autorelease];
+    [menu addItem: menuItem];
+    [menuItem setTag:1201];
+    [menuItem setEnabled: NO];
     menuItem = [[[NSMenuItem alloc] initWithTitle:@"View" action:nil 
keyEquivalent:@""] autorelease];
     [menuItem setSubmenu:menu];
     [[NSApp mainMenu] addItem:menuItem];
@@ -1962,7 +2086,9 @@ static void cocoa_update(DisplayChangeListener *dcl,
     NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
     COCOA_DEBUG("qemu_cocoa: cocoa_update\n");
-
+    if ([cocoaView isCapturing]) {
+        [cocoaView captureFrame];
+    }
     dispatch_async(dispatch_get_main_queue(), ^{
         NSRect rect;
         if ([cocoaView cdx] == 1.0) {
diff --git a/ui/meson.build b/ui/meson.build
index 64286ba150..cef7e90319 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -25,6 +25,7 @@ softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files(
   'udmabuf.c',
 ))
 softmmu_ss.add(when: cocoa, if_true: files('cocoa.m'))
+softmmu_ss.add(when: avfoundation, if_true: [avfoundation, coremedia, 
corevideo])
 
 vnc_ss = ss.source_set()
 vnc_ss.add(files(
-- 
2.30.2


Reply via email to