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