From 85a69d9e8aaf9975c50203d0c6563daf30e9f214 Mon Sep 17 00:00:00 2001 From: Adryzz Date: Mon, 31 Oct 2022 00:28:18 +0100 Subject: [PATCH 1/4] add initial window capture support --- SeeShark/Device/Window.cs | 13 +++ SeeShark/Device/WindowInfo.cs | 14 +++ SeeShark/Device/WindowManager.cs | 145 +++++++++++++++++++++++++++++ SeeShark/Interop/Windows/User32.cs | 12 +++ SeeShark/Interop/X11/XLib.cs | 18 ++-- SeeShark/VideoInputOptions.cs | 5 + 6 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 SeeShark/Device/Window.cs create mode 100644 SeeShark/Device/WindowInfo.cs create mode 100644 SeeShark/Device/WindowManager.cs diff --git a/SeeShark/Device/Window.cs b/SeeShark/Device/Window.cs new file mode 100644 index 0000000..4392c4b --- /dev/null +++ b/SeeShark/Device/Window.cs @@ -0,0 +1,13 @@ +// Copyright (c) The Vignette Authors +// This file is part of SeeShark. +// SeeShark is licensed under the BSD 3-Clause License. See LICENSE for details. + +namespace SeeShark.Device; + +public class Window : VideoDevice +{ + public Window(VideoDeviceInfo info, DeviceInputFormat inputFormat, VideoInputOptions? options = null) + : base(info, inputFormat, options) + { + } +} diff --git a/SeeShark/Device/WindowInfo.cs b/SeeShark/Device/WindowInfo.cs new file mode 100644 index 0000000..dc7493e --- /dev/null +++ b/SeeShark/Device/WindowInfo.cs @@ -0,0 +1,14 @@ +// Copyright (c) The Vignette Authors +// This file is part of SeeShark. +// SeeShark is licensed under the BSD 3-Clause License. See LICENSE for details. + +using System; + +namespace SeeShark.Device; + +public class WindowInfo : VideoDeviceInfo +{ + public string Title { get; init; } = string.Empty; + + public IntPtr Id { get; init; } +} diff --git a/SeeShark/Device/WindowManager.cs b/SeeShark/Device/WindowManager.cs new file mode 100644 index 0000000..0e743ad --- /dev/null +++ b/SeeShark/Device/WindowManager.cs @@ -0,0 +1,145 @@ +// Copyright (c) The Vignette Authors +// This file is part of SeeShark. +// SeeShark is licensed under the BSD 3-Clause License. See LICENSE for details. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SeeShark.Interop.Windows; +using SeeShark.Interop.X11; + +namespace SeeShark.Device; + +public class WindowManager : VideoDeviceManager +{ + public static DeviceInputFormat DefaultInputFormat + { + get + { + return OperatingSystem.IsWindows() ? DeviceInputFormat.GdiGrab + : OperatingSystem.IsLinux() ? DeviceInputFormat.X11Grab + : OperatingSystem.IsMacOS() ? DeviceInputFormat.AVFoundation + : throw new NotSupportedException( + $"Cannot find adequate display input format for RID '{RuntimeInformation.RuntimeIdentifier}'."); + } + } + + + public WindowManager(DeviceInputFormat? inputFormat = null) : base(inputFormat ?? DefaultInputFormat) + { + } + + public override Window GetDevice(WindowInfo info, VideoInputOptions? options = null) + { + if (options is { } o) + { + return new Window(info, InputFormat, o); + } + else + { + return new Window(info, InputFormat, generateInputOptions(info)); + } + } + + /// + /// Enumerates available devices. + /// + protected override WindowInfo[] EnumerateDevices() + { + switch (InputFormat) + { + case DeviceInputFormat.X11Grab: + return enumerateDevicesX11(); + case DeviceInputFormat.GdiGrab: + return enumerateDevicesGdi(); + default: + return base.EnumerateDevices(); + } + } + + private WindowInfo[] enumerateDevicesX11() + { + List windows = new List(); + unsafe + { + IntPtr display = XLib.XOpenDisplay(null); + IntPtr rootWindow = XLib.XDefaultRootWindow(display); + findWindowsX11(display, rootWindow, ref windows); + } + + return windows.ToArray(); + } + + void findWindowsX11(IntPtr display, IntPtr window, ref List windows) + { + IntPtr[] childWindows = Array.Empty(); + + XLib.XQueryTree(display, window, out IntPtr rootWindow, out IntPtr parentWindow, out childWindows, + out int nChildren); + + childWindows = new IntPtr[nChildren]; + + XLib.XQueryTree(display, window, + out rootWindow, out parentWindow, + out childWindows, out nChildren); + + XLib.XFetchName(display, window, out string title); + + windows.Add(new WindowInfo + { + Path = ":0", + Title = title, + Id = window + }); + + for (int i = 0; i < childWindows.Length; i++) + { + XLib.XFetchName(display, childWindows[i], out string childTitle); + + windows.Add(new WindowInfo + { + Path = ":0", + Title = childTitle, + Id = childWindows[i] + }); + + findWindowsX11(display, childWindows[i], ref windows); + } + } + + private WindowInfo[] enumerateDevicesGdi() + { + List windows = new List(); + User32.EnumWindows(delegate(IntPtr wnd, IntPtr param) + { + int size = User32.GetWindowTextLength(wnd); + string title = string.Empty; + if (size > 0) + { + var builder = new StringBuilder(size + 1); + User32.GetWindowText(wnd, builder, builder.Capacity); + title = builder.ToString(); + } + + windows.Add(new WindowInfo + { + Path = $"title={title}", + Title = title, + Id = wnd + }); + return true; + }, IntPtr.Zero); + + return windows.ToArray(); + } + + private VideoInputOptions generateInputOptions(WindowInfo info) + { + return new VideoInputOptions + { + WindowId = info.Id.ToString() + }; + } +} diff --git a/SeeShark/Interop/Windows/User32.cs b/SeeShark/Interop/Windows/User32.cs index d994206..35669c5 100644 --- a/SeeShark/Interop/Windows/User32.cs +++ b/SeeShark/Interop/Windows/User32.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace SeeShark.Interop.Windows; @@ -145,4 +146,15 @@ internal static partial class User32 [DllImport("Shcore.dll")] internal static extern int SetProcessDpiAwareness(int awareness); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); + + internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); } diff --git a/SeeShark/Interop/X11/XLib.cs b/SeeShark/Interop/X11/XLib.cs index 5922a2f..48e7a2d 100644 --- a/SeeShark/Interop/X11/XLib.cs +++ b/SeeShark/Interop/X11/XLib.cs @@ -18,24 +18,30 @@ internal class XLib [DllImport(lib_x11, EntryPoint = "XOpenDisplay")] private static extern unsafe Display sys_XOpenDisplay(sbyte* display); - public static unsafe Display XOpenDisplay(sbyte* display) + internal static unsafe Display XOpenDisplay(sbyte* display) { lock (displayLock) return sys_XOpenDisplay(display); } [DllImport(lib_x11, EntryPoint = "XCloseDisplay")] - public static extern int XCloseDisplay(Display display); + internal static extern int XCloseDisplay(Display display); [DllImport(lib_x11, EntryPoint = "XDefaultRootWindow")] - public static extern Window XDefaultRootWindow(Display display); + internal static extern Window XDefaultRootWindow(Display display); [DllImport(lib_x11, EntryPoint = "XDisplayWidth")] - public static extern int XDisplayWidth(Display display, int screenNumber); + internal static extern int XDisplayWidth(Display display, int screenNumber); [DllImport(lib_x11, EntryPoint = "XDisplayHeight")] - public static extern int XDisplayHeight(Display display, int screenNumber); + internal static extern int XDisplayHeight(Display display, int screenNumber); [DllImport(lib_x11, EntryPoint = "XGetAtomName")] - public static extern IntPtr XGetAtomName(Display display, Atom atom); + internal static extern IntPtr XGetAtomName(Display display, Atom atom); + + [DllImport(lib_x11, EntryPoint = "XQueryTree")] + internal static extern int XQueryTree(IntPtr display, IntPtr w, out IntPtr rootReturn, out IntPtr parentReturn, out IntPtr[] childrenReturn, out int nChildrenReturn); + + [DllImport(lib_x11, EntryPoint = "XFetchName")] + internal static extern int XFetchName(IntPtr display, IntPtr w, out string windowNameReturn); } diff --git a/SeeShark/VideoInputOptions.cs b/SeeShark/VideoInputOptions.cs index 1acd6e5..c32b465 100644 --- a/SeeShark/VideoInputOptions.cs +++ b/SeeShark/VideoInputOptions.cs @@ -59,6 +59,11 @@ public class VideoInputOptions /// public bool DrawMouse { get; set; } = true; + /// + /// Used in Linux only - The ID of the window to capture + /// + public string? WindowId { get; set; } + /// /// Combines all properties into a dictionary of options that FFmpeg can use. /// From 21cc0c121fee1b897ecd4df0cb0b9bfe6e5c9325 Mon Sep 17 00:00:00 2001 From: Adryzz Date: Mon, 31 Oct 2022 00:40:08 +0100 Subject: [PATCH 2/4] x11 option --- SeeShark.Example.Ascii/Program.cs | 8 ++++---- SeeShark/Device/WindowManager.cs | 2 +- SeeShark/VideoInputOptions.cs | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/SeeShark.Example.Ascii/Program.cs b/SeeShark.Example.Ascii/Program.cs index 2f40237..b50e5d5 100644 --- a/SeeShark.Example.Ascii/Program.cs +++ b/SeeShark.Example.Ascii/Program.cs @@ -14,8 +14,8 @@ namespace SeeShark.Example.Ascii; class Program { - static Camera? karen; - static CameraManager? manager; + static Window? karen; + static WindowManager? manager; static FrameConverter? converter; static void Main(string[] args) @@ -43,9 +43,9 @@ static void Main(string[] args) Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32"); Console.WriteLine($"FFmpeg version info: {FFmpegVersion}"); - manager = new CameraManager(); + manager = new WindowManager(); - CameraInfo device; + WindowInfo device; if (args.Length < 1) { /// Select an available camera device. diff --git a/SeeShark/Device/WindowManager.cs b/SeeShark/Device/WindowManager.cs index 0e743ad..c1fbe3e 100644 --- a/SeeShark/Device/WindowManager.cs +++ b/SeeShark/Device/WindowManager.cs @@ -139,7 +139,7 @@ private VideoInputOptions generateInputOptions(WindowInfo info) { return new VideoInputOptions { - WindowId = info.Id.ToString() + WindowId = $"0x{new IntPtr(0x3600003).ToString("X2")}" }; } } diff --git a/SeeShark/VideoInputOptions.cs b/SeeShark/VideoInputOptions.cs index c32b465..f64a542 100644 --- a/SeeShark/VideoInputOptions.cs +++ b/SeeShark/VideoInputOptions.cs @@ -112,6 +112,12 @@ public class VideoInputOptions } } + if (WindowId != null) + { + if (deviceFormat == DeviceInputFormat.X11Grab) + dict.Add("window_id", WindowId); + } + switch (deviceFormat) { case DeviceInputFormat.X11Grab: From 31879fd3c9822f158696482727fbb2b775418baf Mon Sep 17 00:00:00 2001 From: Adryzz Date: Mon, 31 Oct 2022 01:11:18 +0100 Subject: [PATCH 3/4] remove useless no-name junk from window list --- SeeShark/Device/WindowManager.cs | 11 ++++++++++- SeeShark/Interop/Windows/User32.cs | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/SeeShark/Device/WindowManager.cs b/SeeShark/Device/WindowManager.cs index c1fbe3e..cad19d8 100644 --- a/SeeShark/Device/WindowManager.cs +++ b/SeeShark/Device/WindowManager.cs @@ -112,9 +112,18 @@ void findWindowsX11(IntPtr display, IntPtr window, ref List windows) private WindowInfo[] enumerateDevicesGdi() { List windows = new List(); - User32.EnumWindows(delegate(IntPtr wnd, IntPtr param) + + IntPtr shellWindow = User32.GetShellWindow(); + + User32.EnumDesktopWindows(IntPtr.Zero, delegate(IntPtr wnd, IntPtr param) { + if (wnd == shellWindow || !User32.IsWindowVisible(wnd)) + return true; + int size = User32.GetWindowTextLength(wnd); + + if (size <= 1) return true; + string title = string.Empty; if (size > 0) { diff --git a/SeeShark/Interop/Windows/User32.cs b/SeeShark/Interop/Windows/User32.cs index 35669c5..5946a3b 100644 --- a/SeeShark/Interop/Windows/User32.cs +++ b/SeeShark/Interop/Windows/User32.cs @@ -154,7 +154,13 @@ internal static partial class User32 internal static extern int GetWindowTextLength(IntPtr hWnd); [DllImport("user32.dll")] - internal static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); + internal static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsProc enumProc, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern IntPtr GetShellWindow(); internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); } From 6a0573f0921daab5ff4ac975b86a43273d2b920c Mon Sep 17 00:00:00 2001 From: Adryzz Date: Mon, 31 Oct 2022 12:14:57 +0100 Subject: [PATCH 4/4] add fallback fps value --- SeeShark/Device/VideoDevice.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SeeShark/Device/VideoDevice.cs b/SeeShark/Device/VideoDevice.cs index c49aabf..5ce02f6 100644 --- a/SeeShark/Device/VideoDevice.cs +++ b/SeeShark/Device/VideoDevice.cs @@ -84,8 +84,14 @@ public DecodeStatus TryGetFrame(out Frame frame) // See https://github.com/vignetteapp/SeeShark/issues/29 // (RIP big brain move to avoid overloading the CPU...) + + // The decoder frame rate is just a guess from FFmpeg, so when there is no guess, we go to + // a fallback value (60fps) to avoid dividing by zero. + int den = decoder.Framerate.den == 0 ? 1 : decoder.Framerate.den; + int num = decoder.Framerate.num == 0 ? 60 : decoder.Framerate.num; + if (status == DecodeStatus.NoFrameAvailable) - Thread.Sleep(1000 * decoder.Framerate.den / (decoder.Framerate.num * 4)); + Thread.Sleep(1000 * den / (num * 4)); return status; }