From 66128a1257d38550b190c0df3e389b59b61c30a4 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Wed, 18 Feb 2026 20:36:37 -0600 Subject: [PATCH] feat: start capture on app startup fix: always select new capture when desktop/window is selected --- .../linux/linux_pipewire_dma_capture.zig | 33 +++- src/capture/video/linux/pipewire/pipewire.zig | 44 +++-- src/capture/video/linux/pipewire/portal.zig | 139 ++++++++++++--- src/capture/video/video_capture.zig | 16 +- src/capture/video/windows/capture_windows.zig | 158 ------------------ .../video/windows/windows_video_capture.zig | 13 +- src/common/linux/token_storage.zig | 19 +++ src/state/audio_state.zig | 34 ++-- src/state_actor.zig | 40 +++-- 9 files changed, 257 insertions(+), 239 deletions(-) delete mode 100644 src/capture/video/windows/capture_windows.zig diff --git a/src/capture/video/linux/linux_pipewire_dma_capture.zig b/src/capture/video/linux/linux_pipewire_dma_capture.zig index 25e25f0..8869f11 100644 --- a/src/capture/video/linux/linux_pipewire_dma_capture.zig +++ b/src/capture/video/linux/linux_pipewire_dma_capture.zig @@ -4,11 +4,12 @@ const vk = @import("vulkan"); const types = @import("../../../types.zig"); const util = @import("../../../util.zig"); +const TokenStorage = @import("../../../common/linux/token_storage.zig"); const Vulkan = @import("../../../vulkan/vulkan.zig").Vulkan; const Pipewire = @import("./pipewire/pipewire.zig").Pipewire; const Chan = @import("../../../channel.zig").Chan; const ChanError = @import("../../../channel.zig").ChanError; -const VideoCaptureSourceType = @import("../video_capture.zig").VideoCaptureSourceType; +const VideoCaptureSelection = @import("../video_capture.zig").VideoCaptureSelection; const VideoCapture = @import("../video_capture.zig").VideoCapture; const VideoCaptureError = @import("../video_capture.zig").VideoCaptureError; const VulkanImageBuffer = @import("../../../vulkan/vulkan_image_buffer.zig").VulkanImageBuffer; @@ -16,6 +17,7 @@ const rc = @import("zigrc"); pub const LinuxPipewireDmaCapture = struct { const Self = @This(); + const log = std.log.scoped(.LinuxPipewireDmaCapture); allocator: std.mem.Allocator, vulkan: *Vulkan, @@ -31,7 +33,7 @@ pub const LinuxPipewireDmaCapture = struct { return self; } - pub fn selectSource(context: *anyopaque, source_type: VideoCaptureSourceType) (VideoCaptureError || anyerror)!void { + pub fn selectSource(context: *anyopaque, selection: VideoCaptureSelection) (VideoCaptureError || anyerror)!void { const self: *Self = @ptrCast(@alignCast(context)); if (self.pipewire) |pipewire| { // TODO: Probably don't have to destroy all of pipewire @@ -45,10 +47,26 @@ pub const LinuxPipewireDmaCapture = struct { self.vulkan, ); errdefer { - self.pipewire.?.deinit(); - self.pipewire = null; + if (self.pipewire) |pipewire| { + pipewire.deinit(); + self.pipewire = null; + } } - try self.pipewire.?.selectSource(source_type); + try self.pipewire.?.selectSource(selection); + } + + pub fn shouldRestoreCaptureSession(context: *anyopaque) !bool { + const self: *Self = @ptrCast(@alignCast(context)); + const restore_token = TokenStorage.loadToken(self.allocator, "restore_token") catch |err| { + log.err("failed to load restore token: {}", .{err}); + return false; + }; + if (restore_token == null) { + return false; + } + defer self.allocator.free(restore_token.?); + + return restore_token.?.len > 0; } pub fn nextFrame(context: *anyopaque) ChanError!void { @@ -72,7 +90,8 @@ pub const LinuxPipewireDmaCapture = struct { pub fn size(context: *anyopaque) ?types.Size { const self: *Self = @ptrCast(@alignCast(context)); - return if (self.pipewire.?.info) |info| + const pipewire = self.pipewire orelse return null; + return if (pipewire.info) |info| .{ .width = info.size.width, .height = info.size.height, @@ -95,6 +114,7 @@ pub const LinuxPipewireDmaCapture = struct { const self: *Self = @ptrCast(@alignCast(context)); if (self.pipewire) |pipewire| { pipewire.deinit(); + self.pipewire = null; } self.allocator.destroy(self); } @@ -104,6 +124,7 @@ pub const LinuxPipewireDmaCapture = struct { .ptr = self, .vtable = &.{ .selectSource = selectSource, + .shouldRestoreCaptureSession = shouldRestoreCaptureSession, .nextFrame = nextFrame, .closeAllChannels = closeAllChannels, .waitForFrame = waitForFrame, diff --git a/src/capture/video/linux/pipewire/pipewire.zig b/src/capture/video/linux/pipewire/pipewire.zig index f8c6864..880f321 100644 --- a/src/capture/video/linux/pipewire/pipewire.zig +++ b/src/capture/video/linux/pipewire/pipewire.zig @@ -8,7 +8,7 @@ const ChanError = @import("../../../../channel.zig").ChanError; const VulkanImageBufferChan = @import("./vulkan_image_buffer_chan.zig").VulkanImageBufferChan; const Vulkan = @import("../../../../vulkan/vulkan.zig").Vulkan; const VideoCaptureError = @import("../../video_capture.zig").VideoCaptureError; -const VideoCaptureSourceType = @import("../../video_capture.zig").VideoCaptureSourceType; +const VideoCaptureSelection = @import("../../video_capture.zig").VideoCaptureSelection; const c = @import("../../../../common/linux/pipewire_include.zig").c; const c_def = @import("../../../../common/linux/pipewire_include.zig").c_def; const Portal = @import("./portal.zig").Portal; @@ -49,15 +49,25 @@ pub const Pipewire = struct { vulkan: *Vulkan, ) (VideoCaptureError || anyerror)!*Self { const self = try allocator.create(Self); + errdefer allocator.destroy(self); self.* = Self{ .allocator = allocator, - .rx_chan = try .init(allocator), - .tx_chan = try .init(allocator), - .portal = try .init(allocator), + .rx_chan = undefined, + .tx_chan = undefined, + .portal = undefined, .vulkan = vulkan, - .vulkan_image_buffer_chan = try VulkanImageBufferChan.init(allocator), + .vulkan_image_buffer_chan = undefined, }; + self.rx_chan = try .init(allocator); + errdefer self.rx_chan.deinit(); + self.tx_chan = try .init(allocator); + errdefer self.tx_chan.deinit(); + self.portal = try .init(allocator); + errdefer self.portal.deinit(); + self.vulkan_image_buffer_chan = try .init(allocator); + errdefer self.vulkan_image_buffer_chan.deinit(); + return self; } @@ -111,14 +121,14 @@ pub const Pipewire = struct { self.allocator.destroy(self); } - pub fn selectSource( - self: *Self, - source_type: VideoCaptureSourceType, - ) (VideoCaptureError || anyerror)!void { - const pipewire_node = try self.portal.selectSource(source_type); + pub fn selectSource(self: *Self, selection: VideoCaptureSelection) (VideoCaptureError || anyerror)!void { + const pipewire_node = try self.portal.selectSource(selection); const pipewire_fd = try self.portal.openPipewireRemote(); errdefer _ = pw.close(pipewire_fd); + self.has_format = false; + self.info = null; + self.thread_loop = pw.pw_thread_loop_new( "spacecap-pipewire-capture-video", null, @@ -166,19 +176,23 @@ pub const Pipewire = struct { try self.startStream(pipewire_node); - while (!self.has_format) { + while (!self.hasValidInfo()) { pw.pw_thread_loop_wait(self.thread_loop); } pw.pw_thread_loop_unlock(self.thread_loop); - if (self.info == null or self.info.?.format < 0) { - return error.bad_format; - } - self.worker_thread = try std.Thread.spawn(.{}, workerMain, .{self}); } + fn hasValidInfo(self: *const Self) bool { + const info = self.info orelse return false; + return self.has_format and + info.format >= 0 and + info.size.width > 0 and + info.size.height > 0; + } + fn build_format(self: *const Self, b: ?*pw.spa_pod_builder, format: u32, modifiers: []const u64) ?*pw.spa_pod { _ = self; var format_frame = std.mem.zeroes(pw.spa_pod_frame); diff --git a/src/capture/video/linux/pipewire/portal.zig b/src/capture/video/linux/pipewire/portal.zig index 92aecfd..788e647 100644 --- a/src/capture/video/linux/pipewire/portal.zig +++ b/src/capture/video/linux/pipewire/portal.zig @@ -1,6 +1,6 @@ const std = @import("std"); const VideoCaptureError = @import("../../video_capture.zig").VideoCaptureError; -const VideoCaptureSourceType = @import("../../video_capture.zig").VideoCaptureSourceType; +const VideoCaptureSelection = @import("../../video_capture.zig").VideoCaptureSelection; const TokenStorage = @import("../../../../common/linux/token_storage.zig"); const log = std.log.scoped(.portal); @@ -32,8 +32,21 @@ fn mapGError(err: *c.GError) ?VideoCaptureError { return null; } +/// HACK: This is a bit of a hack. When the app starts up it tries to start a capture session +/// if there was a capture session when the app closed. It does this by checking the restore +/// token. There is no way of knowing that a restore token applies to a valid window. If it +/// doesn't, then the desktop portal screencast popup will open (seems like there is no way +/// to prevent this). This timeout will wait this long for the session to be restored, and +/// if it doesn't start, then it will be cancelled so that the source picker closes. This +/// will only occur during app startup if there is a cached refresh token. +/// This will cause the source picker to flash for this long upon app startup, but there +/// don't seem like many other options at this point. Maybe we can revisit this later. +const SESSION_RESTORE_TIMEOUT_MS = 250; + pub const Portal = struct { const Self = @This(); + // Successful restore starts should return quickly. If they stall, assume the + // portal is falling back to the interactive picker and cancel instead. allocator: std.mem.Allocator, portal: *c.XdpPortal, session: ?*c.XdpSession = null, @@ -55,12 +68,12 @@ pub const Portal = struct { return self; } - pub fn selectSource( - self: *Self, - source_type: VideoCaptureSourceType, - ) (VideoCaptureError || anyerror)!u32 { - try self.createSession(source_type); - return try self.startSession(); + pub fn selectSource(self: *Self, selection: VideoCaptureSelection) (VideoCaptureError || anyerror)!u32 { + // A stale restore token can fail before a session starts. Clear and continue. + errdefer self.clearRestoreToken(); + try self.createSession(selection); + const node_id = try self.startSession(selection); + return node_id; } pub fn openPipewireRemote(self: *const Self) !i32 { @@ -88,6 +101,7 @@ pub const Portal = struct { if (self.restore_token) |token| { self.allocator.free(token); + self.restore_token = null; } self.allocator.destroy(self); @@ -111,17 +125,20 @@ pub const Portal = struct { c.g_main_loop_quit(ctx.loop); } - fn createSession(self: *Self, source_type: VideoCaptureSourceType) !void { + fn createSession(self: *Self, selection: VideoCaptureSelection) !void { if (self.session != null) { - return; + return error.session_already_exists; } const loop = c.g_main_loop_new(null, 0) orelse return error.g_main_loop_new_failed; var ctx = CreateSessionContext{ .loop = loop }; - const outputs: c.XdpOutputType = switch (source_type) { - .desktop => c.XDP_OUTPUT_MONITOR, - .window => c.XDP_OUTPUT_WINDOW, + const outputs: c.XdpOutputType = switch (selection) { + .restore_session => c.XDP_OUTPUT_MONITOR | c.XDP_OUTPUT_WINDOW, + .source_type => |source_type| switch (source_type) { + .desktop => c.XDP_OUTPUT_MONITOR, + .window => c.XDP_OUTPUT_WINDOW, + }, }; c.xdp_portal_create_screencast_session( @@ -130,7 +147,7 @@ pub const Portal = struct { c.XDP_SCREENCAST_FLAG_NONE, c.XDP_CURSOR_MODE_EMBEDDED, c.XDP_PERSIST_MODE_PERSISTENT, - if (self.restore_token) |token| token.ptr else null, + if (selection == .restore_session and self.restore_token != null) self.restore_token.?.ptr else null, null, createSessionCallback, &ctx, @@ -155,9 +172,14 @@ pub const Portal = struct { } const StartSessionContext = struct { - loop: *c.GMainLoop, + allocator: std.mem.Allocator, + loop: ?*c.GMainLoop, session: *c.XdpSession, + cancellable: ?*c.GCancellable = null, success: bool = false, + completed: bool = false, + timed_out: bool = false, + detached: bool = false, g_error: ?*c.GError = null, }; @@ -169,28 +191,81 @@ pub const Portal = struct { _ = source_object; const ctx: *StartSessionContext = @ptrCast(@alignCast(user_data)); var err: ?*c.GError = null; - ctx.success = c.xdp_session_start_finish(ctx.session, res, &err) != 0; + const success = c.xdp_session_start_finish(ctx.session, res, &err) != 0; + + if (ctx.detached) { + if (err) |gerr| { + freeErrorMaybe(gerr); + } + return; + } + + ctx.success = success; + ctx.completed = true; ctx.g_error = err; - c.g_main_loop_quit(ctx.loop); + if (ctx.loop) |loop| { + c.g_main_loop_quit(loop); + } } - fn startSession(self: *Self) (VideoCaptureError || anyerror)!u32 { + fn restoreStartTimeoutCallback(user_data: ?*anyopaque) callconv(.c) c.gboolean { + const ctx: *StartSessionContext = @ptrCast(@alignCast(user_data)); + ctx.timed_out = true; + if (ctx.cancellable) |gc| { + c.g_cancellable_cancel(gc); + } + if (ctx.loop) |loop| { + c.g_main_loop_quit(loop); + } + return 0; + } + + fn startSession(self: *Self, selection: VideoCaptureSelection) (VideoCaptureError || anyerror)!u32 { const loop = c.g_main_loop_new(null, 0) orelse return error.g_main_loop_new_failed; - var ctx = StartSessionContext{ + const ctx = try self.allocator.create(StartSessionContext); + defer self.allocator.destroy(ctx); + ctx.* = .{ + .allocator = self.allocator, .loop = loop, .session = self.session.?, + .cancellable = if (selection == .restore_session) c.g_cancellable_new() else null, }; + var restore_timeout_id: ?c_uint = null; + defer { + if (!ctx.timed_out) { + if (restore_timeout_id) |id| { + _ = c.g_source_remove(id); + } + } + if (ctx.cancellable) |cancellable| { + c.g_object_unref(cancellable); + ctx.cancellable = null; + } + c.g_main_loop_unref(loop); + } + + if (ctx.cancellable != null) { + restore_timeout_id = c.g_timeout_add(SESSION_RESTORE_TIMEOUT_MS, restoreStartTimeoutCallback, ctx); + } + c.xdp_session_start( self.session.?, null, - null, + ctx.cancellable, startSessionCallback, - &ctx, + ctx, ); c.g_main_loop_run(loop); - c.g_main_loop_unref(loop); + ctx.loop = null; + + if (ctx.timed_out and !ctx.completed) { + // The restore attempt likely fell through to the interactive picker. + // Return immediately and let a late callback free the detached context. + ctx.detached = true; + return VideoCaptureError.source_picker_cancelled; + } if (ctx.g_error) |err| { defer freeErrorMaybe(err); @@ -204,22 +279,26 @@ pub const Portal = struct { return error.start_session_failed; } - try self.updateRestoreToken(); + try self.updateRestoreToken(selection == .source_type); return try self.processStreams(); } - fn updateRestoreToken(self: *Self) !void { + fn updateRestoreToken(self: *Self, clear_if_missing: bool) !void { const token_ptr = c.xdp_session_get_restore_token(self.session.?); defer freeMaybe(token_ptr); if (token_ptr == null) { + if (clear_if_missing) { + self.clearRestoreToken(); + } return; } const duped = try self.allocator.dupe(u8, std.mem.span(token_ptr)); - if (self.restore_token) |old| { - self.allocator.free(old); + if (self.restore_token) |restore_token| { + self.allocator.free(restore_token); + self.restore_token = null; } self.restore_token = duped; @@ -228,6 +307,16 @@ pub const Portal = struct { }; } + fn clearRestoreToken(self: *Self) void { + if (self.restore_token) |token| { + self.allocator.free(token); + self.restore_token = null; + } + TokenStorage.deleteToken(self.allocator, "restore_token") catch |err| { + log.warn("failed to delete restore token: {}", .{err}); + }; + } + fn processStreams(self: *Self) !u32 { const streams = c.xdp_session_get_streams(self.session.?); if (streams == null) { diff --git a/src/capture/video/video_capture.zig b/src/capture/video/video_capture.zig index c55162d..4820d9b 100644 --- a/src/capture/video/video_capture.zig +++ b/src/capture/video/video_capture.zig @@ -8,6 +8,11 @@ const rc = @import("zigrc"); pub const VideoCaptureSourceType = enum { window, desktop }; +pub const VideoCaptureSelection = union(enum) { + source_type: VideoCaptureSourceType, + restore_session, +}; + pub const VideoCaptureError = error{ portal_service_not_found, source_picker_cancelled, @@ -20,7 +25,8 @@ pub const VideoCapture = struct { vtable: *const VTable, const VTable = struct { - selectSource: *const fn (*anyopaque, VideoCaptureSourceType) anyerror!void, + selectSource: *const fn (*anyopaque, VideoCaptureSelection) anyerror!void, + shouldRestoreCaptureSession: *const fn (*anyopaque) anyerror!bool, nextFrame: *const fn (*anyopaque) ChanError!void, closeAllChannels: *const fn (*anyopaque) void, waitForFrame: *const fn (*anyopaque) ChanError!rc.Arc(*VulkanImageBuffer), @@ -29,8 +35,12 @@ pub const VideoCapture = struct { deinit: *const fn (*anyopaque) void, }; - pub fn selectSource(self: *Self, source_type: VideoCaptureSourceType) (VideoCaptureError || anyerror)!void { - return self.vtable.selectSource(self.ptr, source_type); + pub fn selectSource(self: *Self, selection: VideoCaptureSelection) (VideoCaptureError || anyerror)!void { + return self.vtable.selectSource(self.ptr, selection); + } + + pub fn shouldRestoreCaptureSession(self: *Self) !bool { + return self.vtable.shouldRestoreCaptureSession(self.ptr); } pub fn nextFrame(self: *Self) ChanError!void { diff --git a/src/capture/video/windows/capture_windows.zig b/src/capture/video/windows/capture_windows.zig deleted file mode 100644 index 2180302..0000000 --- a/src/capture/video/windows/capture_windows.zig +++ /dev/null @@ -1,158 +0,0 @@ -const std = @import("std"); - -const vk = @import("vulkan"); - -const types = @import("../../../types.zig"); -const util = @import("../../../util.zig"); -const Vulkan = @import("../../../vulkan/vulkan.zig").Vulkan; -const VideoCaptureSourceType = @import("../video_capture.zig").VideoCaptureSourceType; -const VideoCapture = @import("../video_capture.zig").VideoCapture; -const VulkanImageBuffer = @import("../../../vulkan/vulkan_image_buffer.zig").VulkanImageBuffer; -const ChanError = @import("../../../channel.zig").ChanError; -const rc = @import("zigrc"); - -const DWORD = u32; - -pub const WindowsVideoCapture = struct { - const Self = @This(); - const DWORD = u32; - - allocator: std.mem.Allocator, - vulkan: *Vulkan, - - pub fn init(allocator: std.mem.Allocator, vulkan: *Vulkan) !*Self { - const self = try allocator.create(Self); - self.* = Self{ - .allocator = allocator, - .vulkan = vulkan, - }; - return self; - } - - pub fn selectSource(context: *anyopaque, source_type: VideoCaptureSourceType) !void { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - _ = source_type; - } - - pub fn waitForReady(context: *anyopaque) !void { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - } - - pub fn nextFrame(context: *anyopaque) ChanError!void { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - } - - pub fn closeAllChannels(context: *anyopaque) void { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - } - - pub fn waitForFrame(context: *anyopaque) ChanError!rc.Arc(*VulkanImageBuffer) { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - return ChanError.Closed; - } - - pub fn size(context: *anyopaque) ?types.Size { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - return null; - } - - pub fn stop(context: *anyopaque) !void { - const self: *Self = @ptrCast(@alignCast(context)); - _ = self; - } - - // pub fn selectedScreenCastIdentifier(self: *anyopaque) ?[]const u8 { - // const self: *Self = @ptrCast(@alignCast(context)); - // _ = self; - // return null; - // } - - pub fn deinit(context: *anyopaque) void { - const self: *Self = @ptrCast(@alignCast(context)); - self.allocator.destroy(self); - } - - pub fn videoCapture(self: *Self) VideoCapture { - return .{ - .ptr = self, - .vtable = &.{ - .selectSource = selectSource, - .nextFrame = nextFrame, - .closeAllChannels = closeAllChannels, - .waitForFrame = waitForFrame, - .size = size, - .stop = stop, - .deinit = deinit, - }, - }; - } -}; - -/// Capture a screenshot in windows - the caller owns the returned memory -pub fn windowsCapture(allocator: std.mem.Allocator) ![]u8 { - const win32 = @import("win32"); - const c = win32.everything; - - const width: i32 = c.GetSystemMetrics(c.SM_CXSCREEN); - const height: i32 = c.GetSystemMetrics(c.SM_CYSCREEN); - - const hScreenDC: c.HDC = c.GetDC(null) orelse unreachable; - defer _ = c.DeleteObject(@ptrCast(hScreenDC)); - const hMemoryDC: c.HDC = c.CreateCompatibleDC(hScreenDC); - defer _ = c.DeleteObject(@ptrCast(hMemoryDC)); - const hBitmap: c.HBITMAP = c.CreateCompatibleBitmap(hScreenDC, width, height) orelse unreachable; - defer _ = c.DeleteObject(hBitmap); - - _ = c.SelectObject(hMemoryDC, hBitmap); - _ = c.BitBlt(hMemoryDC, 0, 0, width, height, hScreenDC, 0, 0, c.SRCCOPY); - - const bitMapInfoHeader = win32.everything.BITMAPINFOHEADER{ - .biSize = @sizeOf(win32.everything.BITMAPINFOHEADER), - .biWidth = width, - .biHeight = -height, - .biPlanes = 1, - .biBitCount = 32, - .biCompression = win32.everything.BI_RGB, - .biSizeImage = 0, - .biXPelsPerMeter = 0, - .biYPelsPerMeter = 0, - .biClrUsed = 0, - .biClrImportant = 0, - }; - - const dwBmpSize: DWORD = @intCast(@divFloor((width * bitMapInfoHeader.biBitCount + 31), 32) * 4 * height); - - const lpbitmap = try allocator.alloc(u8, @intCast(dwBmpSize)); - - _ = c.GetDIBits( - hMemoryDC, - hBitmap, - 0, - @intCast(height), - @ptrCast(lpbitmap), - @ptrCast(@constCast(&bitMapInfoHeader)), - c.DIB_RGB_COLORS, - ); - - // Write to file - const bitMapFileHeader = win32.everything.BITMAPFILEHEADER{ - .bfOffBits = @sizeOf(c.BITMAPFILEHEADER) + @sizeOf(c.BITMAPINFOHEADER), - .bfReserved1 = 0, - .bfReserved2 = 0, - .bfSize = dwBmpSize + @sizeOf(c.BITMAPFILEHEADER) + @sizeOf(c.BITMAPINFOHEADER), - .bfType = 0x4D42, // 'BM' - }; - - var out = std.ArrayList(u8).init(allocator); - try out.appendSlice(std.mem.asBytes(&bitMapFileHeader)); - try out.appendSlice(std.mem.asBytes(&bitMapInfoHeader)); - try out.appendSlice(lpbitmap); - - return try out.toOwnedSlice(); -} diff --git a/src/capture/video/windows/windows_video_capture.zig b/src/capture/video/windows/windows_video_capture.zig index 2180302..e537a79 100644 --- a/src/capture/video/windows/windows_video_capture.zig +++ b/src/capture/video/windows/windows_video_capture.zig @@ -5,7 +5,7 @@ const vk = @import("vulkan"); const types = @import("../../../types.zig"); const util = @import("../../../util.zig"); const Vulkan = @import("../../../vulkan/vulkan.zig").Vulkan; -const VideoCaptureSourceType = @import("../video_capture.zig").VideoCaptureSourceType; +const VideoCaptureSelection = @import("../video_capture.zig").VideoCaptureSelection; const VideoCapture = @import("../video_capture.zig").VideoCapture; const VulkanImageBuffer = @import("../../../vulkan/vulkan_image_buffer.zig").VulkanImageBuffer; const ChanError = @import("../../../channel.zig").ChanError; @@ -29,10 +29,16 @@ pub const WindowsVideoCapture = struct { return self; } - pub fn selectSource(context: *anyopaque, source_type: VideoCaptureSourceType) !void { + pub fn selectSource(context: *anyopaque, selection: VideoCaptureSelection) !void { const self: *Self = @ptrCast(@alignCast(context)); _ = self; - _ = source_type; + _ = selection; + } + + pub fn shouldRestoreCaptureSession(context: *anyopaque) !bool { + const self: *Self = @ptrCast(@alignCast(context)); + _ = self; + return false; } pub fn waitForReady(context: *anyopaque) !void { @@ -83,6 +89,7 @@ pub const WindowsVideoCapture = struct { .ptr = self, .vtable = &.{ .selectSource = selectSource, + .shouldRestoreCaptureSession = shouldRestoreCaptureSession, .nextFrame = nextFrame, .closeAllChannels = closeAllChannels, .waitForFrame = waitForFrame, diff --git a/src/common/linux/token_storage.zig b/src/common/linux/token_storage.zig index 06ee161..b0f0e2f 100644 --- a/src/common/linux/token_storage.zig +++ b/src/common/linux/token_storage.zig @@ -71,3 +71,22 @@ pub fn saveToken( try file.writeAll(token_value); } + +/// Delete token file from user app directory. Ignore if file not found. +pub fn deleteToken( + allocator: std.mem.Allocator, + token_file_name: []const u8, +) !void { + const dir = try util.getAppDataDir(allocator); + defer allocator.free(dir); + const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}.txt", .{ + dir, + token_file_name, + }); + defer allocator.free(file_path); + + std.fs.deleteFileAbsolute(file_path) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; +} diff --git a/src/state/audio_state.zig b/src/state/audio_state.zig index a81376a..614570d 100644 --- a/src/state/audio_state.zig +++ b/src/state/audio_state.zig @@ -17,7 +17,7 @@ pub const AUDIO_GAIN_MIN: f32 = 0.0; pub const AUDIO_GAIN_MAX: f32 = 2.0; pub const AudioActions = union(enum) { - start_record_thread, + start_capture_thread, /// Use the capture interface to get all available audio devices on the system. get_available_audio_devices, /// Toggle recording on an audio device by device ID. @@ -46,7 +46,7 @@ pub const AudioState = struct { /// This is a list of all currently available audio devices. devices: std.ArrayList(AudioDeviceViewModel), replay_buffer: Mutex(?*AudioReplayBuffer) = .init(null), - record_thread: ?std.Thread = null, + capture_thread: ?std.Thread = null, pub fn init(allocator: Allocator, audio_capture: *AudioCapture) !Self { return .{ @@ -57,8 +57,8 @@ pub const AudioState = struct { } pub fn deinit(self: *Self) void { - self.stopRecordThread(); - assert(self.record_thread == null); + self.stopCaptureThread(); + assert(self.capture_thread == null); { var replay_buffer_locked = self.replay_buffer.lock(); @@ -75,10 +75,10 @@ pub const AudioState = struct { pub fn handleActions(self: *Self, state_actor: *StateActor, action: AudioActions) !void { switch (action) { - .start_record_thread => { - // This should only ever get called once, so the record_thread must always be null. - assert(self.record_thread == null); - self.record_thread = try std.Thread.spawn(.{}, recordThreadHandler, .{ self, state_actor }); + .start_capture_thread => { + // This should only ever get called once, so the capture_thread must always be null. + assert(self.capture_thread == null); + self.capture_thread = try std.Thread.spawn(.{}, captureThreadHandler, .{ self, state_actor }); }, .get_available_audio_devices => { var available_devices = try self.audio_capture.getAvailableDevices(self.allocator); @@ -156,13 +156,13 @@ pub const AudioState = struct { } } - fn stopRecordThread(self: *Self) void { - if (self.record_thread) |record_thread| { + fn stopCaptureThread(self: *Self) void { + if (self.capture_thread) |capture_thread| { self.audio_capture.stop() catch |err| { - log.err("[stopRecordThread] audio_capture.stop error: {}", .{err}); + log.err("[stopCaptureThread] audio_capture.stop error: {}", .{err}); }; - record_thread.join(); - self.record_thread = null; + capture_thread.join(); + self.capture_thread = null; } } @@ -200,7 +200,7 @@ pub const AudioState = struct { self.devices.clearRetainingCapacity(); } - fn recordThreadHandler(self: *Self, state_actor: *StateActor) !void { + fn captureThreadHandler(self: *Self, state_actor: *StateActor) !void { { var replay_buffer_locked = self.replay_buffer.lock(); defer replay_buffer_locked.unlock(); @@ -212,10 +212,10 @@ pub const AudioState = struct { while (true) { const data = self.audio_capture.receiveData() catch |err| { if (err == ChanError.Closed) { - log.debug("[startRecordThreadHandler] chan closed", .{}); + log.debug("[captureThreadHandler] chan closed", .{}); break; } - log.err("[startRecordThreadHandler] data_chan error: {}", .{err}); + log.err("[captureThreadHandler] data_chan error: {}", .{err}); return err; }; @@ -233,7 +233,7 @@ pub const AudioState = struct { } } - log.err("[recordThreadHandler] Unable to find device ({s}) in available devices. This should never happen.", .{data.id}); + log.err("[captureThreadHandler] Unable to find device ({s}) in available devices. This should never happen.", .{data.id}); assert(false); // Return a default value to keep the compiler happy. We should never reach this point anyway. diff --git a/src/state_actor.zig b/src/state_actor.zig index 2045637..7a1adbf 100644 --- a/src/state_actor.zig +++ b/src/state_actor.zig @@ -6,6 +6,7 @@ const vk = @import("vulkan"); const Util = @import("./util.zig"); const VideoCapture = @import("./capture/video/video_capture.zig").VideoCapture; const VideoCaptureError = @import("./capture/video/video_capture.zig").VideoCaptureError; +const VideoCaptureSelection = @import("./capture/video/video_capture.zig").VideoCaptureSelection; const VideoCaptureSourceType = @import("./capture/video/video_capture.zig").VideoCaptureSourceType; const AudioCapture = @import("./capture/audio/audio_capture.zig").AudioCapture; const SAMPLE_RATE = @import("./capture/audio/audio_capture.zig").SAMPLE_RATE; @@ -27,6 +28,8 @@ pub const Actions = union(enum) { start_record, stop_record, select_video_source: VideoCaptureSourceType, + /// Restore the capture session on startup. + restore_capture_session, save_replay, show_demo, exit, @@ -173,7 +176,8 @@ pub const StateActor = struct { try self.thread_pool.init(.{ .allocator = allocator, .n_jobs = 10 }); try self.dispatch(.{ .audio = .get_available_audio_devices }); - try self.dispatch(.{ .audio = .start_record_thread }); + try self.dispatch(.{ .audio = .start_capture_thread }); + try self.dispatch(.restore_capture_session); return self; } @@ -293,19 +297,15 @@ pub const StateActor = struct { ); }, .select_video_source => |source_type| { - try self.stopCapture(); - - self.video_capture.selectSource(source_type) catch |err| { - if (err != VideoCaptureError.source_picker_cancelled) { - log.err("selectSource error: {}\n", .{err}); - return err; - } else { - log.info("source_picker_cancelled\n", .{}); - } + try self.selectVideoSource(.{ .source_type = source_type }); + }, + .restore_capture_session => { + if (!try self.video_capture.shouldRestoreCaptureSession()) { return; - }; + } - try self.startCapture(); + log.debug("Restoring capture session.", .{}); + try self.selectVideoSource(.restore_session); }, .show_demo => { self.ui_mutex.lock(); @@ -327,6 +327,22 @@ pub const StateActor = struct { } } + fn selectVideoSource(self: *Self, selection: VideoCaptureSelection) !void { + try self.stopCapture(); + + self.video_capture.selectSource(selection) catch |err| { + if (err != VideoCaptureError.source_picker_cancelled) { + log.err("selectSource error: {}\n", .{err}); + return err; + } else { + log.info("source_picker_cancelled\n", .{}); + } + return; + }; + + try self.startCapture(); + } + pub fn globalShortcutsHandler(context: *anyopaque, shortcut: GlobalShortcuts.Shortcut) void { const self: *Self = @ptrCast(@alignCast(context)); switch (shortcut) {